diff --git a/.github/renovate.json b/.github/renovate.json index ca74a436e84..00447475c95 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,29 +1,29 @@ { - "$schema" : "https://docs.renovatebot.com/renovate-schema.json", - "extends" : [ - "config:base" + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" ], - "labels" : [ + "labels": [ "PR-Dependencies" ], - "ignoreDeps" : [ + "ignoreDeps": [ "string:app_name", "gradle" ], - "packageRules" : [ + "packageRules": [ { - "matchPackagePatterns" : [ - "^org.jetbrains.kotlin", - "^com.google.devtools.ksp", - "^androidx.compose.compiler" - ], - "groupName" : "kotlin" + "groupName": "kotlin", + "matchPackageNames": [ + "/^org.jetbrains.kotlin/", + "/^com.google.devtools.ksp/", + "/^androidx.compose.compiler/" + ] }, { - "matchPackagePatterns" : [ - "^org.maplibre" - ], - "versioning" : "semver" + "versioning": "semver", + "matchPackageNames": [ + "/^org.maplibre/" + ] } ] } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ce7acf8809..1668cd2b1fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,10 @@ jobs: 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_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' }} @@ -65,7 +69,7 @@ jobs: retention-days: 5 overwrite: true if-no-files-found: error - - uses: rnkdsh/action-upload-diawi@v1.5.6 + - uses: rnkdsh/action-upload-diawi@605adbad0db6c000eee26adfd8fc128d7df8f7ab # v1.5.9 id: diawi # Do not fail the whole build if Diawi upload fails continue-on-error: true @@ -77,7 +81,7 @@ jobs: 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@v2 + 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. @@ -89,7 +93,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Compile release sources if: ${{ matrix.variant == 'release' }} - run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES + 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 index 9182a9af3c7..a1ec5063c0d 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -33,7 +33,7 @@ jobs: # 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@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 with: ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - name: Clone submodules @@ -54,6 +54,10 @@ jobs: 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_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' }} diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 6b6ac8243e3..9de62e104c1 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 with: ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - name: Clone submodules @@ -20,7 +20,7 @@ jobs: - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger - uses: danger/danger-js@12.3.3 + uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4 with: args: "--dangerfile ./tools/danger/dangerfile.js" env: diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml index e63cb52cc89..1f5551ca8ca 100644 --- a/.github/workflows/generate_github_pages.yml +++ b/.github/workflows/generate_github_pages.yml @@ -12,7 +12,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} steps: - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@v1.2.3 + uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 - name: Use JDK 21 uses: actions/setup-java@v4 with: @@ -32,7 +32,7 @@ jobs: mkdir -p screenshots/en cp tests/uitests/src/test/snapshots/images/* screenshots/en - name: Deploy GitHub Pages - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./screenshots diff --git a/.github/workflows/gradle-wrapper-update.yml b/.github/workflows/gradle-wrapper-update.yml index f5a5b3c94cb..dcc7bc5f0a3 100644 --- a/.github/workflows/gradle-wrapper-update.yml +++ b/.github/workflows/gradle-wrapper-update.yml @@ -19,7 +19,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Update Gradle Wrapper - uses: gradle-update/update-gradle-wrapper-action@v2 + uses: gradle-update/update-gradle-wrapper-action@512b1875f3b6270828abfe77b247d5895a2da1e5 # v2.1.0 with: repo-token: ${{ secrets.DANGER_GITHUB_API_TOKEN }} target-branch: develop diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml index b07eb196fdd..cfb743934f2 100644 --- a/.github/workflows/maestro-local.yml +++ b/.github/workflows/maestro-local.yml @@ -11,7 +11,7 @@ env: CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache ARCH: x86_64 DEVICE: pixel_7_pro - API_LEVEL: 35 + API_LEVEL: 33 TARGET: google_apis jobs: @@ -56,7 +56,7 @@ jobs: maestro-cloud: name: Maestro test suite runs-on: ubuntu-latest - needs: [build-apk] + needs: [ build-apk ] # Allow one per PR. concurrency: group: ${{ format('maestro-{0}', github.ref) }} @@ -80,6 +80,7 @@ jobs: - 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@v2 continue-on-error: true env: @@ -109,3 +110,8 @@ jobs: retention-days: 5 overwrite: true if-no-files-found: error + - 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 index 465a95bd087..d82129e18d2 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -30,6 +30,10 @@ jobs: 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_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 }} diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 55293f972f6..7cc4f766c21 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -18,7 +18,7 @@ jobs: if: ${{ github.repository == 'element-hq/element-x-android' }} steps: - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@v1.2.3 + uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 - name: Use JDK 21 uses: actions/setup-java@v4 diff --git a/.github/workflows/nightly_enterprise.yml b/.github/workflows/nightly_enterprise.yml deleted file mode 100644 index c7700ed56b3..00000000000 --- a/.github/workflows/nightly_enterprise.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Build and release Enterprise nightly application - -on: - workflow_dispatch: - schedule: - # Every nights at 4 - - cron: "0 4 * * *" - -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -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 Enterprise nightly bundle to Firebase - runs-on: ubuntu-latest - if: ${{ github.repository == 'element-hq/element-x-android' }} - steps: - - uses: actions/checkout@v4 - - name: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@v0.9.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@v4 - 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_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/post-release.yml b/.github/workflows/post-release.yml new file mode 100644 index 00000000000..56d40d0cf6c --- /dev/null +++ b/.github/workflows/post-release.yml @@ -0,0 +1,28 @@ +name: Post-release + +on: + push: + tags: + - 'v*' + +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@v7 + 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 new file mode 100644 index 00000000000..929bcb5dcd0 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,77 @@ +name: Pull Request +on: + pull_request_target: + types: [ opened, edited, labeled, unlabeled, synchronize ] + workflow_call: + secrets: + ELEMENT_BOT_TOKEN: + required: true + +jobs: + prevent-blocked: + name: Prevent blocked + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - name: Add notice + uses: actions/github-script@v7 + 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@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3 + 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@v7 + 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 + 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@v7 + 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 index b69c693f197..bb6bbe87d1b 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 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 }} @@ -29,7 +29,7 @@ jobs: - name: Run code quality check suite run: ./tools/check/check_code_quality.sh - checkScreesnhot: + checkScreenshot: name: Search for invalid screenshot files runs-on: ubuntu-latest steps: @@ -77,7 +77,7 @@ jobs: # 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@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 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 }} @@ -117,7 +117,7 @@ jobs: # 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@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 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 }} @@ -138,7 +138,7 @@ jobs: - name: Build Fdroid Debug run: ./gradlew :app:compileFdroidDebugKotlin $CI_GRADLE_ARG_PROPERTIES - name: Run lint - run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug $CI_GRADLE_ARG_PROPERTIES + run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue - name: Upload reports if: always() uses: actions/upload-artifact@v4 @@ -161,7 +161,7 @@ jobs: # 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@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 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 }} @@ -201,7 +201,7 @@ jobs: # 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@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 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 }} @@ -241,7 +241,7 @@ jobs: # 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@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 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 }} @@ -270,7 +270,6 @@ jobs: - name: Run shellcheck uses: ludeeus/action-shellcheck@2.0.0 with: - scandir: ./tools severity: warning upload_reports: @@ -294,7 +293,7 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@12.3.3 + uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4 with: args: "--dangerfile ./tools/danger/dangerfile-lint.js" env: diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 17012282e3f..a5172188ee0 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -19,18 +19,18 @@ jobs: steps: - name: Remove Record-Screenshots label if: github.event.label.name == 'Record-Screenshots' - uses: actions-ecosystem/action-remove-labels@v1 + 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@v1.2.3 + uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 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@v1.2.3 + uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 with: persist-credentials: false - name: ☕️ Use JDK 21 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9de267d3f1..704802258ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,10 @@ jobs: 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_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@v4 @@ -50,7 +54,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 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 }} diff --git a/.github/workflows/scripts/maestro/local-recording.sh b/.github/workflows/scripts/maestro/local-recording.sh index 4a800dc5e71..6534d2feca3 100755 --- a/.github/workflows/scripts/maestro/local-recording.sh +++ b/.github/workflows/scripts/maestro/local-recording.sh @@ -12,8 +12,8 @@ mkdir -p /data/local/tmp/recordings; FILENAME=/data/local/tmp/recordings/testRecording$COUNT.mp4 while true do - ((COUNT++)) + COUNT=$((COUNT+1)) FILENAME=/data/local/tmp/recordings/testRecording$COUNT.mp4 - echo "\nRecording video file #$COUNT" + printf "\nRecording video file #%d\n" $COUNT screenrecord --bugreport --bit-rate=16m --size 720x1280 $FILENAME done diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index dc688cb5a47..3e9e4692a43 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -36,7 +36,7 @@ jobs: ./tools/localazy/importSupportedLocalesFromLocalazy.py ./tools/test/generateAllScreenshots.py - name: Create Pull Request for Strings - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: token: ${{ secrets.DANGER_GITHUB_API_TOKEN }} commit-message: Sync Strings from Localazy diff --git a/.github/workflows/sync-sas-strings.yml b/.github/workflows/sync-sas-strings.yml index 632eb5d8ec8..f9224853fd5 100644 --- a/.github/workflows/sync-sas-strings.yml +++ b/.github/workflows/sync-sas-strings.yml @@ -23,7 +23,7 @@ jobs: - 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@v7 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: commit-message: Sync SAS Strings title: Sync SAS Strings diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ef7a8843c27..a0ffe74f6f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,13 +33,13 @@ jobs: sudo swapon /mnt/swapfile sudo swapon --show - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@v1.2.3 + uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 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@v0.9.0 + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 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 }} @@ -82,7 +82,7 @@ jobs: # https://github.com/codecov/codecov-action - name: ☂️ Upload coverage reports to codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 109b8cc82d9..34c9767da31 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -8,7 +8,7 @@ jobs: triage-new-issues: runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@main + - uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/orgs/element-hq/projects/91 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index f6a9de700c9..b1c776c6ead 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -12,7 +12,7 @@ jobs: if: > github.repository == 'element-hq/element-x-android' steps: - - uses: actions/add-to-project@main + - uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/orgs/element-hq/projects/43 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -21,7 +21,7 @@ jobs: name: Move triaged needs info issues on board runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@main + - uses: actions/add-to-project@v1.0.2 id: addItem with: project-url: https://github.com/orgs/element-hq/projects/91 @@ -29,7 +29,7 @@ jobs: labeled: X-Needs-Info - name: Print itemId run: echo ${{ steps.addItem.outputs.itemId }} - - uses: kalgurn/update-project-item-status@main + - 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 @@ -43,7 +43,7 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Element X Feature') steps: - - uses: actions/add-to-project@main + - uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/orgs/element-hq/projects/73 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -54,7 +54,7 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Verticals Feature') steps: - - uses: actions/add-to-project@main + - uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/orgs/element-hq/projects/57 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -66,7 +66,7 @@ jobs: contains(github.event.issue.labels.*.name, 'Team: QA') || contains(github.event.issue.labels.*.name, 'X-Needs-Signoff') steps: - - uses: actions/add-to-project@main + - uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/orgs/element-hq/projects/69 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -77,7 +77,7 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'X-Needs-Signoff') steps: - - uses: actions/add-to-project@main + - uses: actions/add-to-project@v1.0.2 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 index dd6cc5e64f8..3246cbd1a8f 100644 --- a/.github/workflows/validate-lfs.yml +++ b/.github/workflows/validate-lfs.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest name: Validate steps: - - uses: nschloe/action-cached-lfs-checkout@v1.2.3 + - uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 # v1.2.3 - run: | ./tools/git/validate_lfs.sh diff --git a/.gitignore b/.gitignore index 12bdb4eda81..d4bdf725fc4 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ captures/ .idea/.name .idea/androidTestResultsUserPreferences.xml .idea/assetWizardSettings.xml +.idea/AndroidProjectSystem.xml .idea/compiler.xml .idea/deploymentTargetDropDown.xml .idea/deploymentTargetSelector.xml diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index c22b6fa9ee9..03fcfb7bea5 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.maestro/allTests.yaml b/.maestro/allTests.yaml index 927a1cb0e54..ecbde4dab9d 100644 --- a/.maestro/allTests.yaml +++ b/.maestro/allTests.yaml @@ -1,4 +1,5 @@ appId: ${MAESTRO_APP_ID} +androidWebViewHierarchy: devtools --- ## Check that all env variables required in the whole test suite are declared (to fail faster) - runScript: ./scripts/checkEnv.js diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml index 18054297d62..f3f584ef767 100644 --- a/.maestro/tests/account/login.yaml +++ b/.maestro/tests/account/login.yaml @@ -7,22 +7,39 @@ appId: ${MAESTRO_APP_ID} - runFlow: ../assertions/assertLoginDisplayed.yaml - tapOn: id: "login-continue" +## MAS page +## Conditional workflow to pass the Chrome first launch welcome page. +- runFlow: + when: + visible: 'Use without an account' + commands: + - tapOn: "Use without an account" +## For older chrome versions +- runFlow: + when: + visible: 'Accept & continue' + commands: + - tapOn: "Accept & continue" +- runFlow: + when: + visible: 'No thanks' + commands: + - tapOn: "No thanks" +## Working when running Maestro locally, but not on the CI yet. +- extendedWaitUntil: + visible: + id: "form-1" + timeout: 10000 - tapOn: - id: "login-email_username" + id: "form-1" - inputText: ${MAESTRO_USERNAME} - pressKey: Enter - tapOn: - id: "login-password" -- inputText: "wrong-password" -- pressKey: Enter -- tapOn: "Continue" -- tapOn: "OK" -- tapOn: - id: "login-password" -- eraseText: 20 + id: "form-3" - inputText: ${MAESTRO_PASSWORD} - pressKey: Enter - tapOn: "Continue" +## Back to native world - runFlow: ../assertions/assertSessionVerificationDisplayed.yaml - runFlow: ./verifySession.yaml - runFlow: ../assertions/assertAnalyticsDisplayed.yaml diff --git a/.maestro/tests/account/verifySession.yaml b/.maestro/tests/account/verifySession.yaml index a16322543f9..f1f45527095 100644 --- a/.maestro/tests/account/verifySession.yaml +++ b/.maestro/tests/account/verifySession.yaml @@ -8,6 +8,6 @@ appId: ${MAESTRO_APP_ID} - hideKeyboard - tapOn: "Continue" - extendedWaitUntil: - visible: "Device verified" + visible: "Verification complete" timeout: 30000 - tapOn: "Continue" diff --git a/.maestro/tests/roomList/createAndDeleteDM.yaml b/.maestro/tests/roomList/createAndDeleteDM.yaml index 6e0d55ab26e..7e33fd1d15a 100644 --- a/.maestro/tests/roomList/createAndDeleteDM.yaml +++ b/.maestro/tests/roomList/createAndDeleteDM.yaml @@ -7,8 +7,9 @@ appId: ${MAESTRO_APP_ID} - tapOn: text: ${MAESTRO_INVITEE1_MXID} index: 1 +- tapOn: "Send invite" - takeScreenshot: build/maestro/330-createAndDeleteDM - tapOn: "maestroelement2" - scroll -- tapOn: "Leave conversation" +- tapOn: "Leave room" - tapOn: "Leave" diff --git a/.maestro/tests/roomList/timeline/call/call.yaml b/.maestro/tests/roomList/timeline/call/call.yaml new file mode 100644 index 00000000000..e390d0a3a10 --- /dev/null +++ b/.maestro/tests/roomList/timeline/call/call.yaml @@ -0,0 +1,13 @@ +appId: ${MAESTRO_APP_ID} +--- +- tapOn: "Start a call" +- takeScreenshot: build/maestro/700-Call +- extendedWaitUntil: + visible: "maestroelement" + timeout: 10000 +- takeScreenshot: build/maestro/710-Call +# Hangup +- tapOn: "End call" +- extendedWaitUntil: + visible: "MyRoom" + timeout: 10000 diff --git a/.maestro/tests/roomList/timeline/timeline.yaml b/.maestro/tests/roomList/timeline/timeline.yaml index 5f85366e9ec..d82c5ea1ff4 100644 --- a/.maestro/tests/roomList/timeline/timeline.yaml +++ b/.maestro/tests/roomList/timeline/timeline.yaml @@ -6,5 +6,6 @@ appId: ${MAESTRO_APP_ID} - runFlow: messages/text.yaml - runFlow: messages/location.yaml - runFlow: messages/poll.yaml +- runFlow: call/call.yaml - back - runFlow: ../../assertions/assertHomeDisplayed.yaml diff --git a/CHANGES.md b/CHANGES.md index 60fcfe0046f..4b0b22eb038 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,763 @@ +Changes in Element X v25.06.3 +============================= + +## What's Changed +### ✨ Features +* Feature : room version upgrade by @ganfra in https://github.com/element-hq/element-x-android/pull/4862 +* Add a developer option for history sharing on invite by @richvdh in https://github.com/element-hq/element-x-android/pull/4821 +### 🙌 Improvements +* Change : add tombstoned room decoration by @ganfra in https://github.com/element-hq/element-x-android/pull/4891 +* Show generic notification when Event cannot be resolved by @bmarty in https://github.com/element-hq/element-x-android/pull/4889 +### 🐛 Bugfixes +* [a11y] Improve screen reader on polls by @bmarty in https://github.com/element-hq/element-x-android/pull/4875 +* fix (event action): allow to edit only if permission to send message by @ganfra in https://github.com/element-hq/element-x-android/pull/4895 +* fix (room upgrade) : room predecessor banner on DM room by @ganfra in https://github.com/element-hq/element-x-android/pull/4896 +* fix (join room) : do not navigate up when join is successful by @ganfra in https://github.com/element-hq/element-x-android/pull/4899 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4842 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4881 +### Dependency upgrades +* chore(deps): update plugin dependencycheck to v12.1.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4856 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.10.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4858 +* fix(deps): update kotlin to v2.1.21-2.0.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4850 +* fix(deps): update dependency app.cash.turbine:turbine to v1.2.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4865 +* Update dependency com.posthog:posthog-android to v3.18.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4873 +* Update dependency org.maplibre.gl:android-sdk to v11.10.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4879 +* fix(deps): update dependency com.posthog:posthog-android to v3.19.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4882 +* fix(deps): update dependency io.sentry:sentry-android to v8.13.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4870 +* fix(deps): update showkase to v1.0.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4878 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.18 by @renovate in https://github.com/element-hq/element-x-android/pull/4894 +### Others +* Annotate Composable functions with `@ReadOnlyComposable` where it's possible by @bmarty in https://github.com/element-hq/element-x-android/pull/4859 +* Add documentation on WebViewPipController by @bmarty in https://github.com/element-hq/element-x-android/pull/4861 +* Small cleanup around log tag. by @bmarty in https://github.com/element-hq/element-x-android/pull/4860 +* Another cleanup by @bmarty in https://github.com/element-hq/element-x-android/pull/4869 +* Disable BT audio devices for Element Call on Android < 12 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4876 +* Add a banner to ask the user to disable battery optimization when Event cannot be resolved from Push by @bmarty in https://github.com/element-hq/element-x-android/pull/4845 +* a11y: improve accessibility on rich text editor options. by @bmarty in https://github.com/element-hq/element-x-android/pull/4886 +* A11Y: improve accessibility on event reactions. by @bmarty in https://github.com/element-hq/element-x-android/pull/4877 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.06.2...v25.06.3 + +Changes in Element X v25.06.2 +============================= + +## What's Changed +### 🐛 Bugfixes +* Fix crash when using Element Call on API <= 30 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4847 +* Element Call: add delay before selecting the default audio device by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4854 +* Fix for message composer losing focus in Compose 1.8.0 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4853 +### Dependency upgrades +* chore(deps): update plugin dependencycheck to v12.1.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4840 +* deps (matrix rust sdk) : bump version to 25.06.10 by @ganfra in https://github.com/element-hq/element-x-android/pull/4855 +### Others +* feat: Support matrix: links by @ShadowRZ in https://github.com/element-hq/element-x-android/pull/4839 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.06.1...v25.06.2 + +## What's Changed +### ✨ Features +* Enable support for Android Auto. by @bmarty in https://github.com/element-hq/element-x-android/pull/4818 +* Element Call: Add audio output selector handled by Android by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4663 +### 🙌 Improvements +* Oidc: Fallback to external browser instead of using Webview by @bmarty in https://github.com/element-hq/element-x-android/pull/4808 +* change (room member moderation) : update icon to match figma by @ganfra in https://github.com/element-hq/element-x-android/pull/4837 +### 🐛 Bugfixes +* Fix login flow by @bmarty in https://github.com/element-hq/element-x-android/pull/4813 +* fix: When sending media as files use the `octet-stream` type by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4815 +* fix: Make `Client.findDM` return a `Result` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4816 +* Mark room as fully read when user goes back to the room list. by @bmarty in https://github.com/element-hq/element-x-android/pull/2687 +* fix (identity change) : RoomMemberIdentityStateChange in non encrypted room by @ganfra in https://github.com/element-hq/element-x-android/pull/4824 +* Fix room and user avatar downloaded with a `.bin` extension. by @bmarty in https://github.com/element-hq/element-x-android/pull/4830 +* Log the push resolving failure reason if available by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4835 +### 🧱 Build +* Update Gradle Wrapper from 8.14.1 to 8.14.2 by @ElementBot in https://github.com/element-hq/element-x-android/pull/4831 +### Dependency upgrades +* fix(deps): update dependency androidx.compose:compose-bom to v2025.04.01 by @renovate in https://github.com/element-hq/element-x-android/pull/4631 +* fix(deps): update dependency androidx.compose:compose-bom to v2025.05.01 by @renovate in https://github.com/element-hq/element-x-android/pull/4814 +* fix(deps): update dependency io.sentry:sentry-android to v8.13.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4780 +* fix(deps): update appyx to v1.7.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4672 +* fix(deps): update telephoto to v0.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4749 +* fix(deps): update coil to v3.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4712 +* fix(deps): update dependency androidx.webkit:webkit to v1.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4823 +* fix(deps): update dependency com.posthog:posthog-android to v3.17.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4827 +* fix(deps): update dependency io.element.android:element-call-embedded to v0.12.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4832 +* fix(deps): update dependency com.google.firebase:firebase-bom to v33.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4833 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.10.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4825 +* fix(deps): update lifecycle to v2.9.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4822 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.6 by @renovate in https://github.com/element-hq/element-x-android/pull/4834 +* fix(deps): update dependency io.element.android:opusencoder to v1.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4836 +### Others +* Add `catchingExceptions` method to replace `runCatching` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4797 +* Rename classes overriding classes from the FFI layer. by @bmarty in https://github.com/element-hq/element-x-android/pull/4817 +* Fix coroutine scope by @bmarty in https://github.com/element-hq/element-x-android/pull/4820 +* Add extra logs the 'send call notification' flow by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4819 +* misc (matrix) : use innerClient.subscribeToRoomInfo sdk method by @ganfra in https://github.com/element-hq/element-x-android/pull/4838 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.06.0...v25.06.1 + +Changes in Element X v25.06.0 +============================= + +Rust SDK: https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-ffi%2F20250603 + +## What's Changed +### ✨ Features +* Add support for login link by @bmarty in https://github.com/element-hq/element-x-android/pull/4752 +### 🙌 Improvements +* On boarding flow: add a screen to select account provider among a fixed list by @bmarty in https://github.com/element-hq/element-x-android/pull/4769 +* Change : RoomMember moderation by @ganfra in https://github.com/element-hq/element-x-android/pull/4779 +### 🐛 Bugfixes +* Fix left room membership change by @ganfra in https://github.com/element-hq/element-x-android/pull/4765 +* fix: exclude more domains from being backed up by the system by @lucasmz-dev in https://github.com/element-hq/element-x-android/pull/4773 +* Make sure HeaderFooterPage contents can be scrolled by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4704 +* Fix mobile link by @bmarty in https://github.com/element-hq/element-x-android/pull/4805 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4775 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4804 +### 🧱 Build +* Maestro: fix MAS and EC breaking the tests by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4762 +* Update Gradle Wrapper from 8.14 to 8.14.1 by @ElementBot in https://github.com/element-hq/element-x-android/pull/4766 +* Stronger lambda error by @bmarty in https://github.com/element-hq/element-x-android/pull/4771 +* Use Localazy's `langAliases` for Indonesian language by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4801 +### Dependency upgrades +* fix(deps): update datastore to v1.1.7 by @renovate in https://github.com/element-hq/element-x-android/pull/4754 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.8 by @renovate in https://github.com/element-hq/element-x-android/pull/4721 +* chore(deps): update plugin ktlint to v12.3.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4767 +* fix(deps): update dependency com.google.firebase:firebase-bom to v33.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4755 +* Update UnifiedPush library by @bmarty in https://github.com/element-hq/element-x-android/pull/4358 +* fix(deps): update sqldelight to v2.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4735 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.26 by @renovate in https://github.com/element-hq/element-x-android/pull/4781 +* fix(deps): update dependency com.posthog:posthog-android to v3.15.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4787 +* fix(deps): update dependency com.posthog:posthog-android to v3.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4789 +* fix(deps): update dependency io.element.android:element-call-embedded to v0.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4743 +* fix(deps): update dependencyanalysis to v2.18.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4796 +* fix(deps): update android.gradle.plugin to v8.10.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4795 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.29 by @renovate in https://github.com/element-hq/element-x-android/pull/4799 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4810 +### Others +* fix(deps): update media3 to v1.7.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4733 +* fix: Ignore global proxy settings if system thinks there's none by @ShadowRZ in https://github.com/element-hq/element-x-android/pull/4744 +* Add `ActiveRoomHolder` to manage the active room for a session by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4758 +* Notification events resolving and rendering in batches by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4722 +* Hide Element Call entry point if Element Call service is not available. by @bmarty in https://github.com/element-hq/element-x-android/pull/4783 +* Fix dependencies on test by @bmarty in https://github.com/element-hq/element-x-android/pull/4790 +* Update _developer_onboarding.md by @lex-neufeld in https://github.com/element-hq/element-x-android/pull/4570 + +## New Contributors +* @lucasmz-dev made their first contribution in https://github.com/element-hq/element-x-android/pull/4773 +* @lex-neufeld made their first contribution in https://github.com/element-hq/element-x-android/pull/4570 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.05.4...v25.06.0 + + + +Changes in Element X v25.05.4 +============================= + +Rust SDK: https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-ffi%2F20250521 + +## What's Changed +### 🙌 Improvements +* Change (report room) : check if server supports the report room api by @ganfra in https://github.com/element-hq/element-x-android/pull/4718 +### 🐛 Bugfixes +* Improve audio focus management by @bmarty in https://github.com/element-hq/element-x-android/pull/4707 +* When transcoding a video fails, send it as a file by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4257 +* Disable mutliple click (parallel or serial) on a room by @bmarty in https://github.com/element-hq/element-x-android/pull/4683 +* Fix generic mime type used when externally sharing several files by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4715 +* Fix issues on JoinedRoom / BaseRoom by @bmarty in https://github.com/element-hq/element-x-android/pull/4724 +* Use the right live timeline instance in `RustRoomFactory` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4745 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4739 +### 🧱 Build +* Ensure the CI is marked as failed when Maestro test is failing by @bmarty in https://github.com/element-hq/element-x-android/pull/4700 +* Trigger pipeline build when a release tag is pushed by @bmarty in https://github.com/element-hq/element-x-android/pull/4741 +* Fix compilation issues. by @bmarty in https://github.com/element-hq/element-x-android/pull/4750 +### 📄 Documentation +* README.md: fix broken link by @richvdh in https://github.com/element-hq/element-x-android/pull/4728 +### Dependency upgrades +* chore(config): migrate renovate config by @renovate in https://github.com/element-hq/element-x-android/pull/4688 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.13 by @renovate in https://github.com/element-hq/element-x-android/pull/4716 +* fix(deps): update dependency io.sentry:sentry-android to v8.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4717 +* chore(deps): update plugin sonarqube to v6.2.0.5505 by @renovate in https://github.com/element-hq/element-x-android/pull/4725 +* fix(deps): update dependency com.posthog:posthog-android to v3.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4723 +* fix(deps): update dependency com.squareup.retrofit2:retrofit-bom to v2.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4727 +* chore(deps): update codecov/codecov-action action to v5.4.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4730 +* fix(deps): update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4713 +* fix(deps): update dependency com.squareup.retrofit2:retrofit-bom to v3 by @renovate in https://github.com/element-hq/element-x-android/pull/4729 +* fix(deps): update kotlinpoet to v2.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4732 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.21 by @renovate in https://github.com/element-hq/element-x-android/pull/4759 +### Others +* Remove event cache feature flag by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4719 +* Check homeserver when login using qr code by @bmarty in https://github.com/element-hq/element-x-android/pull/4708 +* Merge on boarding module to login module by @bmarty in https://github.com/element-hq/element-x-android/pull/4746 +* Allow configuration to provide multiple account providers. by @bmarty in https://github.com/element-hq/element-x-android/pull/4742 +* Reduce API of JoinedRoom, caller must use the Timeline API from liveTimeline instead by @bmarty in https://github.com/element-hq/element-x-android/pull/4731 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.05.3...v25.05.4 + +Changes in Element X v25.05.3 +============================= + +Version 25.05.2 was skipped. + +## What's Changed +### 🐛 Bugfixes +* Disable Continue button when the login field is cleared. by @bmarty in https://github.com/element-hq/element-x-android/pull/4699 +* Revert "fix(deps): update dependency io.element.android:element-call-embedded to v0.10.0", which caused an issue with to-device events in the latest version by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4706 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4703 +### 🧱 Build +* Update Gradle Wrapper from 8.13 to 8.14 by @ElementBot in https://github.com/element-hq/element-x-android/pull/4645 +### Dependency upgrades +* fix(deps): update datastore to v1.1.6 by @renovate in https://github.com/element-hq/element-x-android/pull/4630 +* fix(deps): update lifecycle to v2.9.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4693 +* fix(deps): update dependency androidx.sqlite:sqlite-ktx to v2.5.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4692 +### Others +* Update "Learn more" link by @bmarty in https://github.com/element-hq/element-x-android/pull/4686 +* Keep call notification ringing while a call is present in the room by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4634 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.05.1...v25.05.3 + +Changes in Element X v25.05.1 +============================= + +## What's Changed +### 🐛 Bugfixes +* Fix broken Element Call in 25.05.0 by @bmarty in https://github.com/element-hq/element-x-android/pull/4694 +### Dependency upgrades +* fix(deps): update android.gradle.plugin to v8.10.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4687 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.8 by @renovate in https://github.com/element-hq/element-x-android/pull/4696 +### Others +* Let EnterpriseService prevent usage of homeserver by @bmarty in https://github.com/element-hq/element-x-android/pull/4682 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.05.0...v25.05.1 + +Changes in Element X v25.05.0 +============================= + +## What's Changed +### ✨ Features +* Feature : Report room by @ganfra in https://github.com/element-hq/element-x-android/pull/4654 +### 🙌 Improvements +* Render kick and ban reason in the timeline when available by @bmarty in https://github.com/element-hq/element-x-android/pull/4642 +### 🐛 Bugfixes +* Accessibility: improve behavior of list items by @bmarty in https://github.com/element-hq/element-x-android/pull/4626 +* Render caller avatar on Incoming call screen by @bmarty in https://github.com/element-hq/element-x-android/pull/4635 +* Fix `Client.getJoinedRoom` crash when a room doesn't exist locally by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4656 +* Fix wrong member count in join room screen for invitation by @bmarty in https://github.com/element-hq/element-x-android/pull/4651 +* Make sure any `JoinedRustRoom` is destroyed after being used by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4678 +* Fix read receipt behavior when the timeline is opened. by @bmarty in https://github.com/element-hq/element-x-android/pull/4679 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4648 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4677 +### 🧱 Build +* OIDC configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/4623 +* Pin commit sha on GitHub actions by @bmarty in https://github.com/element-hq/element-x-android/pull/4653 +### Dependency upgrades +* fix(deps): update dependency io.sentry:sentry-android to v8.9.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4624 +* fix(deps): update dependency com.posthog:posthog-android to v3.14.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4628 +* fix(deps): update dependency androidx.exifinterface:exifinterface to v1.4.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4632 +* fix(deps): update dependencyanalysis to v2.17.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4638 +* fix(deps): update dependency com.google.firebase:firebase-bom to v33.13.0 - autoclosed by @renovate in https://github.com/element-hq/element-x-android/pull/4637 +* fix(deps): update dependency io.sentry:sentry-android to v8.10.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4644 +* fix(deps): update dependency org.jsoup:jsoup to v1.20.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4655 +* fix(deps): update dependency com.google.accompanist:accompanist-permissions to v0.37.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4649 +* fix(deps): update dependency io.sentry:sentry-android to v8.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4660 +* fix(deps): update dependency io.mockk:mockk to v1.14.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4658 +* fix(deps): update dependency io.github.sergio-sastre.composablepreviewscanner:android to v0.6.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4647 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.30 by @renovate in https://github.com/element-hq/element-x-android/pull/4665 +* fix(deps): update kotlin to v2.1.20-2.0.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4670 +* fix(deps): update dependency io.sentry:sentry-android to v8.11.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4668 +* fix(deps): update dependency io.element.android:element-call-embedded to v0.10.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4667 +* chore(deps): update rnkdsh/action-upload-diawi action to v1.5.9 by @renovate in https://github.com/element-hq/element-x-android/pull/4674 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.7 by @renovate in https://github.com/element-hq/element-x-android/pull/4673 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.6 by @renovate in https://github.com/element-hq/element-x-android/pull/4681 +### Others +* Split `MatrixRoom` into `MatrixRoom` and `JoinedMatrixRoom` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4561 +* Cleanup element call and UI by @bmarty in https://github.com/element-hq/element-x-android/pull/4641 +* Take change of screen_change_server_error_no_sliding_sync_message into account by @bmarty in https://github.com/element-hq/element-x-android/pull/4650 +* Improve the callback uri format and customization. by @bmarty in https://github.com/element-hq/element-x-android/pull/4664 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.04.3...v25.05.0 + +Changes in Element X v25.04.3 +============================= + +### 🙌 Improvements +* Use PreferenceDropdown for appearance by @ganfra in https://github.com/element-hq/element-x-android/pull/4581 +### 🐛 Bugfixes +* Use in-call volume and mode for EC by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4481 +* Send SVG images as files by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4595 +* Fetch the initial ignored user list manually when subscribing by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4598 +* Fix audio output selection for Element Call by @bmarty in https://github.com/element-hq/element-x-android/pull/4602 +* [a11y] Make more items focusable by @bmarty in https://github.com/element-hq/element-x-android/pull/4605 +* Fix ringing calls not stopping when the other user cancels the call by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4613 +* Ensure that pinning an event makes the pinned messages banner appear by @bmarty in https://github.com/element-hq/element-x-android/pull/4606 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4590 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4612 +### 📄 Documentation +* Improve onboarding docs: by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4578 +### Dependency upgrades +* Upgrade Rust bindings to `v25.04.11` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4580 +* fix(deps): update dependency androidx.sqlite:sqlite-ktx to v2.5.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4568 +* fix(deps): update dependency app.cash.molecule:molecule-runtime to v2.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4585 +* fix(deps): update core to v1.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4564 +* Upate datastore to 1.1.4 by @bmarty in https://github.com/element-hq/element-x-android/pull/4551 +* fix(deps): update media3 to v1.6.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4592 +* chore(deps): update danger/danger-js action to v13 by @renovate in https://github.com/element-hq/element-x-android/pull/4596 +* fix(deps): update dependency io.element.android:emojibase-bindings to v1.4.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4591 +* fix(deps): update dagger to v2.56.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4603 +* fix(deps): update dependency io.sentry:sentry-android to v8.8.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4557 +* fix(deps): update dependency androidx.compose:compose-bom to v2025.04.00 - autoclosed by @renovate in https://github.com/element-hq/element-x-android/pull/4565 +* fix(deps): update dependency com.posthog:posthog-android to v3.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4616 +* fix(deps): update android.gradle.plugin to v8.9.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4615 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.22 by @renovate in https://github.com/element-hq/element-x-android/pull/4622 +### Others +* Improve accessibility of the timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4579 +* Push: improve Push history screen, log and stored data by @bmarty in https://github.com/element-hq/element-x-android/pull/4601 +* Push gateway config by @bmarty in https://github.com/element-hq/element-x-android/pull/4608 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.04.2...v25.04.3 + +Changes in Element X v25.04.2 +============================= + +Security fixes 🔐 +----------------- +- Fix for [GHSA-m5px-pwq3-4p5m](https://github.com/element-hq/element-x-android/security/advisories/GHSA-m5px-pwq3-4p5m) / [CVE-2025-27599](https://www.cve.org/CVERecord?id=CVE-2025-27599) + +Changes in Element X v25.04.1 +============================= + + + +## What's Changed +### ✨ Features +* Introduce PushHistoryService to store data about the received push by @bmarty in https://github.com/element-hq/element-x-android/pull/4573 +### 🙌 Improvements +* change (preferences) : new moderation and safety settings by @ganfra in https://github.com/element-hq/element-x-android/pull/4574 +### 🐛 Bugfixes +* Ensure that we have only one single instance of SeenInviteStore per session by @bmarty in https://github.com/element-hq/element-x-android/pull/4577 +### Dependency upgrades +* fix(deps): update dependencyanalysis to v2.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4558 +* fix(deps): update dependency io.mockk:mockk to v1.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4562 +* fix(deps): update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4552 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.6 by @renovate in https://github.com/element-hq/element-x-android/pull/4567 +* fix(deps): update dependencyanalysis to v2.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4575 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.04.0...v25.04.1 + +Changes in Element X v25.04.0 +============================= + + + +## What's Changed +### ✨ Features +* Enable Rust trace log packs by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4514 +* Allow using a hardware keyboard to unlock the app using a pin code by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4530 +### 🙌 Improvements +* Change (mention span) : rework and add more cases by @ganfra in https://github.com/element-hq/element-x-android/pull/4476 +* Add kick (remove) confirmation and reason by @bmarty in https://github.com/element-hq/element-x-android/pull/4507 +* Remove the green badge on a pending invite after a first preview by @bmarty in https://github.com/element-hq/element-x-android/pull/4532 +### 🐛 Bugfixes +* Improve touch indicators for the user info UI in the timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4482 +* Limit the text length in the 'in reply to' preview by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4491 +* Timeline header: ensure that the decoration is clickable by @bmarty in https://github.com/element-hq/element-x-android/pull/4495 +* Add video autoplay to media gallery by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4499 +* Add `WakeLock` to dismiss ringing call screen when call is cancelled by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4478 +* Make sure the live timeline is destroyed before clearing a room's cache by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4515 +* Fix bullet points not having leading margin on timeline items by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4536 +* Fix the share location URI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4544 +* Add a inderminate progress bar when loging out and in Waiting state. by @bmarty in https://github.com/element-hq/element-x-android/pull/4538 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4506 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4543 +### 🧱 Build +* Element config by @bmarty in https://github.com/element-hq/element-x-android/pull/4471 +* Check if Manifest.permission.REQUEST_INSTALL_PACKAGES is in the manifest by @bmarty in https://github.com/element-hq/element-x-android/pull/4490 +* Remove nightly_enterprise.yml. by @bmarty in https://github.com/element-hq/element-x-android/pull/4492 +* Log the packageId which is currently built. by @bmarty in https://github.com/element-hq/element-x-android/pull/4494 +* Use handy buildConfigFieldStr. by @bmarty in https://github.com/element-hq/element-x-android/pull/4501 +* Fix warnings in InMemoryAppPreferencesStore by @bmarty in https://github.com/element-hq/element-x-android/pull/4523 +### Dependency upgrades +* fix(deps): update camera to v1.4.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4483 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.5 by @renovate in https://github.com/element-hq/element-x-android/pull/4487 +* fix(deps): update dependency com.posthog:posthog-android to v3.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4469 +* fix(deps): update dependency androidx.compose:compose-bom to v2025.03.01 by @renovate in https://github.com/element-hq/element-x-android/pull/4484 +* fix(deps): update dependencyanalysis to v2.13.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4493 +* fix(deps): update media3 to v1.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4488 +* fix(deps): update dependency io.element.android:element-call-embedded to v0.9.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4498 +* fix(deps): update dependency com.google.firebase:firebase-bom to v33.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4508 +* fix(deps): update dependency com.posthog:posthog-android to v3.13.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4516 +* fix(deps): update dependency io.sentry:sentry-android to v8.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4509 +* fix(deps): update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4444 +* fix(deps): update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4522 +* fix(deps): update dependencyanalysis to v2.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4527 +* fix(deps): update dependency io.element.android:compound-android to v25.4.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4537 +* chore(deps): update plugin dependencycheck to v12.1.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4540 +* fix(deps): update appyx to v1.7.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4547 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.4.7 by @renovate in https://github.com/element-hq/element-x-android/pull/4548 +### Others +* Update screenshots by @bmarty in https://github.com/element-hq/element-x-android/pull/4497 +* Update store description. by @bmarty in https://github.com/element-hq/element-x-android/pull/4496 +* Improve TextFieldDialog by @bmarty in https://github.com/element-hq/element-x-android/pull/4512 +* Make `RustMatrixClient.close` asynchronous by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4513 +* Replace OutlinedTextField by our TextField by @bmarty in https://github.com/element-hq/element-x-android/pull/4521 +* Remove alias from room invite item by @bmarty in https://github.com/element-hq/element-x-android/pull/4531 +* Remember flows by @bmarty in https://github.com/element-hq/element-x-android/pull/4533 +* Use colors from compound for badges by @bmarty in https://github.com/element-hq/element-x-android/pull/4545 +* Update app icon by @bmarty in https://github.com/element-hq/element-x-android/pull/4534 +* Click on userId / room alias to copy value to clipboard. by @bmarty in https://github.com/element-hq/element-x-android/pull/4549 +* Run the 'prevent blocked' workflow even if PR has conflicts by @robintown in https://github.com/element-hq/element-x-android/pull/4432 +* Update wording for push provider support test. (#4079) by @bmarty in https://github.com/element-hq/element-x-android/pull/4553 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.03.4...v25.04.0 + +Changes in Element X v25.03.4 +============================= + + + +## What's Changed +### 🙌 Improvements +* Change : composer suggestions by @ganfra in https://github.com/element-hq/element-x-android/pull/4485 +### 🧱 Build +* Fix flaky incoming verification tests by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4479 +### Dependency upgrades +* fix(deps): update dagger to v2.56.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4472 +* fix(deps): update dependencyanalysis to v2.13.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4473 +* Upgrade embedded EC version to `v0.9.0-rc.4` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4489 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.03.3...v25.03.4 + +Changes in Element X v25.03.3 +============================= + + + +## What's Changed +### ✨ Features +* Add 'unencrypted room' badges and labels by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4445 +* Use embedded version of Element Call by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4470 +### 🐛 Bugfixes +* Fix 'unverified session' flow displayed when creating account by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4467 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4461 +### 🧱 Build +* Let element enterprise be able to configure id for mapTiler. by @bmarty in https://github.com/element-hq/element-x-android/pull/4446 +### Dependency upgrades +* chore(deps): update rnkdsh/action-upload-diawi action to v1.5.8 by @renovate in https://github.com/element-hq/element-x-android/pull/4457 +* chore(deps): update plugin licensee to v1.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4447 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4450 +* fix(deps): update dependency com.google.firebase:firebase-bom to v33.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4448 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.3.24 by @renovate in https://github.com/element-hq/element-x-android/pull/4394 +* fix(deps): update dependencyanalysis to v2.13.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4464 +* chore(deps): update plugin sonarqube to v6.1.0.5360 by @renovate in https://github.com/element-hq/element-x-android/pull/4468 +* fix(deps): update android.gradle.plugin to v8.9.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4465 +### Others +* Sync Strings - tweaks to identity change messages by @andybalaam in https://github.com/element-hq/element-x-android/pull/4454 +* Check link click by @bmarty in https://github.com/element-hq/element-x-android/pull/4463 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.03.2...v25.03.3 + +Changes in Element X v25.03.2 +============================= + + + +## What's Changed +### ✨ Features +* Implement user verification by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4294 +* Add user verification and verification state violation badges by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4392 +* Open txt document inside the application by @bmarty in https://github.com/element-hq/element-x-android/pull/4414 +* Add timeline item prefetching by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4399 +### 🐛 Bugfixes +* fix(read receipt): track read receipts for focused timeline by @ganfra in https://github.com/element-hq/element-x-android/pull/4374 +* Discard timed out verification requests by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4385 +* Ensure the snackbar "No more media to show" is not rendered when opening the media viewer. by @bmarty in https://github.com/element-hq/element-x-android/pull/4397 +* Disable click effect on Stickers by @bmarty in https://github.com/element-hq/element-x-android/pull/4401 +* Ensure that a click on a media open the correct media. by @bmarty in https://github.com/element-hq/element-x-android/pull/4413 +* Display user verification violation icon in DM rooms too by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4423 +* Add a filter to avoid stack overflow when pressing the back button several times. by @bmarty in https://github.com/element-hq/element-x-android/pull/4430 +* Make verification screens scrollable and emoji labels multiline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4449 +### 🗣 Translations +* Sync Strings - New translations in Basque by @ElementBot in https://github.com/element-hq/element-x-android/pull/4381 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4421 +### 🧱 Build +* More PR checks by @bmarty in https://github.com/element-hq/element-x-android/pull/4384 +* "Core Team" is a team of matrix-org. Use team "Vector Core" instead. by @bmarty in https://github.com/element-hq/element-x-android/pull/4393 +* Fix warnings in tests for push provider modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4398 +* Update Gradle Wrapper from 8.12.1 to 8.13 by @ElementBot in https://github.com/element-hq/element-x-android/pull/4308 +* Revert agp to 8.8.1 by @bmarty in https://github.com/element-hq/element-x-android/pull/4451 +### Dependency upgrades +* Update rnkdsh/action-upload-diawi action to v1.5.7 by @renovate in https://github.com/element-hq/element-x-android/pull/4354 +* fix(deps): update dependency com.posthog:posthog-android to v3.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4387 +* fix(deps): update dependencyanalysis to v2.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4395 +* fix(deps): update dependency androidx.compose:compose-bom to v2025.03.00 by @renovate in https://github.com/element-hq/element-x-android/pull/4407 +* fix(deps): update dependency androidx.webkit:webkit to v1.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4408 +* fix(deps): update dependency net.java.dev.jna:jna to v5.17.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4419 +* fix(deps): update dependencyanalysis to v2.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4409 +* Add Google Tink dependency, replacing `androidx.security.crypto` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4405 +* fix(deps): update dependency io.sentry:sentry-android to v8.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4411 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4427 +* chore(deps): update webfactory/ssh-agent action to v0.9.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4426 +* fix(deps): update android.gradle.plugin to v8.9.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4320 +* Update SDK version to `25.03.13` and fix breaking changes by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4406 +* Update dagger to v2.56 by @renovate in https://github.com/element-hq/element-x-android/pull/4440 +* Update dependency io.sentry:sentry-android to v8.5.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4433 +* Update dependencyAnalysis to v2.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4442 +* Update dependency com.google.crypto.tink:tink-android to v1.17.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4422 +* deps(rust sdk) : update to 25.03.20 and fix api change by @ganfra in https://github.com/element-hq/element-x-android/pull/4452 +### Others +* Migrate some icons to Compound icon by @bmarty in https://github.com/element-hq/element-x-android/pull/4375 +* Long press link to copy URL to clipboard by @ShadowRZ in https://github.com/element-hq/element-x-android/pull/4376 +* Use public icon from Compound by @bmarty in https://github.com/element-hq/element-x-android/pull/4386 +* Be able to correctly render the UI with other colors. by @bmarty in https://github.com/element-hq/element-x-android/pull/4378 +* Let EnterpriseService provides push gateways by @bmarty in https://github.com/element-hq/element-x-android/pull/4400 +* Add feature flag to let the application prints logs to logcat in release builds. by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4402 +* Hide "unencrypted" lock for redacted messages by @Xant3s in https://github.com/element-hq/element-x-android/pull/4410 +* Hide unencrypted lock for redacted msgs by @bmarty in https://github.com/element-hq/element-x-android/pull/4429 +* Clear SDK cache properly by @bmarty in https://github.com/element-hq/element-x-android/pull/4396 + +## New Contributors +* @ShadowRZ made their first contribution in https://github.com/element-hq/element-x-android/pull/4376 +* @Xant3s made their first contribution in https://github.com/element-hq/element-x-android/pull/4410 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.03.1...v25.03.2 + +Changes in Element X v25.03.1 +============================= + + + +## What's Changed +### ✨ Features +* Enable the Event cache by default. by @bmarty in https://github.com/element-hq/element-x-android/pull/4373 +### 🙌 Improvements +* change(create room) : use history visibility "invited" by @ganfra in https://github.com/element-hq/element-x-android/pull/4335 +* change(room directory) : move the the room directory entry by @ganfra in https://github.com/element-hq/element-x-android/pull/4348 +* [Change] Invited state room preview by @ganfra in https://github.com/element-hq/element-x-android/pull/4353 +* change(left room snackbar) : manage cancel knock and decline invite by @ganfra in https://github.com/element-hq/element-x-android/pull/4360 +### 🐛 Bugfixes +* Restore manual `Client` cleanup on session logout by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4333 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4346 +### 🧱 Build +* Fix typo on job name. by @bmarty in https://github.com/element-hq/element-x-android/pull/4352 +### Dependency upgrades +* chore(deps): update plugin ktlint to v12.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4338 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4340 +* fix(deps): update dependency io.mockk:mockk to v1.13.17 by @renovate in https://github.com/element-hq/element-x-android/pull/4334 +* fix(deps): update kotlin to v2.1.10-1.0.31 by @renovate in https://github.com/element-hq/element-x-android/pull/4337 +* fix(deps): update dependency com.google.firebase:firebase-bom to v33.10.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4339 +* Migrate to coil3 by @bmarty in https://github.com/element-hq/element-x-android/pull/4347 +* fix(deps): update dependency org.jsoup:jsoup to v1.19.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4351 +* deps(rust sdk) : update to 25.03.05 by @ganfra in https://github.com/element-hq/element-x-android/pull/4370 +* Update dependency org.matrix.rustcomponents:sdk-android to v25.3.6 by @renovate in https://github.com/element-hq/element-x-android/pull/4371 +### Others +* Prevent PRs with the X-Blocked label from being merged by @robintown in https://github.com/element-hq/element-x-android/pull/4350 +* Fix some icon colors by @bmarty in https://github.com/element-hq/element-x-android/pull/4365 +* Remove PreferenceText, replace by ListItem. by @bmarty in https://github.com/element-hq/element-x-android/pull/4369 +* Show error screens in group calls by @robintown in https://github.com/element-hq/element-x-android/pull/4297 + +## New Contributors +* @robintown made their first contribution in https://github.com/element-hq/element-x-android/pull/4350 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.03.0...v25.03.1 + +Changes in Element X v25.03.0 +============================= + + + +## What's Changed +### ✨ Features +* Create `SyncOrchestrator` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4176 +* feature(crypto): verification violation handling and block sending by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/4126 +* Update Matrix Room API and allow media swipe on pinned event only. by @bmarty in https://github.com/element-hq/element-x-android/pull/4274 +* Feature : join room by address by @ganfra in https://github.com/element-hq/element-x-android/pull/4302 +### 🙌 Improvements +* Change : Room Preview by @ganfra in https://github.com/element-hq/element-x-android/pull/4250 +### 🐛 Bugfixes +* SyncOrchestrator: restore the initial sync step by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4242 +* When an emoji is used as the 'initial' for an avatar, use the whole emoji by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4277 +* Try avoiding trailing punctuation inside linkified URLs by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4214 +* Preload account urls by @bmarty in https://github.com/element-hq/element-x-android/pull/4301 +* Fix issues due to multiple ntfy applications with the same name. by @bmarty in https://github.com/element-hq/element-x-android/pull/4312 +* Use `Settings.System.DEFAULT_RINGTONE_URI` for ringing notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4310 +### 🗣 Translations +* Sync Strings - New translations to turkish by @ElementBot in https://github.com/element-hq/element-x-android/pull/4253 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4298 +### 🧱 Build +* Fix nightly reports by @bmarty in https://github.com/element-hq/element-x-android/pull/4235 +* Fix nightly reports - next step by @bmarty in https://github.com/element-hq/element-x-android/pull/4239 +* Prepare application for being configurable by @bmarty in https://github.com/element-hq/element-x-android/pull/4285 +* runQualityChecks task shouldn't fail fast by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4309 +* Get library ComposablePreviewScanner from maven and update to the latest version by @bmarty in https://github.com/element-hq/element-x-android/pull/4327 +### Dependency upgrades +* Update dependency com.posthog:posthog-android to v3.11.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4230 +* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.78 by @renovate in https://github.com/element-hq/element-x-android/pull/4234 +* Update dependency org.maplibre.gl:android-sdk to v11.8.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4245 +* fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.6.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4258 +* fix(deps): update dependency io.sentry:sentry-android to v8.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4262 +* fix(deps): update telephoto to v0.15.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4270 +* fix(deps): update dependency com.google.firebase:firebase-bom to v33.9.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4249 +* chore(deps): update danger/danger-js action to v12.3.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4259 +* fix(deps): update android.gradle.plugin to v8.8.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4263 +* chore(deps): update plugin dependencycheck to v12.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4272 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25 by @renovate in https://github.com/element-hq/element-x-android/pull/4273 +* fix(deps): update dependency androidx.compose:compose-bom to v2025.02.00 by @renovate in https://github.com/element-hq/element-x-android/pull/4261 +* fix(deps): update kotlin to v2.1.10-1.0.30 by @renovate in https://github.com/element-hq/element-x-android/pull/4265 +* fix(deps): update dependency io.github.zxing-cpp:android to v2.3.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4282 +* fix(deps): update firebaseappdistribution to v5.1.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4246 +* fix(deps): update dependencyanalysis to v2.8.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4251 +* fix(deps): update dependency com.google.accompanist:accompanist-permissions to v0.37.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4283 +* fix(deps): update dependency com.google.accompanist:accompanist-permissions to v0.37.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4287 +* fix(deps): update dependencyanalysis to v2.10.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4288 +* fix(deps): update dependencyanalysis to v2.10.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4295 +* Upgrade SDK version to 25.02.26 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4305 +* fix(deps): update kotlinpoet to v2.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4304 +* Update compound by @bmarty in https://github.com/element-hq/element-x-android/pull/4319 +* fix(deps): update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4324 +* fix(deps): update activity to v1.10.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4321 +* fix(deps): update dependency androidx.exifinterface:exifinterface to v1.4.0 - autoclosed by @renovate in https://github.com/element-hq/element-x-android/pull/4325 +* fix(deps): update dependency androidx.constraintlayout:constraintlayout to v2.2.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4322 +* fix(deps): update dependency io.sentry:sentry-android to v8.3.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4316 +* fix(deps): update dependency com.posthog:posthog-android to v3.11.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4313 +* fix(deps): update dependency com.android.tools:desugar_jdk_libs to v2.1.5 by @renovate in https://github.com/element-hq/element-x-android/pull/4299 +* chore(deps): update plugin detekt to v1.23.8 by @renovate in https://github.com/element-hq/element-x-android/pull/4292 +### Others +* Update incoming call notification content to "📹 Incoming call" by @bmarty in https://github.com/element-hq/element-x-android/pull/4231 +* Display a bottom sheet to let user confirm the DM creation by @bmarty in https://github.com/element-hq/element-x-android/pull/4233 +* Open chat links in regular browser tabs by @cbs228 in https://github.com/element-hq/element-x-android/pull/4198 +* Theme override by @bmarty in https://github.com/element-hq/element-x-android/pull/4226 +* Allow user certificate in production builds. by @bmarty in https://github.com/element-hq/element-x-android/pull/4275 +* Replace Material icons with Compound icons wherever it's possible by @bmarty in https://github.com/element-hq/element-x-android/pull/4323 + +## New Contributors +* @cbs228 made their first contribution in https://github.com/element-hq/element-x-android/pull/4198 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.02.0...v25.03.0 + +Changes in Element X v25.02.0 (2025-02-04) +========================================== + + + +## What's Changed +### ✨ Features +* Media navigation with swipe gesture by @bmarty in https://github.com/element-hq/element-x-android/pull/4161 +* Add ability to swipe between media when opened from the timeline. by @bmarty in https://github.com/element-hq/element-x-android/pull/4205 +### 🙌 Improvements +* change(design) : use ElementTheme.typography.fontBodyLgMedium by @ganfra in https://github.com/element-hq/element-x-android/pull/4145 +* change(design) : New component Announcement by @ganfra in https://github.com/element-hq/element-x-android/pull/4140 +* update rust sdk 0.2.75 by @ganfra in https://github.com/element-hq/element-x-android/pull/4158 +### 🐛 Bugfixes +* Fix dm avatar rtl by @bmarty in https://github.com/element-hq/element-x-android/pull/4103 +* Unified push gateway resolver improvement by @bmarty in https://github.com/element-hq/element-x-android/pull/4101 +* Close the media preview screen ASAP with sending queue enabled by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4089 +* fix(coroutine) : make sure to switch coroutine context by @ganfra in https://github.com/element-hq/element-x-android/pull/4146 +* Fix snack bar not displayed in MediaViewer by @bmarty in https://github.com/element-hq/element-x-android/pull/4195 +* Let the SDK provide the "network is available information" by @bmarty in https://github.com/element-hq/element-x-android/pull/4215 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4088 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4100 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4114 +* Fix import of en-US translations. by @bmarty in https://github.com/element-hq/element-x-android/pull/4135 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4139 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4172 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4199 +* Sync Strings - new (partial) language: Norwegian by @ElementBot in https://github.com/element-hq/element-x-android/pull/4227 +### 🧱 Build +* Update Gradle Wrapper from 8.11.1 to 8.12 by @ElementBot in https://github.com/element-hq/element-x-android/pull/4085 +* Test using Maestro CLI + emulator instead of Cloud by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4092 +* Make Maestro run for each PR push by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4121 +* Migrate to CalVer like versioning by @bmarty in https://github.com/element-hq/element-x-android/pull/4187 +* Kover: include back :libraries:matrix:impl module. by @bmarty in https://github.com/element-hq/element-x-android/pull/4193 +* Update Gradle Wrapper from 8.12 to 8.12.1 by @ElementBot in https://github.com/element-hq/element-x-android/pull/4196 +* Use secret Sentry DSN value by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4210 +* Use Sentry breadcrumbs instead of logging new events by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4223 +### 🚧 In development 🚧 +* Media Viewer: show snackbar when reaching end of timeline. by @bmarty in https://github.com/element-hq/element-x-android/pull/4201 +* Feature : room settings - security and privacy by @ganfra in https://github.com/element-hq/element-x-android/pull/4212 +### Dependency upgrades +* Update dependency io.mockk:mockk to v1.13.14 by @renovate in https://github.com/element-hq/element-x-android/pull/4083 +* Update dependency net.java.dev.jna:jna to v5.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4087 +* Update kotlin to v1.10.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4073 +* Update dagger to v2.54 by @renovate in https://github.com/element-hq/element-x-android/pull/4084 +* Update dependency io.sentry:sentry-android to v7.19.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4090 +* Update dependency com.android.tools:desugar_jdk_libs to v2.1.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4077 +* Update dependency com.posthog:posthog-android to v3.10.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4120 +* Update appyx to v1.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4129 +* Update dagger to v2.55 by @renovate in https://github.com/element-hq/element-x-android/pull/4131 +* Update android.gradle.plugin to v8.8.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4130 +* Update dependency org.maplibre.gl:android-sdk to v11.8.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4132 +* Update dependency io.mockk:mockk to v1.13.16 by @renovate in https://github.com/element-hq/element-x-android/pull/4134 +* Update dependencyAnalysis to v2.7.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4136 +* Update anvil to v0.4.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4144 +* Update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4117 +* Update plugin dependencycheck to v12 by @renovate in https://github.com/element-hq/element-x-android/pull/4137 +* Update dependency io.sentry:sentry-android to v7.20.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4107 +* Update wysiwyg to v2.38.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4104 +* Update dependency androidx.recyclerview:recyclerview to v1.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4154 +* Update activity to v1.10.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4152 +* Update firebaseAppDistribution to v5.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4159 +* Update dependency com.google.firebase:firebase-bom to v33.8.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4160 +* Update dependency androidx.compose:compose-bom to v2025 by @renovate in https://github.com/element-hq/element-x-android/pull/4155 +* Update dependency io.sentry:sentry-android to v7.20.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4178 +* Update dependency io.sentry:sentry-android to v8 by @renovate in https://github.com/element-hq/element-x-android/pull/4180 +* Update wysiwyg to v2.38.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4177 +* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.76 by @renovate in https://github.com/element-hq/element-x-android/pull/4183 +* Update wysiwyg to v2.38.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4186 +* Update dependency com.posthog:posthog-android to v3.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4204 +* Update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4200 +* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.77 by @renovate in https://github.com/element-hq/element-x-android/pull/4228 +* Update dependency com.posthog:posthog-android to v3.11.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4222 +* Update dependency io.element.android:emojibase-bindings to v1.3.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4213 +* Update dependencyAnalysis to v2.8.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4218 +* Update dependency androidx.compose:compose-bom to v2025.01.01 by @renovate in https://github.com/element-hq/element-x-android/pull/4217 +* Update dependency io.sentry:sentry-android to v8.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4221 +* Update rnkdsh/action-upload-diawi action to v1.5.6 by @renovate in https://github.com/element-hq/element-x-android/pull/4173 +* Update plugin dependencycheck to v12.0.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4170 +### Others +* Improve gallery loading state by @bmarty in https://github.com/element-hq/element-x-android/pull/4080 +* Show more detail about the error when pusher registration fails. by @bmarty in https://github.com/element-hq/element-x-android/pull/4081 +* Update pull request template and CI automation by @bmarty in https://github.com/element-hq/element-x-android/pull/4037 +* Add a log function for handling complex values to the WebView client. by @Half-Shot in https://github.com/element-hq/element-x-android/pull/4098 +* design : CounterAtom by @ganfra in https://github.com/element-hq/element-x-android/pull/4108 +* Change sticker mimetype fallback to image by @surakin in https://github.com/element-hq/element-x-android/pull/4111 +* Dual licensing: AGPL + Element Commercial by @bmarty in https://github.com/element-hq/element-x-android/pull/4118 +* Replace the InfoListOrganism default bg color by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4091 +* Ignore dependency that are not third-party licenses to us. by @bmarty in https://github.com/element-hq/element-x-android/pull/4122 +* misc(send queue) : do not disable send queue when Network is Offline by @ganfra in https://github.com/element-hq/element-x-android/pull/4105 +* Remove or replace unnecessary `BackHandler` calls by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4148 +* Replace our firstIfSingle extension with singleOrNull from the Kotlin library by @bmarty in https://github.com/element-hq/element-x-android/pull/4184 +* Remove log. by @bmarty in https://github.com/element-hq/element-x-android/pull/4203 +* Remove unused types / code. by @bmarty in https://github.com/element-hq/element-x-android/pull/4185 +* Consider that the topic of a room has been removed when it's blank. by @bmarty in https://github.com/element-hq/element-x-android/pull/4209 +* CalVer: use 2 digits for the year and 2 digits for the month. by @bmarty in https://github.com/element-hq/element-x-android/pull/4192 +* Always display encryption badge by @bmarty in https://github.com/element-hq/element-x-android/pull/4219 + +## New Contributors +* @Half-Shot made their first contribution in https://github.com/element-hq/element-x-android/pull/4098 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v0.7.6...v25.02.0 + Changes in Element X v0.7.6 (2024-12-20) ======================================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc4acfd5aa6..4e0c9b94463 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,8 @@ -* [Contributing code to Matrix](#contributing-code-to-matrix) * [Developer onboarding](#developer-onboarding) +* [Contributing code to Matrix](#contributing-code-to-matrix) * [Android Studio settings](#android-studio-settings) * [Compilation](#compilation) * [Strings](#strings) @@ -28,17 +28,17 @@ -## Contributing code to Matrix +## Developer onboarding -Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md +For a detailed overview of the project, see [Developer Onboarding](./docs/_developer_onboarding.md). -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). +## Contributing code to Matrix -The rest of the document contains specific rules for Matrix Android projects +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). -## Developer onboarding +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). -For a detailed overview of the project, see [Developer Onboarding](./docs/_developer_onboarding.md). +The rest of the document contains specific rules for Matrix Android projects. ## Android Studio settings diff --git a/README.md b/README.md index 37cfc58e9b7..20ac1c0536e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Learn more about why we are building Element X in our blog post: [https://elemen * [Translations](#translations) * [Rust SDK](#rust-sdk) * [Status](#status) +* [Minimum SDK version](#minimum-sdk-version) * [Contributing](#contributing) * [Build instructions](#build-instructions) * [Support](#support) @@ -73,6 +74,12 @@ We're doing this as a way to share code between platforms and while we've seen p This project is in an early rollout and migration phase. +## Minimum SDK version + +Element X Android requires a minimum SDK version of 24 (Android 7.0, Nougat). We aim to support devices running Android 7.0 and above, which covers a wide range of devices still in use today. + +Element Android Enterprise requires a minimum SDK version of 33 (Android 13, Tiramisu). For Element Enterprise, we support only devices that still receive security updates, which means devices running Android 13 and above. Android does not have a documented support policy, but some information can be found at [https://endoflife.date/android](https://endoflife.date/android). + ## Contributing Want to get actively involved in the project? You're more than welcome! A good way to start is to check the issues that are labelled with the [good first issue](https://github.com/element-hq/element-x-android/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label. Let us know by commenting the issue that you're starting working on it. @@ -87,7 +94,7 @@ Just clone the project and open it in Android Studio. Make sure to select the `app` configuration when building (as we also have sample apps in the project). To build against a local copy of the Rust SDK, see the [Developer -onboarding](docs/_developer_onboarding.md#build-the-sdk-locally) instructions. +onboarding](docs/_developer_onboarding.md#building-the-sdk-locally) instructions. ## Support diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt index 80c552428f7..ec6ad5958e3 100644 --- a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt +++ b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt @@ -13,7 +13,7 @@ import com.google.devtools.ksp.processing.SymbolProcessorProvider class ContributesNodeProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { - val enableLogging = environment.options["enableLogging"]?.toBoolean() ?: false + val enableLogging = environment.options["enableLogging"]?.toBoolean() == true return ContributesNodeProcessor( logger = environment.logger, codeGenerator = environment.codeGenerator, diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9e5bb50d268..e170c3de6fb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,6 +10,8 @@ import com.android.build.api.variant.FilterConfiguration.FilterType.ABI import com.android.build.gradle.internal.tasks.factory.dependsOn import com.android.build.gradle.tasks.GenerateBuildConfig +import com.google.firebase.appdistribution.gradle.firebaseAppDistribution +import config.BuildTimeConfig import extension.AssetCopyTask import extension.ComponentMergingStrategy import extension.GitBranchNameValueSource @@ -18,6 +20,7 @@ import extension.allEnterpriseImpl import extension.allFeaturesImpl import extension.allLibrariesImpl import extension.allServicesImpl +import extension.buildConfigFieldStr import extension.koverDependencies import extension.locales import extension.setupAnvil @@ -43,11 +46,7 @@ android { namespace = "io.element.android.x" defaultConfig { - applicationId = if (isEnterpriseBuild) { - "org.therevivalists.app" - } else { - "org.therevivalists.app" - } + applicationId = BuildTimeConfig.APPLICATION_ID targetSdk = Versions.TARGET_SDK versionCode = Versions.VERSION_CODE versionName = Versions.VERSION_NAME @@ -61,21 +60,27 @@ android { splits { // Configures multiple APKs based on ABI. abi { - // Enables building multiple APKs per ABI. - isEnable = true + val buildingAppBundle = gradle.startParameter.taskNames.any { it.contains("bundle") } + + // Enables building multiple APKs per ABI. This should be disabled when building an AAB. + isEnable = !buildingAppBundle + // By default all ABIs are included, so use reset() and include to specify that we only // want APKs for armeabi-v7a, x86, arm64-v8a and x86_64. // Resets the list of ABIs that Gradle should create APKs for to none. reset() - // Specifies a list of ABIs that Gradle should create APKs for. - include("armeabi-v7a", "x86", "arm64-v8a", "x86_64") - // Generate a universal APK that includes all ABIs, so user who installs from CI tool can use this one by default. - isUniversalApk = true + + if (!buildingAppBundle) { + // Specifies a list of ABIs that Gradle should create APKs for. + include("armeabi-v7a", "x86", "arm64-v8a", "x86_64") + // Generate a universal APK that includes all ABIs, so user who installs from CI tool can use this one by default. + isUniversalApk = true + } } } - defaultConfig { - resourceConfigurations += locales + androidResources { + localeFilters += locales } } @@ -96,23 +101,30 @@ android { ?: project.property("signing.element.nightly.storePassword") as? String? } } - - val baseAppName = if (isEnterpriseBuild) { - "The Revivalists" - } else { - "The Revivalists" - } - logger.warnInBox("Building $baseAppName") + + val baseAppName = BuildTimeConfig.APPLICATION_NAME + logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName)") buildTypes { + val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android" getByName("debug") { resValue("string", "app_name", "$baseAppName dbg") + resValue( + "string", + "login_redirect_scheme", + "$oidcRedirectSchemeBase.debug", + ) applicationIdSuffix = ".debug" signingConfig = signingConfigs.getByName("debug") } getByName("release") { resValue("string", "app_name", baseAppName) + resValue( + "string", + "login_redirect_scheme", + oidcRedirectSchemeBase, + ) signingConfig = signingConfigs.getByName("debug") postprocessing { @@ -130,6 +142,11 @@ android { applicationIdSuffix = ".nightly" versionNameSuffix = "-nightly" resValue("string", "app_name", "$baseAppName nightly") + resValue( + "string", + "login_redirect_scheme", + "$oidcRedirectSchemeBase.nightly", + ) matchingFallbacks += listOf("release") signingConfig = signingConfigs.getByName("nightly") @@ -170,13 +187,13 @@ android { create("gplay") { dimension = "store" isDefault = true - buildConfigField("String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"") - buildConfigField("String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"") + buildConfigFieldStr("SHORT_FLAVOR_DESCRIPTION", "G") + buildConfigFieldStr("FLAVOR_DESCRIPTION", "GooglePlay") } create("fdroid") { dimension = "store" - buildConfigField("String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"") - buildConfigField("String", "FLAVOR_DESCRIPTION", "\"FDroid\"") + buildConfigFieldStr("SHORT_FLAVOR_DESCRIPTION", "F") + buildConfigFieldStr("FLAVOR_DESCRIPTION", "FDroid") } } } @@ -283,6 +300,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.toolbox.test) koverDependencies() } @@ -291,14 +309,15 @@ tasks.withType().configureEach { outputs.upToDateWhen { false } val gitRevision = providers.of(GitRevisionValueSource::class.java) {}.get() val gitBranchName = providers.of(GitBranchNameValueSource::class.java) {}.get() - android.defaultConfig.buildConfigField("String", "GIT_REVISION", "\"$gitRevision\"") - android.defaultConfig.buildConfigField("String", "GIT_BRANCH_NAME", "\"$gitBranchName\"") + android.defaultConfig.buildConfigFieldStr("GIT_REVISION", gitRevision) + android.defaultConfig.buildConfigFieldStr("GIT_BRANCH_NAME", gitBranchName) } licensee { allow("Apache-2.0") allow("MIT") allow("BSD-2-Clause") + allow("BSD-3-Clause") allowUrl("https://opensource.org/licenses/MIT") allowUrl("https://developer.android.com/studio/terms.html") allowUrl("https://www.zetetic.net/sqlcipher/license/") @@ -335,3 +354,12 @@ fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) { } } } + +configurations.all { + resolutionStrategy { + dependencySubstitution { + val tink = libs.google.tink.get() + substitute(module("com.google.crypto.tink:tink")).using(module("${tink.group}:${tink.name}:${tink.version}")) + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2f354128213..d7de07e34b4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,8 +21,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ElementX" - tools:targetApi="33"> - + tools:targetApi="33"> + @@ -52,13 +57,16 @@ android:host="open" android:scheme="elementx" /> + - + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index 4f4e360f4fb..30bef76339e 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -9,13 +9,15 @@ package io.element.android.x import android.app.Application import androidx.startup.AppInitializer +import io.element.android.appconfig.RageshakeConfig +import io.element.android.appconfig.isEnabled import io.element.android.features.cachecleaner.api.CacheCleanerInitializer import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.x.di.AppComponent import io.element.android.x.di.DaggerAppComponent import io.element.android.x.info.logApplicationInfo import io.element.android.x.initializer.CrashInitializer -import io.element.android.x.initializer.TracingInitializer +import io.element.android.x.initializer.PlatformInitializer class ElementXApplication : Application(), DaggerComponentOwner { override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this) @@ -23,8 +25,10 @@ class ElementXApplication : Application(), DaggerComponentOwner { override fun onCreate() { super.onCreate() AppInitializer.getInstance(this).apply { - initializeComponent(CrashInitializer::class.java) - initializeComponent(TracingInitializer::class.java) + if (RageshakeConfig.isEnabled) { + initializeComponent(CrashInitializer::class.java) + } + initializeComponent(PlatformInitializer::class.java) initializeComponent(CacheCleanerInitializer::class.java) } logApplicationInfo(this) 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 2d2b645f3fb..f80db878a76 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -14,7 +14,6 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier @@ -26,6 +25,7 @@ import androidx.lifecycle.repeatOnLifecycle import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integrationpoint.NodeActivity import com.bumble.appyx.core.plugin.NodeReadyObserver +import io.element.android.compound.theme.ElementTheme import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenService @@ -61,7 +61,11 @@ class MainActivity : NodeActivity() { @Composable private fun MainContent(appBindings: AppBindings) { val migrationState = appBindings.migrationEntryPoint().present() - ElementThemeApp(appBindings.preferencesStore()) { + ElementThemeApp( + appPreferencesStore = appBindings.preferencesStore(), + enterpriseService = appBindings.enterpriseService(), + buildMeta = appBindings.buildMeta() + ) { CompositionLocalProvider( LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(), LocalUriHandler provides SafeUriHandler(this), @@ -69,8 +73,8 @@ class MainActivity : NodeActivity() { ) { Box( modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), + .fillMaxSize() + .background(ElementTheme.colors.bgCanvasDefault), ) { if (migrationState.migrationAction.isSuccess()) { MainNodeHost() diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index e24fdbb9fb2..8525f6356b2 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -9,11 +9,15 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo import io.element.android.features.api.MigrationEntryPoint +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.rageshake.api.reporter.BugReporter +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.di.AppScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.matrix.api.platform.InitPlatformService import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.services.analytics.api.AnalyticsService @@ -24,6 +28,8 @@ interface AppBindings { fun tracingService(): TracingService + fun platformService(): InitPlatformService + fun bugReporter(): BugReporter fun lockScreenService(): LockScreenService @@ -35,4 +41,10 @@ interface AppBindings { fun lockScreenEntryPoint(): LockScreenEntryPoint fun analyticsService(): AnalyticsService + + fun enterpriseService(): EnterpriseService + + fun featureFlagService(): FeatureFlagService + + fun buildMeta(): BuildMeta } diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index d682cb75248..c5bc3f4598b 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.x.BuildConfig import io.element.android.x.R import kotlinx.coroutines.CoroutineName @@ -56,6 +57,7 @@ object AppModule { } @Provides + @AppCoroutineScope @SingleIn(AppScope::class) fun providesAppCoroutineScope(): CoroutineScope { return MainScope() + CoroutineName("ElementX Scope") @@ -73,23 +75,26 @@ object AppModule { @ApplicationContext context: Context, buildType: BuildType, enterpriseService: EnterpriseService, - ) = BuildMeta( - isDebuggable = BuildConfig.DEBUG, - buildType = buildType, - applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name), - productionApplicationName = ApplicationConfig.PRODUCTION_APPLICATION_NAME, - desktopApplicationName = ApplicationConfig.DESKTOP_APPLICATION_NAME, - applicationId = BuildConfig.APPLICATION_ID, - isEnterpriseBuild = enterpriseService.isEnterpriseBuild, - // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE, - lowPrivacyLoggingEnabled = false, - versionName = BuildConfig.VERSION_NAME, - versionCode = context.getVersionCodeFromManifest(), - gitRevision = BuildConfig.GIT_REVISION, - gitBranchName = BuildConfig.GIT_BRANCH_NAME, - flavorDescription = BuildConfig.FLAVOR_DESCRIPTION, - flavorShortDescription = BuildConfig.SHORT_FLAVOR_DESCRIPTION, - ) + ): BuildMeta { + val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name) + return BuildMeta( + isDebuggable = BuildConfig.DEBUG, + buildType = buildType, + applicationName = applicationName, + productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME, + desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME, + applicationId = BuildConfig.APPLICATION_ID, + isEnterpriseBuild = enterpriseService.isEnterpriseBuild, + // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE, + lowPrivacyLoggingEnabled = false, + versionName = BuildConfig.VERSION_NAME, + versionCode = context.getVersionCodeFromManifest(), + gitRevision = BuildConfig.GIT_REVISION, + gitBranchName = BuildConfig.GIT_BRANCH_NAME, + flavorDescription = BuildConfig.FLAVOR_DESCRIPTION, + flavorShortDescription = BuildConfig.SHORT_FLAVOR_DESCRIPTION, + ) + } @Provides @SingleIn(AppScope::class) diff --git a/app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt b/app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt index 5ed8e89e24d..fb7a24ae8a1 100644 --- a/app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt +++ b/app/src/main/kotlin/io/element/android/x/di/DefaultRoomComponentFactory.kt @@ -10,14 +10,17 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appnav.di.RoomComponentFactory import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import javax.inject.Inject @ContributesBinding(SessionScope::class) class DefaultRoomComponentFactory @Inject constructor( private val roomComponentBuilder: RoomComponent.Builder ) : RoomComponentFactory { - override fun create(room: MatrixRoom): Any { - return roomComponentBuilder.room(room).build() + override fun create(room: JoinedRoom): Any { + return roomComponentBuilder + .joinedRoom(room) + .baseRoom(room) + .build() } } diff --git a/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt b/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt index e5958180516..ac126ce0f03 100644 --- a/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt +++ b/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt @@ -14,7 +14,8 @@ import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom @SingleIn(RoomScope::class) @MergeSubcomponent(RoomScope::class) @@ -22,7 +23,11 @@ interface RoomComponent : NodeFactoriesBindings { @MergeSubcomponent.Builder interface Builder { @BindsInstance - fun room(room: MatrixRoom): Builder + fun joinedRoom(room: JoinedRoom): Builder + + @BindsInstance + fun baseRoom(room: BaseRoom): Builder + fun build(): RoomComponent } diff --git a/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt new file mode 100644 index 00000000000..6dd0d69dc04 --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022-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. + */ + +package io.element.android.x.initializer + +import android.content.Context +import android.system.Os +import androidx.startup.Initializer +import io.element.android.features.rageshake.api.reporter.BugReporter +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.tracing.TracingConfiguration +import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration +import io.element.android.x.di.AppBindings +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import timber.log.Timber + +private const val ELEMENT_X_TARGET = "elementx" + +class PlatformInitializer : Initializer { + override fun create(context: Context) { + val appBindings = context.bindings() + val tracingService = appBindings.tracingService() + val platformService = appBindings.platformService() + val bugReporter = appBindings.bugReporter() + Timber.plant(tracingService.createTimberTree(ELEMENT_X_TARGET)) + val preferencesStore = appBindings.preferencesStore() + val featureFlagService = appBindings.featureFlagService() + val logLevel = runBlocking { preferencesStore.getTracingLogLevelFlow().first() } + val tracingConfiguration = TracingConfiguration( + writesToLogcat = runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PrintLogsToLogcat) }, + writesToFilesConfiguration = defaultWriteToDiskConfiguration(bugReporter), + logLevel = logLevel, + extraTargets = listOf(ELEMENT_X_TARGET), + traceLogPacks = runBlocking { preferencesStore.getTracingLogPacksFlow().first() }, + ) + bugReporter.setCurrentTracingLogLevel(logLevel.name) + platformService.init(tracingConfiguration) + // Also set env variable for rust back trace + Os.setenv("RUST_BACKTRACE", "1", true) + } + + private fun defaultWriteToDiskConfiguration(bugReporter: BugReporter): WriteToFilesConfiguration.Enabled { + return WriteToFilesConfiguration.Enabled( + directory = bugReporter.logDirectory().absolutePath, + filenamePrefix = "logs", + // Keep a maximum of 1 week of log files. + numberOfFiles = 7 * 24, + ) + } + + override fun dependencies(): List>> = mutableListOf() +} diff --git a/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt deleted file mode 100644 index 5c53968ece7..00000000000 --- a/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2022-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. - */ - -package io.element.android.x.initializer - -import android.content.Context -import android.system.Os -import androidx.startup.Initializer -import io.element.android.features.rageshake.api.reporter.BugReporter -import io.element.android.libraries.architecture.bindings -import io.element.android.libraries.matrix.api.tracing.TracingConfiguration -import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration -import io.element.android.x.BuildConfig -import io.element.android.x.di.AppBindings -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import timber.log.Timber - -private const val ELEMENT_X_TARGET = "elementx" - -class TracingInitializer : Initializer { - override fun create(context: Context) { - val appBindings = context.bindings() - val tracingService = appBindings.tracingService() - val bugReporter = appBindings.bugReporter() - Timber.plant(tracingService.createTimberTree(ELEMENT_X_TARGET)) - val preferencesStore = appBindings.preferencesStore() - val logLevel = runBlocking { preferencesStore.getTracingLogLevelFlow().first() } - val tracingConfiguration = TracingConfiguration( - writesToLogcat = BuildConfig.DEBUG, - writesToFilesConfiguration = defaultWriteToDiskConfiguration(bugReporter), - logLevel = logLevel, - extraTargets = listOf(ELEMENT_X_TARGET), - ) - bugReporter.setCurrentTracingLogLevel(logLevel.name) - tracingService.setupTracing(tracingConfiguration) - // Also set env variable for rust back trace - Os.setenv("RUST_BACKTRACE", "1", true) - } - - private fun defaultWriteToDiskConfiguration(bugReporter: BugReporter): WriteToFilesConfiguration.Enabled { - return WriteToFilesConfiguration.Enabled( - directory = bugReporter.logDirectory().absolutePath, - filenamePrefix = "logs", - // Keep a maximum of 1 week of log files. - numberOfFiles = 7 * 24, - ) - } - - override fun dependencies(): List>> = mutableListOf() -} diff --git a/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt b/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt new file mode 100644 index 00000000000..4a103b4daa1 --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt @@ -0,0 +1,25 @@ +/* + * 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.x.oidc + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider +import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.x.R +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultOidcRedirectUrlProvider @Inject constructor( + private val stringProvider: StringProvider, +) : OidcRedirectUrlProvider { + override fun provide() = buildString { + append(stringProvider.getString(R.string.login_redirect_scheme)) + append(":/") + } +} diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index d73e1f0c3d0..3602e38d2db 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -9,4 +9,8 @@ --> + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml index ae76014f3b5..d11b4203feb 100644 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -9,9 +9,18 @@ --> - + + + + + + - + + + + + - + \ No newline at end of file diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index a89ce28bfd3..73a2ed405cf 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -3,12 +3,15 @@ + + + @@ -16,6 +19,7 @@ + @@ -25,7 +29,9 @@ + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index b9f3d039863..96add8d148c 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -1,8 +1,15 @@ - + - + + + + + + @@ -24,12 +31,4 @@ lan localdomain - - - - - - - - diff --git a/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt b/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt new file mode 100644 index 00000000000..5a0df57bdad --- /dev/null +++ b/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt @@ -0,0 +1,28 @@ +/* + * 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.x.oidc + +import com.google.common.truth.Truth.assertThat +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.x.R +import org.junit.Test + +class DefaultOidcRedirectUrlProviderTest { + @Test + fun `test provide`() { + val stringProvider = FakeStringProvider( + defaultResult = "str" + ) + val sut = DefaultOidcRedirectUrlProvider( + stringProvider = stringProvider, + ) + val result = sut.provide() + assertThat(result).isEqualTo("str:/") + assertThat(stringProvider.lastResIdParam).isEqualTo(R.string.login_redirect_scheme) + } +} diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts index 99c7e1defaa..ecfed881d0b 100644 --- a/appconfig/build.gradle.kts +++ b/appconfig/build.gradle.kts @@ -1,3 +1,6 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr + /* * Copyright 2022-2024 New Vector Ltd. * @@ -10,6 +13,29 @@ plugins { android { namespace = "io.element.android.appconfig" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigFieldStr( + name = "URL_POLICY", + value = if (isEnterpriseBuild) { + BuildTimeConfig.URL_POLICY ?: "" + } else { + "https://element.io/cookie-policy" + }, + ) + buildConfigFieldStr( + name = "BUG_REPORT_URL", + value = BuildTimeConfig.BUG_REPORT_URL, + ) + buildConfigFieldStr( + name = "BUG_REPORT_APP_NAME", + value = BuildTimeConfig.BUG_REPORT_APP_NAME, + ) + } } dependencies { diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt index 4b213db6371..346fce47254 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt @@ -8,5 +8,5 @@ package io.element.android.appconfig object AnalyticsConfig { - const val POLICY_LINK = "https://element.io/cookie-policy" + const val POLICY_LINK = BuildConfig.URL_POLICY } diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt index a2918a8515d..7bea78f3b8b 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt @@ -8,12 +8,8 @@ package io.element.android.appconfig object AuthenticationConfig { - const val MATRIX_ORG_URL = "https://chat.the-revivalists.org" - /** - * Default homeserver url to sign in with, unless the user selects a different one. - */ - const val DEFAULT_HOMESERVER_URL = MATRIX_ORG_URL + const val MATRIX_ORG_URL = "https://chat.the-revivalists.org" /** * URL with some docs that explain what's sliding sync and how to add it to your home server. diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt index c9b7557c87d..ca9a6391f2a 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt @@ -8,11 +8,6 @@ package io.element.android.appconfig object ElementCallConfig { - /** - * The default base URL for the Element Call service. - */ - const val DEFAULT_BASE_URL = "https://call.element.io" - /** * The default duration of a ringing call in seconds before it's automatically dismissed. */ diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt index 9c1cd82def9..9dee73fc94d 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt @@ -9,6 +9,7 @@ package io.element.android.appconfig object LearnMoreConfig { const val ENCRYPTION_URL: String = "https://element.io/help#encryption" + const val DEVICE_VERIFICATION_URL: String = "https://element.io/help#encryption-device-verification" const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5" const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18" } diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt index fb8d24973c6..8c836bc8a2e 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt @@ -11,17 +11,23 @@ object RageshakeConfig { /** * The URL to submit bug reports to. */ - const val BUG_REPORT_URL = "https://rageshake.the-revivalists.org/api/submit" + const val BUG_REPORT_URL = BuildConfig.BUG_REPORT_URL /** * As per https://github.com/matrix-org/rageshake: * Identifier for the application (eg 'riot-web'). * Should correspond to a mapping configured in the configuration file for github issue reporting to work. */ - const val BUG_REPORT_APP_NAME = "element-x-android" + const val BUG_REPORT_APP_NAME = BuildConfig.BUG_REPORT_APP_NAME /** * The maximum size of the upload request. Default value is just below CloudFlare's max request size. */ const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L } + +/** + * Whether the rageshake feature is enabled. + */ +val RageshakeConfig.isEnabled: Boolean + get() = BUG_REPORT_URL.isNotEmpty() && BUG_REPORT_APP_NAME.isNotEmpty() diff --git a/appicon/element/src/main/ic_launcher-playstore.png b/appicon/element/src/main/ic_launcher-playstore.png index 62af9cf4b27..325bf570f53 100644 Binary files a/appicon/element/src/main/ic_launcher-playstore.png and b/appicon/element/src/main/ic_launcher-playstore.png differ diff --git a/appicon/element/src/main/kotlin/io/element/android/appicon/element/IconPreview.kt b/appicon/element/src/main/kotlin/io/element/android/appicon/element/IconPreview.kt index f55c6f23e80..c25675bffcb 100644 --- a/appicon/element/src/main/kotlin/io/element/android/appicon/element/IconPreview.kt +++ b/appicon/element/src/main/kotlin/io/element/android/appicon/element/IconPreview.kt @@ -10,25 +10,28 @@ package io.element.android.appicon.element import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -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.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp @Preview @Composable internal fun IconPreview() { Box { - Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null) - Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null) + Image( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_launcher_background), + contentDescription = null, + ) + Image( + painter = painterResource(id = R.mipmap.ic_launcher_foreground), + contentDescription = null, + ) } } @@ -36,8 +39,15 @@ internal fun IconPreview() { @Composable internal fun RoundIconPreview() { Box(modifier = Modifier.clip(shape = CircleShape)) { - Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null) - Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null) + Image( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_launcher_background), + contentDescription = null, + ) + Image( + painter = painterResource(id = R.mipmap.ic_launcher_foreground), + contentDescription = null, + ) } } @@ -46,10 +56,7 @@ internal fun RoundIconPreview() { internal fun MonochromeIconPreview() { Box( modifier = Modifier - .size(108.dp) - .background(Color(0xFF2F3133)) - .clip(shape = RoundedCornerShape(32.dp)), - contentAlignment = Alignment.Center + .background(Color(0xFF2F3133)), ) { Image( painter = painterResource(id = R.mipmap.ic_launcher_monochrome), diff --git a/appicon/element/src/main/res/mipmap-hdpi/ic_launcher.webp b/appicon/element/src/main/res/mipmap-hdpi/ic_launcher.webp index 793d5ca60d6..2ae0da8d0fe 100644 Binary files a/appicon/element/src/main/res/mipmap-hdpi/ic_launcher.webp and b/appicon/element/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_background.webp deleted file mode 100644 index f051ae3c818..00000000000 Binary files a/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_background.webp and /dev/null differ diff --git a/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index d1ff05833e1..e40370b86f6 100644 Binary files a/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 78a93b86f18..8ad6b749012 100644 Binary files a/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/appicon/element/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/appicon/element/src/main/res/mipmap-mdpi/ic_launcher.webp b/appicon/element/src/main/res/mipmap-mdpi/ic_launcher.webp index e8f321ff178..d4e1b90f22f 100644 Binary files a/appicon/element/src/main/res/mipmap-mdpi/ic_launcher.webp and b/appicon/element/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_background.webp deleted file mode 100644 index 27d9d1db19f..00000000000 Binary files a/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_background.webp and /dev/null differ diff --git a/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index f411c1016c8..ac2361f8b03 100644 Binary files a/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 5380a9e8617..3cd52b2182f 100644 Binary files a/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/appicon/element/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher.webp b/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher.webp index b31de825854..527b23880ad 100644 Binary files a/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_background.webp deleted file mode 100644 index 4dbc6db0662..00000000000 Binary files a/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_background.webp and /dev/null differ diff --git a/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index 5e6654b50c3..f8c5c5f218a 100644 Binary files a/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index a368522d59d..1c98f35c9f5 100644 Binary files a/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/appicon/element/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 889388eab63..ed524b893ca 100644 Binary files a/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp deleted file mode 100644 index b635d5cbb56..00000000000 Binary files a/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp and /dev/null differ diff --git a/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index 9aebd17d213..bb401bcb377 100644 Binary files a/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index af593824176..a6b0547ed07 100644 Binary files a/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/appicon/element/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 97afc844cb5..359e3921a11 100644 Binary files a/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp deleted file mode 100644 index b5cb68c7bb2..00000000000 Binary files a/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp and /dev/null differ diff --git a/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index 92e763d12f9..f0f9a633249 100644 Binary files a/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index d71ab178fe6..36125792fe6 100644 Binary files a/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/appicon/element/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/appicon/element/src/release/res/drawable/ic_launcher_background.xml b/appicon/element/src/release/res/drawable/ic_launcher_background.xml index 6ff3e595430..1cbabacb4fa 100644 --- a/appicon/element/src/release/res/drawable/ic_launcher_background.xml +++ b/appicon/element/src/release/res/drawable/ic_launcher_background.xml @@ -1,2 +1,10 @@ - + + + diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 8b2257b4a3b..09b0a634a44 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -53,16 +53,15 @@ dependencies { testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(projects.features.login.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.oidc.impl) + testImplementation(projects.libraries.oidc.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.features.networkmonitor.test) - testImplementation(projects.features.login.impl) testImplementation(projects.tests.testutils) testImplementation(projects.features.rageshake.test) - testImplementation(projects.features.rageshake.impl) testImplementation(projects.features.share.test) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.analytics.test) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt index 99876d4ff57..ae169fcd347 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt @@ -5,12 +5,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(DelicateCoilApi::class) + package io.element.android.appnav import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import coil.Coil +import coil3.SingletonImageLoader +import coil3.annotation.DelicateCoilApi import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext @@ -69,7 +72,7 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor( super.onBuilt() lifecycle.subscribe( onCreate = { - Coil.setImageLoader(imageLoaderHolder.get(inputs.matrixClient)) + SingletonImageLoader.setUnsafe(imageLoaderHolder.get(inputs.matrixClient)) }, ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt index 2759e49f70a..65980e34275 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt @@ -10,34 +10,33 @@ package io.element.android.appnav import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import javax.inject.Inject class LoggedInEventProcessor @Inject constructor( private val snackbarDispatcher: SnackbarDispatcher, - roomMembershipObserver: RoomMembershipObserver, + private val roomMembershipObserver: RoomMembershipObserver, ) { private var observingJob: Job? = null - private val displayLeftRoomMessage = roomMembershipObserver.updates - .map { !it.isUserInRoom } - fun observeEvents(coroutineScope: CoroutineScope) { - observingJob = coroutineScope.launch { - displayLeftRoomMessage - .filter { it } - .onEach { - displayMessage(CommonStrings.common_current_user_left_room) + observingJob = roomMembershipObserver.updates + .filter { !it.isUserInRoom } + .onEach { + when (it.change) { + MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room) + MembershipChange.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite) + MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock) + else -> Unit } - .launchIn(this) - } + } + .launchIn(coroutineScope) } fun stopObserving() { 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 ced5d0033a7..043dc75a92d 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -14,9 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import com.bumble.appyx.core.composable.PermanentChild import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext @@ -43,6 +41,7 @@ import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode +import io.element.android.appnav.loggedin.MediaPreviewConfigMigration import io.element.android.appnav.loggedin.SendQueues import io.element.android.appnav.room.RoomFlowNode import io.element.android.appnav.room.RoomNavigationTarget @@ -51,13 +50,11 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint 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.logout.api.LogoutEntryPoint -import io.element.android.features.networkmonitor.api.NetworkMonitor -import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint -import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.share.api.ShareEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint @@ -69,45 +66,57 @@ import io.element.android.libraries.architecture.waitForNavTargetAttached import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData -import io.element.android.libraries.matrix.api.sync.SyncState -import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener -import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import kotlinx.parcelize.Parcelize import timber.log.Timber +import java.time.Duration +import java.time.Instant import java.util.Optional import java.util.UUID +import kotlin.collections.List +import kotlin.collections.any +import kotlin.collections.emptyList +import kotlin.collections.first +import kotlin.collections.forEach +import kotlin.collections.listOf +import kotlin.collections.mapNotNull +import kotlin.collections.plus +import kotlin.collections.setOf +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toKotlinDuration @ContributesNode(SessionScope::class) class LoggedInFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val roomListEntryPoint: RoomListEntryPoint, + private val homeEntryPoint: HomeEntryPoint, private val preferencesEntryPoint: PreferencesEntryPoint, private val createRoomEntryPoint: CreateRoomEntryPoint, private val appNavigationStateService: AppNavigationStateService, private val secureBackupEntryPoint: SecureBackupEntryPoint, private val userProfileEntryPoint: UserProfileEntryPoint, private val ftueEntryPoint: FtueEntryPoint, - private val coroutineScope: CoroutineScope, - private val networkMonitor: NetworkMonitor, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val ftueService: FtueService, private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint, private val shareEntryPoint: ShareEntryPoint, @@ -115,7 +124,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val sendingQueue: SendQueues, private val logoutEntryPoint: LogoutEntryPoint, private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint, - private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase, + private val mediaPreviewConfigMigration: MediaPreviewConfigMigration, snackbarDispatcher: SnackbarDispatcher, ) : BaseFlowNode( backstack = BackStack( @@ -133,44 +142,66 @@ class LoggedInFlowNode @AssistedInject constructor( fun onOpenBugReport() } - private val syncService = matrixClient.syncService() private val loggedInFlowProcessor = LoggedInEventProcessor( snackbarDispatcher, matrixClient.roomMembershipObserver(), ) private val verificationListener = object : SessionVerificationServiceListener { - override fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails) { - backstack.singleTop(NavTarget.IncomingVerificationRequest(sessionVerificationRequestDetails)) + override fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) { + // Without this launch the rendering and actual state of this Appyx node's children gets out of sync, resulting in a crash. + // This might be because this method is called back from Rust in a background thread. + lifecycleScope.launch { + val receivedAt = Instant.now() + + // Wait until the app is in foreground to display the incoming verification request + appNavigationStateService.appNavigationState.first { it.isInForeground } + + // TODO there should also be a timeout for > 10 minutes elapsed since the request was created, but the SDK doesn't expose that info yet + val now = Instant.now() + val elapsedTimeSinceReceived = Duration.between(receivedAt, now).toKotlinDuration() + + // Discard the incoming verification request if it has timed out + if (elapsedTimeSinceReceived > 2.minutes) { + Timber.w("Incoming verification request ${verificationRequest.details.flowId} discarded due to timeout.") + return@launch + } + + // Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it + // Otherwise, the RoomList UI may be incorrectly displayed on top + withTimeout(5.seconds) { + backstack.elements.first { elements -> + elements.any { it.key.navTarget == NavTarget.Home } + } + } + + backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest)) + } } } override fun onBuilt() { super.onBuilt() + lifecycle.subscribe( onCreate = { appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId) // TODO We do not support Space yet, so directly navigate to main space appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE) - loggedInFlowProcessor.observeEvents(coroutineScope) + loggedInFlowProcessor.observeEvents(sessionCoroutineScope) matrixClient.sessionVerificationService().setListener(verificationListener) + mediaPreviewConfigMigration() ftueService.state .onEach { ftueState -> when (ftueState) { is FtueState.Unknown -> Unit // Nothing to do is FtueState.Incomplete -> backstack.safeRoot(NavTarget.Ftue) - is FtueState.Complete -> backstack.safeRoot(NavTarget.RoomList) + is FtueState.Complete -> backstack.safeRoot(NavTarget.Home) } } .launchIn(lifecycleScope) }, - onStop = { - coroutineScope.launch { - // Counterpart startSync is done in observeSyncStateAndNetworkStatus method. - syncService.stopSync() - } - }, onDestroy = { appNavigationStateService.onLeavingSpace(id) appNavigationStateService.onLeavingSession(id) @@ -178,7 +209,6 @@ class LoggedInFlowNode @AssistedInject constructor( matrixClient.sessionVerificationService().setListener(null) } ) - observeSyncStateAndNetworkStatus() setupSendingQueue() } @@ -186,31 +216,6 @@ class LoggedInFlowNode @AssistedInject constructor( sendingQueue.launchIn(lifecycleScope) } - @OptIn(FlowPreview::class) - private fun observeSyncStateAndNetworkStatus() { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - combine( - // small debounce to avoid spamming startSync when the state is changing quickly in case of error. - syncService.syncState.debounce(100), - networkMonitor.connectivity - ) { syncState, networkStatus -> - Pair(syncState, networkStatus) - } - .onStart { - // Temporary fix to ensure that the sync is started even if the networkStatus is offline. - syncService.startSync() - } - .collect { (syncState, networkStatus) -> - Timber.d("Sync state: $syncState, network status: $networkStatus") - if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) { - syncService.startSync() - } - } - } - } - } - sealed interface NavTarget : Parcelable { @Parcelize data object Placeholder : NavTarget @@ -219,7 +224,7 @@ class LoggedInFlowNode @AssistedInject constructor( data object LoggedInPermanent : NavTarget @Parcelize - data object RoomList : NavTarget + data object Home : NavTarget @Parcelize data class Room( @@ -262,7 +267,7 @@ class LoggedInFlowNode @AssistedInject constructor( data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget @Parcelize - data class IncomingVerificationRequest(val data: SessionVerificationRequestDetails) : NavTarget + data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -276,8 +281,8 @@ class LoggedInFlowNode @AssistedInject constructor( } createNode(buildContext, listOf(callback)) } - NavTarget.RoomList -> { - val callback = object : RoomListEntryPoint.Callback { + NavTarget.Home -> { + val callback = object : HomeEntryPoint.Callback { override fun onRoomClick(roomId: RoomId) { backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) } @@ -306,15 +311,11 @@ class LoggedInFlowNode @AssistedInject constructor( plugins().forEach { it.onOpenBugReport() } } - override fun onRoomDirectorySearchClick() { - backstack.push(NavTarget.RoomDirectorySearch) - } - override fun onLogoutForNativeSlidingSyncMigrationNeeded() { backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded) } } - roomListEntryPoint + homeEntryPoint .nodeBuilder(this, buildContext) .callback(callback) .build() @@ -326,7 +327,7 @@ class LoggedInFlowNode @AssistedInject constructor( } override fun onForwardedToSingleRoom(roomId: RoomId) { - coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false) } + sessionCoroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false) } } override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { @@ -392,6 +393,14 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onOpenRoomNotificationSettings(roomId: RoomId) { backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings)) } + + override fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId) { + // We do not check the sessionId, but it will have to be done at some point (multi account) + if (sessionId != matrixClient.sessionId) { + Timber.e("SessionId mismatch, expected ${matrixClient.sessionId} but got $sessionId") + } + backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId))) + } } val inputs = PreferencesEntryPoint.Params(navTarget.initialElement) preferencesEntryPoint.nodeBuilder(this, buildContext) @@ -401,8 +410,12 @@ class LoggedInFlowNode @AssistedInject constructor( } NavTarget.CreateRoom -> { val callback = object : CreateRoomEntryPoint.Callback { - override fun onSuccess(roomId: RoomId) { - backstack.replace(NavTarget.Room(roomId.toRoomIdOrAlias())) + override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) { + backstack.replace(NavTarget.Room(roomIdOrAlias = roomIdOrAlias, serverNames = serverNames)) + } + + override fun onOpenRoomDirectory() { + backstack.push(NavTarget.RoomDirectorySearch) } } @@ -462,9 +475,6 @@ class LoggedInFlowNode @AssistedInject constructor( } logoutEntryPoint.nodeBuilder(this, buildContext) - .onSuccessfulLogoutPendingAction { - enableNativeSlidingSyncUseCase() - } .callback(callback) .build() } @@ -489,7 +499,7 @@ class LoggedInFlowNode @AssistedInject constructor( clearBackstack: Boolean, ) { waitForNavTargetAttached { navTarget -> - navTarget is NavTarget.RoomList + navTarget is NavTarget.Home } attachChild { val roomNavTarget = NavTarget.Room( @@ -506,7 +516,7 @@ class LoggedInFlowNode @AssistedInject constructor( suspend fun attachUser(userId: UserId) { waitForNavTargetAttached { navTarget -> - navTarget is NavTarget.RoomList + navTarget is NavTarget.Home } attachChild { backstack.push( @@ -519,7 +529,7 @@ class LoggedInFlowNode @AssistedInject constructor( internal suspend fun attachIncomingShare(intent: Intent) { waitForNavTargetAttached { navTarget -> - navTarget is NavTarget.RoomList + navTarget is NavTarget.Home } attachChild { backstack.push( @@ -557,7 +567,7 @@ private class AttachRoomOperation( return if (clearBackstack) { // Makes sure the room list target is alone in the backstack and stashed elements.mapNotNull { element -> - if (element.key.navTarget == LoggedInFlowNode.NavTarget.RoomList) { + if (element.key.navTarget == LoggedInFlowNode.NavTarget.Home) { element.transitionTo(STASHED, this) } else { null diff --git a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt index c8c88ad02df..bd4802b77ba 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt @@ -5,27 +5,30 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(DelicateCoilApi::class) + package io.element.android.appnav import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import coil.Coil +import coil3.SingletonImageLoader +import coil3.annotation.DelicateCoilApi import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack -import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.login.api.LoginEntryPoint -import io.element.android.features.login.api.LoginFlowType -import io.element.android.features.onboarding.api.OnBoardingEntryPoint +import io.element.android.features.login.api.LoginParams 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.inputs import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices import io.element.android.libraries.designsystem.utils.ScreenOrientation import io.element.android.libraries.di.AppScope @@ -36,68 +39,59 @@ import kotlinx.parcelize.Parcelize class NotLoggedInFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val onBoardingEntryPoint: OnBoardingEntryPoint, private val loginEntryPoint: LoginEntryPoint, private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.OnBoarding, + initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap ), buildContext = buildContext, plugins = plugins, ) { + data class Params( + val loginParams: LoginParams?, + ) : NodeInputs + interface Callback : Plugin { fun onOpenBugReport() } + private val inputs = inputs() + override fun onBuilt() { super.onBuilt() lifecycle.subscribe( onCreate = { - Coil.setImageLoader(notLoggedInImageLoaderFactory) + SingletonImageLoader.setUnsafe(notLoggedInImageLoaderFactory.newImageLoader()) }, ) } sealed interface NavTarget : Parcelable { @Parcelize - data object OnBoarding : NavTarget - - @Parcelize - data class LoginFlow(val type: LoginFlowType) : NavTarget + data object Root : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.OnBoarding -> { - val callback = object : OnBoardingEntryPoint.Callback { - override fun onSignUp() { - backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_UP)) - } - - override fun onSignIn() { - backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_IN_MANUAL)) - } - - override fun onSignInWithQrCode() { - backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_IN_QR_CODE)) - } - + NavTarget.Root -> { + val callback = object : LoginEntryPoint.Callback { override fun onReportProblem() { plugins().forEach { it.onOpenBugReport() } } } - onBoardingEntryPoint + loginEntryPoint .nodeBuilder(this, buildContext) + .params( + LoginEntryPoint.Params( + accountProvider = inputs.loginParams?.accountProvider, + loginHint = inputs.loginParams?.loginHint, + ) + ) .callback(callback) .build() } - is NavTarget.LoginFlow -> { - loginEntryPoint.nodeBuilder(this, buildContext) - .params(LoginEntryPoint.Params(flowType = navTarget.type)) - .build() - } } } 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 5f5eb7566ed..ae9f935061c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -27,12 +27,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.anvilannotations.ContributesNode -import io.element.android.appnav.di.MatrixClientsHolder +import io.element.android.appnav.di.MatrixSessionCache import io.element.android.appnav.intent.IntentResolver import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.login.api.LoginParams import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.viewfolder.api.ViewFolderEntryPoint @@ -40,6 +42,7 @@ import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.waitForChildAttached +import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.di.AppScope @@ -61,8 +64,9 @@ class RootFlowNode @AssistedInject constructor( @Assisted val buildContext: BuildContext, @Assisted plugins: List, private val authenticationService: MatrixAuthenticationService, + private val enterpriseService: EnterpriseService, private val navStateFlowFactory: RootNavStateFlowFactory, - private val matrixClientsHolder: MatrixClientsHolder, + private val matrixSessionCache: MatrixSessionCache, private val presenter: RootPresenter, private val bugReportEntryPoint: BugReportEntryPoint, private val viewFolderEntryPoint: ViewFolderEntryPoint, @@ -78,14 +82,14 @@ class RootFlowNode @AssistedInject constructor( plugins = plugins ) { override fun onBuilt() { - matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap) + matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap) super.onBuilt() observeNavState() } override fun onSaveInstanceState(state: MutableSavedStateMap) { super.onSaveInstanceState(state) - matrixClientsHolder.saveIntoSavedState(state) + matrixSessionCache.saveIntoSavedState(state) navStateFlowFactory.saveIntoSavedState(state) } @@ -99,14 +103,14 @@ class RootFlowNode @AssistedInject constructor( if (navState.loggedInState.isTokenValid) { tryToRestoreLatestSession( onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, - onFailure = { switchToNotLoggedInFlow() } + onFailure = { switchToNotLoggedInFlow(null) } ) } else { switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId)) } } LoggedInState.NotLoggedIn -> { - switchToNotLoggedInFlow() + switchToNotLoggedInFlow(null) } } } @@ -117,9 +121,9 @@ class RootFlowNode @AssistedInject constructor( backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId)) } - private fun switchToNotLoggedInFlow() { - matrixClientsHolder.removeAll() - backstack.safeRoot(NavTarget.NotLoggedInFlow) + private fun switchToNotLoggedInFlow(params: LoginParams?) { + matrixSessionCache.removeAll() + backstack.safeRoot(NavTarget.NotLoggedInFlow(params)) } private fun switchToSignedOutFlow(sessionId: SessionId) { @@ -131,7 +135,7 @@ class RootFlowNode @AssistedInject constructor( onFailure: () -> Unit, onSuccess: (SessionId) -> Unit, ) { - matrixClientsHolder.getOrRestore(sessionId) + matrixSessionCache.getOrRestore(sessionId) .onSuccess { Timber.v("Succeed to restore session $sessionId") onSuccess(sessionId) @@ -175,7 +179,9 @@ class RootFlowNode @AssistedInject constructor( data object SplashScreen : NavTarget @Parcelize - data object NotLoggedInFlow : NavTarget + data class NotLoggedInFlow( + val params: LoginParams? + ) : NavTarget @Parcelize data class LoggedInFlow( @@ -200,7 +206,7 @@ class RootFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.LoggedInFlow -> { - val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also { + val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also { Timber.w("Couldn't find any session, go through SplashScreen") } val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient) @@ -211,13 +217,16 @@ class RootFlowNode @AssistedInject constructor( } createNode(buildContext, plugins = listOf(inputs, callback)) } - NavTarget.NotLoggedInFlow -> { + is NavTarget.NotLoggedInFlow -> { val callback = object : NotLoggedInFlowNode.Callback { override fun onOpenBugReport() { backstack.push(NavTarget.BugReport) } } - createNode(buildContext, plugins = listOf(callback)) + val params = NotLoggedInFlowNode.Params( + loginParams = navTarget.params, + ) + createNode(buildContext, plugins = listOf(params, callback)) } is NavTarget.SignedOutFlow -> { signedOutEntryPoint.nodeBuilder(this, buildContext) @@ -272,18 +281,36 @@ class RootFlowNode @AssistedInject constructor( val resolvedIntent = intentResolver.resolve(intent) ?: return when (resolvedIntent) { is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData) + is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params) is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction) is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData) is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.intent) } } + private suspend fun onLoginLink(params: LoginParams) { + // Is there a session already? + val latestSessionId = authenticationService.getLatestSessionId() + if (latestSessionId == null) { + // No session, open login + if (enterpriseService.isAllowedToConnectToHomeserver(params.accountProvider.ensureProtocol())) { + switchToNotLoggedInFlow(params) + } else { + Timber.w("Login link ignored, we are not allowed to connect to the homeserver") + switchToNotLoggedInFlow(null) + } + } else { + // Just ignore the login link if we already have a session + Timber.w("Login link ignored, we already have a session") + } + } + private suspend fun onIncomingShare(intent: Intent) { // Is there a session already? val latestSessionId = authenticationService.getLatestSessionId() if (latestSessionId == null) { // No session, open login - switchToNotLoggedInFlow() + switchToNotLoggedInFlow(null) } else { attachSession(latestSessionId) .attachIncomingShare(intent) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt deleted file mode 100644 index 2871df28998..00000000000 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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. - */ - -package io.element.android.appnav.di - -import com.bumble.appyx.core.state.MutableSavedStateMap -import com.bumble.appyx.core.state.SavedStateMap -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.core.SessionId -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject - -private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" - -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class MatrixClientsHolder @Inject constructor( - private val authenticationService: MatrixAuthenticationService, -) : MatrixClientProvider { - private val sessionIdsToMatrixClient = ConcurrentHashMap() - private val restoreMutex = Mutex() - - init { - authenticationService.listenToNewMatrixClients { matrixClient -> - sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient - } - } - - fun removeAll() { - sessionIdsToMatrixClient.clear() - } - - fun remove(sessionId: SessionId) { - sessionIdsToMatrixClient.remove(sessionId) - } - - override fun getOrNull(sessionId: SessionId): MatrixClient? { - return sessionIdsToMatrixClient[sessionId] - } - - override suspend fun getOrRestore(sessionId: SessionId): Result { - return restoreMutex.withLock { - when (val matrixClient = getOrNull(sessionId)) { - null -> restore(sessionId) - else -> Result.success(matrixClient) - } - } - } - - @Suppress("UNCHECKED_CAST") - fun restoreWithSavedState(state: SavedStateMap?) { - Timber.d("Restore state") - if (state == null || sessionIdsToMatrixClient.isNotEmpty()) { - Timber.w("Restore with non-empty map") - return - } - val sessionIds = state[SAVE_INSTANCE_KEY] as? Array - Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}") - if (sessionIds.isNullOrEmpty()) return - // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. - runBlocking { - sessionIds.forEach { sessionId -> - getOrRestore(sessionId) - } - } - } - - fun saveIntoSavedState(state: MutableSavedStateMap) { - val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray() - Timber.d("Save matrix session keys = ${sessionKeys.map { it.value }}") - state[SAVE_INSTANCE_KEY] = sessionKeys - } - - private suspend fun restore(sessionId: SessionId): Result { - Timber.d("Restore matrix session: $sessionId") - return authenticationService.restoreSession(sessionId) - .onSuccess { matrixClient -> - sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient - } - .onFailure { - Timber.e(it, "Fail to restore session") - } - } -} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt new file mode 100644 index 00000000000..302a12d99b9 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt @@ -0,0 +1,124 @@ +/* + * 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. + */ + +package io.element.android.appnav.di + +import androidx.annotation.VisibleForTesting +import com.bumble.appyx.core.state.MutableSavedStateMap +import com.bumble.appyx.core.state.SavedStateMap +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" + +/** + * In-memory cache for logged in Matrix sessions. + * + * This component contains both the [MatrixClient] and the [SyncOrchestrator] for each session. + */ +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class MatrixSessionCache @Inject constructor( + private val authenticationService: MatrixAuthenticationService, + private val syncOrchestratorFactory: SyncOrchestrator.Factory, +) : MatrixClientProvider { + private val sessionIdsToMatrixSession = ConcurrentHashMap() + private val restoreMutex = Mutex() + + init { + authenticationService.listenToNewMatrixClients { matrixClient -> + val syncOrchestrator = syncOrchestratorFactory.create(matrixClient) + sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession( + matrixClient = matrixClient, + syncOrchestrator = syncOrchestrator, + ) + syncOrchestrator.start() + } + } + + fun removeAll() { + sessionIdsToMatrixSession.clear() + } + + fun remove(sessionId: SessionId) { + sessionIdsToMatrixSession.remove(sessionId) + } + + override fun getOrNull(sessionId: SessionId): MatrixClient? { + return sessionIdsToMatrixSession[sessionId]?.matrixClient + } + + override suspend fun getOrRestore(sessionId: SessionId): Result { + return restoreMutex.withLock { + when (val cached = getOrNull(sessionId)) { + null -> restore(sessionId) + else -> Result.success(cached) + } + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun getSyncOrchestrator(sessionId: SessionId): SyncOrchestrator? { + return sessionIdsToMatrixSession[sessionId]?.syncOrchestrator + } + + @Suppress("UNCHECKED_CAST") + fun restoreWithSavedState(state: SavedStateMap?) { + Timber.d("Restore state") + if (state == null || sessionIdsToMatrixSession.isNotEmpty()) { + Timber.w("Restore with non-empty map") + return + } + val sessionIds = state[SAVE_INSTANCE_KEY] as? Array + Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}") + if (sessionIds.isNullOrEmpty()) return + // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. + runBlocking { + sessionIds.forEach { sessionId -> + getOrRestore(sessionId) + } + } + } + + fun saveIntoSavedState(state: MutableSavedStateMap) { + val sessionKeys = sessionIdsToMatrixSession.keys.toTypedArray() + Timber.d("Save matrix session keys = ${sessionKeys.map { it.value }}") + state[SAVE_INSTANCE_KEY] = sessionKeys + } + + private suspend fun restore(sessionId: SessionId): Result { + Timber.d("Restore matrix session: $sessionId") + return authenticationService.restoreSession(sessionId) + .onSuccess { matrixClient -> + val syncOrchestrator = syncOrchestratorFactory.create(matrixClient) + sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession( + matrixClient = matrixClient, + syncOrchestrator = syncOrchestrator, + ) + syncOrchestrator.start() + } + .onFailure { + Timber.e(it, "Fail to restore session") + } + } +} + +private data class InMemoryMatrixSession( + val matrixClient: MatrixClient, + val syncOrchestrator: SyncOrchestrator, +) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt index 16575f1a91b..4911ab50b50 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/RoomComponentFactory.kt @@ -7,8 +7,8 @@ package io.element.android.appnav.di -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom interface RoomComponentFactory { - fun create(room: MatrixRoom): Any + fun create(room: JoinedRoom): Any } 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 new file mode 100644 index 00000000000..d3ab19466f6 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt @@ -0,0 +1,133 @@ +/* + * 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.appnav.di + +import androidx.annotation.VisibleForTesting +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.networkmonitor.api.NetworkMonitor +import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.coroutine.childScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.sync.SyncState +import io.element.android.services.appnavstate.api.AppForegroundStateService +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class SyncOrchestrator @AssistedInject constructor( + @Assisted matrixClient: MatrixClient, + private val appForegroundStateService: AppForegroundStateService, + private val networkMonitor: NetworkMonitor, + dispatchers: CoroutineDispatchers, +) { + @AssistedFactory + interface Factory { + fun create(matrixClient: MatrixClient): SyncOrchestrator + } + + private val syncService = matrixClient.syncService() + + private val tag = "SyncOrchestrator" + + private val coroutineScope = matrixClient.sessionCoroutineScope.childScope(dispatchers.io, tag) + + private val started = AtomicBoolean(false) + + /** + * Starting observing the app state and network state to start/stop the sync service. + * + * Before observing the state, a first attempt at starting the sync service will happen if it's not already running. + */ + fun start() { + if (!started.compareAndSet(false, true)) { + Timber.tag(tag).d("already started, exiting early") + return + } + + coroutineScope.launch { + // Perform an initial sync if the sync service is not running, to check whether the homeserver is accessible + // Otherwise, if the device is offline the sync service will never start and the SyncState will be Idle, not Offline + Timber.tag(tag).d("performing initial sync attempt") + syncService.startSync() + + // Wait until the sync service is not idle, either it will be running or in error/offline state + syncService.syncState.first { it != SyncState.Idle } + + observeStates() + } + } + + @OptIn(FlowPreview::class) + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun observeStates() = coroutineScope.launch { + Timber.tag(tag).d("start observing the app and network state") + + val isAppActiveFlow = combine( + appForegroundStateService.isInForeground, + appForegroundStateService.isInCall, + appForegroundStateService.isSyncingNotificationEvent, + appForegroundStateService.hasRingingCall, + ) { 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), + networkMonitor.connectivity, + isAppActiveFlow, + ) { syncState, networkState, isAppActive -> + val isNetworkAvailable = networkState == NetworkStatus.Connected + + Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable") + if (syncState == SyncState.Running && !isAppActive) { + SyncStateAction.StopSync + } else if (syncState == SyncState.Idle && isAppActive && isNetworkAvailable) { + SyncStateAction.StartSync + } else { + SyncStateAction.NoOp + } + } + .distinctUntilChanged() + .debounce { action -> + // Don't stop the sync immediately, wait a bit to avoid starting/stopping the sync too often + if (action == SyncStateAction.StopSync) 3.seconds else 0.seconds + } + .onCompletion { + Timber.tag(tag).d("has been stopped") + } + .collect { action -> + when (action) { + SyncStateAction.StartSync -> { + syncService.startSync() + } + SyncStateAction.StopSync -> { + syncService.stopSync() + } + SyncStateAction.NoOp -> Unit + } + } + } +} + +private enum class SyncStateAction { + StartSync, + StopSync, + NoOp, +} 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 8e802c323ee..786b694c3fd 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 @@ -8,6 +8,8 @@ package io.element.android.appnav.intent import android.content.Intent +import io.element.android.features.login.api.LoginIntentResolver +import io.element.android.features.login.api.LoginParams import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkData @@ -21,11 +23,13 @@ sealed interface ResolvedIntent { data class Navigation(val deeplinkData: DeeplinkData) : 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 intent: Intent) : ResolvedIntent } class IntentResolver @Inject constructor( private val deeplinkParser: DeeplinkParser, + private val loginIntentResolver: LoginIntentResolver, private val oidcIntentResolver: OidcIntentResolver, private val permalinkParser: PermalinkParser, ) { @@ -40,10 +44,17 @@ class IntentResolver @Inject constructor( val oidcAction = oidcIntentResolver.resolve(intent) if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction) - // External link clicked? (matrix.to, element.io, etc.) - val permalinkData = intent + val actionViewData = intent .takeIf { it.action == Intent.ACTION_VIEW } ?.dataString + + // Mobile configuration link clicked? (mobile.element.io) + val mobileLoginData = actionViewData + ?.let { loginIntentResolver.parse(it) } + if (mobileLoginData != null) return ResolvedIntent.Login(mobileLoginData) + + // External link clicked? (matrix.to, element.io, etc.) + val permalinkData = actionViewData ?.let { permalinkParser.parse(it) } ?.takeIf { it !is PermalinkData.FallbackLink } if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData) 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 9e673d59d3b..b825126ed7e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -21,20 +21,22 @@ import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.UserProperties 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.log.logger.LoggerTag +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 -import io.element.android.libraries.matrix.api.sync.isOnline import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus -import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase import io.element.android.libraries.push.api.PushService import io.element.android.libraries.pushproviders.api.RegistrationFailure import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -51,7 +53,7 @@ class LoggedInPresenter @Inject constructor( private val sessionVerificationService: SessionVerificationService, private val analyticsService: AnalyticsService, private val encryptionService: EncryptionService, - private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase, + private val buildMeta: BuildMeta, ) : Presenter { @Composable override fun present(): LoggedInState { @@ -60,6 +62,7 @@ class LoggedInPresenter @Inject constructor( pushService.ignoreRegistrationError(matrixClient.sessionId) }.collectAsState(initial = false) val pusherRegistrationState = remember>> { mutableStateOf(AsyncData.Uninitialized) } + LaunchedEffect(Unit) { preloadAccountManagementUrl() } LaunchedEffect(Unit) { sessionVerificationService.sessionVerifiedStatus .onEach { sessionVerifiedStatus -> @@ -76,7 +79,7 @@ class LoggedInPresenter @Inject constructor( .launchIn(this) } val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState() - val isOnline by syncService.isOnline().collectAsState() + val isOnline by syncService.isOnline.collectAsState() val showSyncSpinner by remember { derivedStateOf { isOnline && syncIndicator == RoomListService.SyncIndicator.Show @@ -103,12 +106,10 @@ class LoggedInPresenter @Inject constructor( } } LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch { - forceNativeSlidingSyncMigration = matrixClient.forceNativeSlidingSyncMigration().getOrDefault(false) + forceNativeSlidingSyncMigration = matrixClient.needsForcedNativeSlidingSyncMigration().getOrDefault(false) } LoggedInEvents.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch { - // Enable native sliding sync if it wasn't already the case - enableNativeSlidingSyncUseCase() - // Then force the logout + // Force the logout since Native Sliding Sync is already enforced by the SDK matrixClient.logout(userInitiated = true, ignoreSdkError = true) } } @@ -119,20 +120,15 @@ class LoggedInPresenter @Inject constructor( pusherRegistrationState = pusherRegistrationState.value, ignoreRegistrationError = ignoreRegistrationError, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, + appName = buildMeta.applicationName, eventSink = ::handleEvent ) } - // Force the user to log out if they were using the proxy sliding sync and it's no longer available, but native sliding sync is. - private suspend fun MatrixClient.forceNativeSlidingSyncMigration(): Result = runCatching { + // Force the user to log out if they were using the proxy sliding sync as it's no longer supported by the SDK + private suspend fun MatrixClient.needsForcedNativeSlidingSyncMigration(): Result = runCatchingExceptions { val currentSlidingSyncVersion = currentSlidingSyncVersion().getOrThrow() - if (currentSlidingSyncVersion == SlidingSyncVersion.Proxy) { - val availableSlidingSyncVersions = availableSlidingSyncVersions().getOrThrow() - availableSlidingSyncVersions.contains(SlidingSyncVersion.Native) && - !availableSlidingSyncVersions.contains(SlidingSyncVersion.Proxy) - } else { - false - } + currentSlidingSyncVersion == SlidingSyncVersion.Proxy } private suspend fun ensurePusherIsRegistered(pusherRegistrationState: MutableState>) { @@ -209,4 +205,9 @@ class LoggedInPresenter @Inject constructor( analyticsService.capture(CryptoSessionStateChange(changeRecoveryState, changeVerificationState)) } } + + private fun CoroutineScope.preloadAccountManagementUrl() = launch { + matrixClient.getAccountManagementUrl(AccountManagementAction.Profile) + matrixClient.getAccountManagementUrl(AccountManagementAction.SessionsList) + } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index 540d5d0d8f7..8f039ffa2be 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -14,5 +14,6 @@ data class LoggedInState( val pusherRegistrationState: AsyncData, val ignoreRegistrationError: Boolean, val forceNativeSlidingSyncMigration: Boolean, + val appName: String, val eventSink: (LoggedInEvents) -> Unit, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index 43b7164f2a2..2ba0c8de81d 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -24,10 +24,12 @@ fun aLoggedInState( showSyncSpinner: Boolean = false, pusherRegistrationState: AsyncData = AsyncData.Uninitialized, forceNativeSlidingSyncMigration: Boolean = false, + appName: String = "Element X", ) = LoggedInState( showSyncSpinner = showSyncSpinner, pusherRegistrationState = pusherRegistrationState, ignoreRegistrationError = false, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, + appName = appName, eventSink = {}, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index cd9ecf637a4..d6a12ea27bd 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -73,9 +73,12 @@ fun LoggedInView( // Set the force migration dialog here so it's always displayed over every screen if (state.forceNativeSlidingSyncMigration) { - ForceNativeSlidingSyncMigrationDialog(onSubmit = { - state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync) - }) + ForceNativeSlidingSyncMigrationDialog( + appName = state.appName, + onSubmit = { + state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync) + } + ) } } @@ -98,11 +101,12 @@ private fun Throwable.getReason(): String? { @Composable private fun ForceNativeSlidingSyncMigrationDialog( + appName: String, onSubmit: () -> Unit, ) { ErrorDialog( title = null, - content = stringResource(R.string.banner_migrate_to_native_sliding_sync_force_logout_title), + content = stringResource(R.string.banner_migrate_to_native_sliding_sync_app_force_logout_title, appName), submitText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action), onSubmit = onSubmit, canDismiss = false, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt new file mode 100644 index 00000000000..d9ed15318a9 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt @@ -0,0 +1,58 @@ +/* + * 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.appnav.loggedin + +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.media.MediaPreviewService +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +/** + * This migration is temporary, will be safe to remove after some time. + * The goal is to set the server config if it's not set, and remove the local data. + */ +class MediaPreviewConfigMigration @Inject constructor( + private val mediaPreviewService: MediaPreviewService, + private val appPreferencesStore: AppPreferencesStore, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, +) { + @Suppress("DEPRECATION") + operator fun invoke() = sessionCoroutineScope.launch { + val hideInviteAvatars = appPreferencesStore.getHideInviteAvatarsFlow().first() + val mediaPreviewValue = appPreferencesStore.getTimelineMediaPreviewValueFlow().first() + if (hideInviteAvatars == null && mediaPreviewValue == null) { + // No local data, abort. + return@launch + } + mediaPreviewService + .fetchMediaPreviewConfig() + .onSuccess { config -> + if (config != null) { + appPreferencesStore.setHideInviteAvatars(null) + appPreferencesStore.setTimelineMediaPreviewValue(null) + } else { + if (hideInviteAvatars != null) { + mediaPreviewService.setHideInviteAvatars(hideInviteAvatars) + appPreferencesStore.setHideInviteAvatars(null) + } + if (mediaPreviewValue != null) { + mediaPreviewService.setMediaPreviewValue(mediaPreviewValue) + appPreferencesStore.setTimelineMediaPreviewValue(null) + } + } + } + .onFailure { + Timber.e(it, "Couldn't perform migration, failed to fetch media preview config.") + } + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt index 458e6e5c495..cbb247569a5 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendQueues.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import timber.log.Timber import javax.inject.Inject @VisibleForTesting @@ -32,7 +33,7 @@ class SendQueues @Inject constructor( ) { /** * Launches the send queues retry mechanism in the given [coroutineScope]. - * Makes sure to re-enable all send queues when the network status is [NetworkStatus.Online]. + * Makes sure to re-enable all send queues when the network status is [NetworkStatus.Connected]. */ @OptIn(FlowPreview::class) fun launchIn(coroutineScope: CoroutineScope) { @@ -42,7 +43,9 @@ class SendQueues @Inject constructor( ) { syncState, _ -> syncState } .debounce(SEND_QUEUES_RETRY_DELAY_MILLIS) .onEach { syncState -> + Timber.tag("SendQueues").d("Sync state changed: $syncState") if (syncState == SyncState.Running) { + Timber.tag("SendQueues").d("Enabling send queues again") matrixClient.setAllSendQueuesEnabled(enabled = true) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index 56f3716ca1e..103e97b2583 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -28,7 +28,6 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.LoadingRoomNodeView -import io.element.android.appnav.room.joined.LoadingRoomState import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint import io.element.android.features.roomdirectory.api.RoomDescription @@ -43,13 +42,11 @@ import io.element.android.libraries.matrix.api.MatrixClient 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 -import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias -import io.element.android.libraries.matrix.api.getRoomInfoFlow import io.element.android.libraries.matrix.api.room.CurrentUserMembership 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.sync.SyncService -import io.element.android.libraries.matrix.api.sync.isOnline +import io.element.android.libraries.matrix.ui.room.LoadingRoomState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first @@ -125,7 +122,7 @@ class RoomFlowNode @AssistedInject constructor( } private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List) { - val roomInfoFlow = client.getRoomInfoFlow(roomIdOrAlias = roomId.toRoomIdOrAlias()) + val roomInfoFlow = client.getRoomInfoFlow(roomId) val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged() val currentMembershipFlow = roomInfoFlow.map { it.getOrNull()?.currentUserMembership }.distinctUntilChanged() combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace -> @@ -211,7 +208,7 @@ class RoomFlowNode @AssistedInject constructor( } private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier -> - val isOnline by syncService.isOnline().collectAsState() + val isOnline by syncService.isOnline.collectAsState() LoadingRoomNodeView( state = LoadingRoomState.Loading, hasNetworkConnection = isOnline, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt index 49a05c9d365..07580060c73 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt @@ -36,7 +36,8 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.sync.SyncService -import io.element.android.libraries.matrix.api.sync.isOnline +import io.element.android.libraries.matrix.ui.room.LoadingRoomState +import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -114,7 +115,7 @@ class JoinedRoomFlowNode @AssistedInject constructor( private fun loadingNode(buildContext: BuildContext, onBackClick: () -> Unit) = node(buildContext) { modifier -> val loadingRoomState by loadingRoomStateStateFlow.collectAsState() - val isOnline by syncService.isOnline().collectAsState() + val isOnline by syncService.isOnline.collectAsState() LoadingRoomNodeView( state = loadingRoomState, hasNetworkConnection = isOnline, 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 df2e25d4b38..3e5947aada9 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 @@ -9,9 +9,7 @@ package io.element.android.appnav.room.joined import android.os.Parcelable import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext @@ -32,12 +30,14 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.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.permalink.PermalinkData -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -51,12 +51,14 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( private val messagesEntryPoint: MessagesEntryPoint, private val roomDetailsEntryPoint: RoomDetailsEntryPoint, private val appNavigationStateService: AppNavigationStateService, - private val appCoroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val matrixClient: MatrixClient, + private val activeRoomsHolder: ActiveRoomsHolder, roomComponentFactory: RoomComponentFactory, ) : BaseFlowNode( backstack = BackStack( - initialElement = when (val input = plugins.filterIsInstance(Inputs::class.java).first().initialElement) { + initialElement = when (val input = plugins.filterIsInstance().first().initialElement) { is RoomNavigationTarget.Messages -> NavTarget.Messages(input.focusedEventId) RoomNavigationTarget.Details -> NavTarget.RoomDetails RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings @@ -74,7 +76,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( } data class Inputs( - val room: MatrixRoom, + val room: JoinedRoom, val initialElement: RoomNavigationTarget, ) : NodeInputs @@ -87,16 +89,19 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( onCreate = { Timber.v("OnCreate => ${inputs.room.roomId}") appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId) + activeRoomsHolder.addRoom(inputs.room) fetchRoomMembers() trackVisitedRoom() }, onResume = { - appCoroutineScope.launch { + sessionCoroutineScope.launch { inputs.room.subscribeToSync() } }, onDestroy = { Timber.v("OnDestroy") + activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId) + inputs.room.destroy() appNavigationStateService.onLeavingRoom(id) } ) @@ -197,16 +202,6 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - // Rely on the View Lifecycle in addition to the Node Lifecycle, - // because this node enters 'onDestroy' before his children, so it can leads to - // using the room in a child node where it's already closed. - DisposableEffect(Unit) { - onDispose { - if (lifecycle.currentState == Lifecycle.State.DESTROYED) { - inputs.room.destroy() - } - } - } BackstackView() } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt index 83c4e0e6232..938e46f9156 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt @@ -31,6 +31,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.ui.room.LoadingRoomState +import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt index 731072889dd..08e999245bf 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt @@ -9,7 +9,7 @@ package io.element.android.appnav.root import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.SavedStateMap -import io.element.android.appnav.di.MatrixClientsHolder +import io.element.android.appnav.di.MatrixSessionCache import io.element.android.features.login.api.LoginUserStory import io.element.android.features.preferences.api.CacheService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -31,7 +31,7 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFact class RootNavStateFlowFactory @Inject constructor( private val authenticationService: MatrixAuthenticationService, private val cacheService: CacheService, - private val matrixClientsHolder: MatrixClientsHolder, + private val matrixSessionCache: MatrixSessionCache, private val imageLoaderHolder: ImageLoaderHolder, private val loginUserStory: LoginUserStory, private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory, @@ -63,7 +63,7 @@ class RootNavStateFlowFactory @Inject constructor( val initialCacheIndex = savedStateMap.getCacheIndexOrDefault() return cacheService.clearedCacheEventFlow .onEach { sessionId -> - matrixClientsHolder.remove(sessionId) + matrixSessionCache.remove(sessionId) // Ensure image loader will be recreated with the new MatrixClient imageLoaderHolder.remove(sessionId) // Also remove cached value for SessionPreferencesStore diff --git a/appnav/src/main/res/values-cs/translations.xml b/appnav/src/main/res/values-cs/translations.xml index e749887bc4b..3652bda56f1 100644 --- a/appnav/src/main/res/values-cs/translations.xml +++ b/appnav/src/main/res/values-cs/translations.xml @@ -1,5 +1,6 @@ "Odhlásit se a upgradovat" + "%1$s již nepodporuje starý protokol. Odhlaste se a znovu přihlaste, abyste mohli pokračovat v používání aplikace." "Váš domovský server již nepodporuje starý protokol. Chcete-li pokračovat v používání aplikace, odhlaste se a znovu se přihlaste." diff --git a/appnav/src/main/res/values-cy/translations.xml b/appnav/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..06d53cd61bc --- /dev/null +++ b/appnav/src/main/res/values-cy/translations.xml @@ -0,0 +1,6 @@ + + + "Allgofnodi ac Uwchraddio" + "Nid yw %1$s bellach yn cefnogi\'r hen brotocol. Allgofnodwch a mewngofnodi\'n ôl i barhau i ddefnyddio\'r ap." + "Nid yw eich gweinydd cartref yn cefnogi\'r hen brotocol mwyach. Allgofnodwch a mewngofnodi yn ôl i barhau i ddefnyddio\'r ap." + diff --git a/appnav/src/main/res/values-da/translations.xml b/appnav/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..ef5025ed614 --- /dev/null +++ b/appnav/src/main/res/values-da/translations.xml @@ -0,0 +1,6 @@ + + + "Log ud og opgradér" + "%1$s understøtter ikke længere den gamle protokol. Log ud og log ind igen for at fortsætte med at bruge appen." + "Din hjemmeserver understøtter ikke længere den gamle protokol. Log ud og log ind igen for at fortsætte med at bruge appen." + diff --git a/appnav/src/main/res/values-de/translations.xml b/appnav/src/main/res/values-de/translations.xml index 2b33dacaca0..339d346a6b7 100644 --- a/appnav/src/main/res/values-de/translations.xml +++ b/appnav/src/main/res/values-de/translations.xml @@ -1,5 +1,6 @@ "Abmelden und aktualisieren" + "%1$s unterstützt das alte Protokoll nicht mehr. Bitte melden Sie sich ab und wieder an, um die App weiter nutzen zu können." "Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen." diff --git a/appnav/src/main/res/values-el/translations.xml b/appnav/src/main/res/values-el/translations.xml index ef9f8d3c7dc..03848d79c07 100644 --- a/appnav/src/main/res/values-el/translations.xml +++ b/appnav/src/main/res/values-el/translations.xml @@ -1,5 +1,6 @@ "Αποσύνδεση & Αναβάθμιση" + "Το %1$s δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδεθείτε και συνδεθείτε ξανά για να συνεχίσετε να χρησιμοποιείτε την εφαρμογή." "Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή." diff --git a/appnav/src/main/res/values-es/translations.xml b/appnav/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..20f155513e2 --- /dev/null +++ b/appnav/src/main/res/values-es/translations.xml @@ -0,0 +1,6 @@ + + + "Cerrar sesión y actualizar" + "%1$s ya no es compatible con el antiguo protocolo. Cierra sesión y vuelve a iniciarla para seguir usando la aplicación." + "Tu servidor base ya no es compatible con el protocolo anterior. Cierra sesión y vuelve a iniciarla para seguir usando la aplicación." + diff --git a/appnav/src/main/res/values-et/translations.xml b/appnav/src/main/res/values-et/translations.xml index 1631134d607..b71640a4b4b 100644 --- a/appnav/src/main/res/values-et/translations.xml +++ b/appnav/src/main/res/values-et/translations.xml @@ -1,5 +1,6 @@ "Logi välja ja uuenda" + "%1$s enam ei toeta vana protokolli. Kui soovid rakendust edasi kasutada, siis logi korraks temast välja ning seejärel tagasi." "Sinu koduserver enam ei toeta vana protokolli. Jätkamaks rakenduse kasutamist palun logi välja ning seejärel tagasi." diff --git a/appnav/src/main/res/values-eu/translations.xml b/appnav/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..909ade151c6 --- /dev/null +++ b/appnav/src/main/res/values-eu/translations.xml @@ -0,0 +1,6 @@ + + + "Amaitu saioa eta bertsio-berritu" + "%1$s(e)k ez da bateragarria lehengo protokoloarekin. Amaitu saioa eta hasi berriro aplikazioa erabiltzen jarraitzeko." + "Zure zerbitzaria ez da bateragarria protokolo zaharrarekin. Amaitu saioa eta hasi berriro aplikazioa erabiltzen jarraitzeko." + diff --git a/appnav/src/main/res/values-fi/translations.xml b/appnav/src/main/res/values-fi/translations.xml index bdd10a92c44..14605ea6e1c 100644 --- a/appnav/src/main/res/values-fi/translations.xml +++ b/appnav/src/main/res/values-fi/translations.xml @@ -1,5 +1,6 @@ "Kirjaudu Ulos & Päivitä" + "%1$s ei enää tue vanhaa protokollaa. Kirjaudu ulos ja takaisin sisään jatkaaksesi sovelluksen käyttöä." "Kotipalvelimesi ei enää tue vanhaa protokollaa. Kirjaudu ulos ja takaisin sisään jatkaaksesi sovelluksen käyttöä." diff --git a/appnav/src/main/res/values-fr/translations.xml b/appnav/src/main/res/values-fr/translations.xml index 6295bc88df3..e3885f84baf 100644 --- a/appnav/src/main/res/values-fr/translations.xml +++ b/appnav/src/main/res/values-fr/translations.xml @@ -1,5 +1,6 @@ "Déconnecter et mettre à niveau" + "%1$s ne prend plus en charge l’ancien protocole. Veuillez vous déconnecter puis vous reconnecter pour continuer à utiliser l’application." "Votre serveur d’accueil ne prend plus en charge l’ancien protocole. Veuillez vous déconnecter puis vous reconnecter pour continuer à utiliser l’application." diff --git a/appnav/src/main/res/values-hu/translations.xml b/appnav/src/main/res/values-hu/translations.xml index e4b24f811b3..a4dca1c214a 100644 --- a/appnav/src/main/res/values-hu/translations.xml +++ b/appnav/src/main/res/values-hu/translations.xml @@ -1,5 +1,6 @@ "Kijelentkezés és frissítés" + "%1$s már nem támogatja a régi protokollt. Kérjük, jelentkezzen ki és jelentkezzen be újra az alkalmazás használatának folytatásához." "A Matrix-kiszolgáló már nem támogatja a régi protokollt. Az alkalmazás további használatához jelentkezzen ki és be." diff --git a/appnav/src/main/res/values-in/translations.xml b/appnav/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..e4f445dbc7f --- /dev/null +++ b/appnav/src/main/res/values-in/translations.xml @@ -0,0 +1,6 @@ + + + "Keluar & Tingkatkan" + "%1$s tidak lagi mendukung protokol lama. Silakan keluar dan masuk kembali untuk terus menggunakan aplikasi." + "Homeserver Anda tidak lagi mendukung protokol lama. Silakan keluar dan masuk kembali untuk terus menggunakan aplikasi." + diff --git a/appnav/src/main/res/values-it/translations.xml b/appnav/src/main/res/values-it/translations.xml index d93dc1e6883..725604ed3f7 100644 --- a/appnav/src/main/res/values-it/translations.xml +++ b/appnav/src/main/res/values-it/translations.xml @@ -1,5 +1,6 @@ "Esci e aggiorna" + "%1$s non supporta più il vecchio protocollo. Esci e accedi nuovamente per continuare a utilizzare l\'app." "Il tuo homeserver non supporta più il vecchio protocollo. Esci e rientra per continuare a usare l\'app." diff --git a/appnav/src/main/res/values-nb/translations.xml b/appnav/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..82704998535 --- /dev/null +++ b/appnav/src/main/res/values-nb/translations.xml @@ -0,0 +1,6 @@ + + + "Logg ut og oppgrader" + "%1$s støtter ikke lenger den gamle protokollen. Logg ut og logg inn igjen for å fortsette å bruke appen." + "Hjemmeserveren din støtter ikke lenger den gamle protokollen. Vennligst logg ut og inn igjen for å fortsette å bruke appen." + diff --git a/appnav/src/main/res/values-pl/translations.xml b/appnav/src/main/res/values-pl/translations.xml index 0df56520b1c..68de66a54eb 100644 --- a/appnav/src/main/res/values-pl/translations.xml +++ b/appnav/src/main/res/values-pl/translations.xml @@ -1,5 +1,6 @@ "Wyloguj się i zaktualizuj" + "%1$s już nie wspiera starego protokołu. Zaloguj się ponownie, aby dalej korzystać z aplikacji." "Twój serwer domowy już nie wspiera starego protokołu. Zaloguj się ponownie, aby kontynuować korzystanie z aplikacji." diff --git a/appnav/src/main/res/values-pt-rBR/translations.xml b/appnav/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..385e07d7d19 --- /dev/null +++ b/appnav/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,6 @@ + + + "Sair e atualizar" + "%1$s não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação." + "Seu servidor doméstico não é mais compatível com o protocolo antigo. Faça logout e login novamente para continuar usando o aplicativo." + diff --git a/appnav/src/main/res/values-pt/translations.xml b/appnav/src/main/res/values-pt/translations.xml index 8b29a17ecfe..d606a8d103a 100644 --- a/appnav/src/main/res/values-pt/translations.xml +++ b/appnav/src/main/res/values-pt/translations.xml @@ -1,5 +1,6 @@ "Sair & Atualizar" + "%1$s já não suporta o protocolo antigo. Termina a sessão e volta a iniciar sessão para continuares a utilizar a aplicação." "Seu homeserver não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação." diff --git a/appnav/src/main/res/values-ru/translations.xml b/appnav/src/main/res/values-ru/translations.xml index ac03f180984..45f00a7cb21 100644 --- a/appnav/src/main/res/values-ru/translations.xml +++ b/appnav/src/main/res/values-ru/translations.xml @@ -1,5 +1,6 @@ "Выйти и обновить" + "%1$s больше не поддерживает старый протокол. Пожалуйста, выйдите из системы и войдите снова, чтобы продолжить использование приложения." "Ваш домашний сервер больше не поддерживает старый протокол. Пожалуйста, выйдите и войдите в свою учётную запись снова, чтобы продолжить использование приложения." diff --git a/appnav/src/main/res/values-sk/translations.xml b/appnav/src/main/res/values-sk/translations.xml index d1ac766f8cb..a75c2fd3d0f 100644 --- a/appnav/src/main/res/values-sk/translations.xml +++ b/appnav/src/main/res/values-sk/translations.xml @@ -1,5 +1,6 @@ "Odhlásiť sa a aktualizovať" + "%1$s už nepodporuje starý protokol. Odhláste sa a znova prihláste, aby ste mohli pokračovať v používaní aplikácie." "Váš domovský server už nepodporuje starý protokol. Ak chcete pokračovať v používaní aplikácie, odhláste sa a znova sa prihláste." diff --git a/appnav/src/main/res/values-sv/translations.xml b/appnav/src/main/res/values-sv/translations.xml index 89726dd893f..408e02057ef 100644 --- a/appnav/src/main/res/values-sv/translations.xml +++ b/appnav/src/main/res/values-sv/translations.xml @@ -1,5 +1,6 @@ "Logga ut och uppgradera" + "%1$s stöder inte längre det gamla protokollet. Logga ut och logga in igen för att fortsätta använda appen." "Din hemserver stöder inte längre det gamla protokollet. Logga ut och logga in igen för att fortsätta använda appen." diff --git a/appnav/src/main/res/values-tr/translations.xml b/appnav/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..c0a8ccd1361 --- /dev/null +++ b/appnav/src/main/res/values-tr/translations.xml @@ -0,0 +1,5 @@ + + + "Çıkış Yap ve Yükselt" + "Ana sunucunuz artık eski protokolü desteklemiyor. Lütfen oturumu kapatın ve uygulamayı kullanmaya devam etmek için tekrar oturum açın." + diff --git a/appnav/src/main/res/values-uk/translations.xml b/appnav/src/main/res/values-uk/translations.xml index d2ee822d30c..b65ab1a1cb9 100644 --- a/appnav/src/main/res/values-uk/translations.xml +++ b/appnav/src/main/res/values-uk/translations.xml @@ -1,5 +1,6 @@ "Вийти та оновити" + "%1$s більше не підтримує старий протокол. Вийдіть і знов увійдіть, щоб продовжити користуватися застосунком." "Ваш домашній сервер більше не підтримує старий протокол. Будь ласка, вийдіть і увійдіть знову, щоб продовжити використання програми." diff --git a/appnav/src/main/res/values-zh-rTW/translations.xml b/appnav/src/main/res/values-zh-rTW/translations.xml index 5ff8996b041..ea69a33df89 100644 --- a/appnav/src/main/res/values-zh-rTW/translations.xml +++ b/appnav/src/main/res/values-zh-rTW/translations.xml @@ -1,5 +1,6 @@ "登出並升級" + "%1$s 不再支援舊版通訊協定。請登出並重新登入以繼續使用應用程式。" "您的家伺服器不再支援舊協定。請登出並重新登入以繼續使用應用程式。" diff --git a/appnav/src/main/res/values-zh/translations.xml b/appnav/src/main/res/values-zh/translations.xml index 134f3c07fe3..406471196e0 100644 --- a/appnav/src/main/res/values-zh/translations.xml +++ b/appnav/src/main/res/values-zh/translations.xml @@ -1,5 +1,6 @@ "登出并升级" + "%1$s 不再支持旧协议。请注销并重新登录以继续使用该应用程序。" "您的服务器不再支持旧协议。请登出并重新登录以继续使用此应用。" diff --git a/appnav/src/main/res/values/localazy.xml b/appnav/src/main/res/values/localazy.xml index bb6df44051e..c018fbf7fd8 100644 --- a/appnav/src/main/res/values/localazy.xml +++ b/appnav/src/main/res/values/localazy.xml @@ -1,5 +1,6 @@ "Log Out & Upgrade" + "%1$s no longer supports the old protocol. Please log out and log back in to continue using the app." "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app." diff --git a/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt deleted file mode 100644 index be317701bbb..00000000000 --- a/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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. - */ - -package io.element.android.appnav - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Lifecycle -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.node.node -import com.bumble.appyx.core.plugin.Plugin -import com.bumble.appyx.navmodel.backstack.activeElement -import com.bumble.appyx.testing.junit4.util.MainDispatcherRule -import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper -import com.google.common.truth.Truth.assertThat -import io.element.android.appnav.di.RoomComponentFactory -import io.element.android.appnav.room.RoomNavigationTarget -import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode -import io.element.android.features.messages.api.MessagesEntryPoint -import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint -import io.element.android.libraries.architecture.childNode -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.services.appnavstate.test.FakeAppNavigationStateService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class JoinRoomLoadedFlowNodeTest { - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() - - @get:Rule - val mainDispatcherRule = MainDispatcherRule() - - private class FakeMessagesEntryPoint : MessagesEntryPoint, MessagesEntryPoint.NodeBuilder { - var buildContext: BuildContext? = null - var nodeId: String? = null - var parameters: MessagesEntryPoint.Params? = null - var callback: MessagesEntryPoint.Callback? = null - - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder { - this.buildContext = buildContext - return this - } - - override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder { - parameters = params - return this - } - - override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder { - this.callback = callback - return this - } - - override fun build(): Node { - return node(buildContext!!) {}.also { - nodeId = it.id - } - } - } - - private class FakeRoomComponentFactory : RoomComponentFactory { - override fun create(room: MatrixRoom): Any { - return Unit - } - } - - private class FakeRoomDetailsEntryPoint : RoomDetailsEntryPoint { - var nodeId: String? = null - - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder { - return object : RoomDetailsEntryPoint.NodeBuilder { - override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder { - return this - } - - override fun callback(callback: RoomDetailsEntryPoint.Callback): RoomDetailsEntryPoint.NodeBuilder { - return this - } - - override fun build(): Node { - return node(buildContext) {}.also { - nodeId = it.id - } - } - } - } - } - - private fun createJoinedRoomLoadedFlowNode( - plugins: List, - messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), - roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), - coroutineScope: CoroutineScope, - ) = JoinedRoomLoadedFlowNode( - buildContext = BuildContext.root(savedStateMap = null), - plugins = plugins, - messagesEntryPoint = messagesEntryPoint, - roomDetailsEntryPoint = roomDetailsEntryPoint, - appNavigationStateService = FakeAppNavigationStateService(), - appCoroutineScope = coroutineScope, - roomComponentFactory = FakeRoomComponentFactory(), - matrixClient = FakeMatrixClient(), - ) - - @Test - fun `given a room flow node when initialized then it loads messages entry point`() = runTest { - // GIVEN - val room = FakeMatrixRoom( - updateMembersResult = { } - ) - val fakeMessagesEntryPoint = FakeMessagesEntryPoint() - val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) - val roomFlowNode = createJoinedRoomLoadedFlowNode( - plugins = listOf(inputs), - messagesEntryPoint = fakeMessagesEntryPoint, - coroutineScope = this - ) - // WHEN - val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() - - // THEN - assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages()) - roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(), Lifecycle.State.CREATED) - val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages())!! - assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId) - } - - @Test - fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest { - // GIVEN - val room = FakeMatrixRoom( - updateMembersResult = { } - ) - val fakeMessagesEntryPoint = FakeMessagesEntryPoint() - val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() - val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) - val roomFlowNode = createJoinedRoomLoadedFlowNode( - plugins = listOf(inputs), - messagesEntryPoint = fakeMessagesEntryPoint, - roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, - coroutineScope = this - ) - val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() - // WHEN - fakeMessagesEntryPoint.callback?.onRoomDetailsClick() - // THEN - roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) - val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!! - assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId) - } -} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt new file mode 100644 index 00000000000..bbaa1965202 --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt @@ -0,0 +1,207 @@ +/* + * 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. + */ + +package io.element.android.appnav + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.activeElement +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper +import com.google.common.truth.Truth.assertThat +import io.element.android.appnav.di.RoomComponentFactory +import io.element.android.appnav.room.RoomNavigationTarget +import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode +import io.element.android.features.messages.api.MessagesEntryPoint +import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint +import io.element.android.libraries.architecture.childNode +import io.element.android.libraries.matrix.api.room.JoinedRoom +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.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.services.appnavstate.api.ActiveRoomsHolder +import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class JoinedRoomLoadedFlowNodeTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private class FakeMessagesEntryPoint : MessagesEntryPoint, MessagesEntryPoint.NodeBuilder { + var buildContext: BuildContext? = null + var nodeId: String? = null + var parameters: MessagesEntryPoint.Params? = null + var callback: MessagesEntryPoint.Callback? = null + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder { + this.buildContext = buildContext + return this + } + + override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder { + parameters = params + return this + } + + override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder { + this.callback = callback + return this + } + + override fun build(): Node { + return node(buildContext!!) {}.also { + nodeId = it.id + } + } + } + + private class FakeRoomComponentFactory : RoomComponentFactory { + override fun create(room: JoinedRoom): Any { + return Unit + } + } + + private class FakeRoomDetailsEntryPoint : RoomDetailsEntryPoint { + var nodeId: String? = null + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder { + return object : RoomDetailsEntryPoint.NodeBuilder { + override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder { + return this + } + + override fun callback(callback: RoomDetailsEntryPoint.Callback): RoomDetailsEntryPoint.NodeBuilder { + return this + } + + override fun build(): Node { + return node(buildContext) {}.also { + nodeId = it.id + } + } + } + } + } + + private fun TestScope.createJoinedRoomLoadedFlowNode( + plugins: List, + messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), + roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), + activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(), + ) = JoinedRoomLoadedFlowNode( + buildContext = BuildContext.root(savedStateMap = null), + plugins = plugins, + messagesEntryPoint = messagesEntryPoint, + roomDetailsEntryPoint = roomDetailsEntryPoint, + appNavigationStateService = FakeAppNavigationStateService(), + sessionCoroutineScope = this, + roomComponentFactory = FakeRoomComponentFactory(), + matrixClient = FakeMatrixClient(), + activeRoomsHolder = activeRoomsHolder, + ) + + @Test + fun `given a room flow node when initialized then it loads messages entry point`() = runTest { + // GIVEN + val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})) + val fakeMessagesEntryPoint = FakeMessagesEntryPoint() + val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) + val roomFlowNode = createJoinedRoomLoadedFlowNode( + plugins = listOf(inputs), + messagesEntryPoint = fakeMessagesEntryPoint, + ) + // WHEN + val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() + + // THEN + assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages()) + roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(), Lifecycle.State.CREATED) + val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages())!! + assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId) + } + + @Test + fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest { + // GIVEN + val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})) + val fakeMessagesEntryPoint = FakeMessagesEntryPoint() + val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() + val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) + val roomFlowNode = createJoinedRoomLoadedFlowNode( + plugins = listOf(inputs), + messagesEntryPoint = fakeMessagesEntryPoint, + roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, + ) + val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() + // WHEN + fakeMessagesEntryPoint.callback?.onRoomDetailsClick() + // THEN + roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) + val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!! + assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId) + } + + @Test + fun `the ActiveRoomsHolder will be updated with the loaded room on create`() = runTest { + // GIVEN + val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})) + val fakeMessagesEntryPoint = FakeMessagesEntryPoint() + val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() + val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) + val activeRoomsHolder = ActiveRoomsHolder() + val roomFlowNode = createJoinedRoomLoadedFlowNode( + plugins = listOf(inputs), + messagesEntryPoint = fakeMessagesEntryPoint, + roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, + activeRoomsHolder = activeRoomsHolder, + ) + + assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull() + val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() + // WHEN + roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED) + // THEN + assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull() + } + + @Test + fun `the ActiveRoomsHolder will be removed on destroy`() = runTest { + // GIVEN + val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})) + val fakeMessagesEntryPoint = FakeMessagesEntryPoint() + val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() + val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) + val activeRoomsHolder = ActiveRoomsHolder().apply { + addRoom(room) + } + val roomFlowNode = createJoinedRoomLoadedFlowNode( + plugins = listOf(inputs), + messagesEntryPoint = fakeMessagesEntryPoint, + roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, + activeRoomsHolder = activeRoomsHolder, + ) + val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() + roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED) + assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull() + // WHEN + roomFlowNode.updateLifecycleState(Lifecycle.State.DESTROYED) + // THEN + roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.DESTROYED) + assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull() + } +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt new file mode 100644 index 00000000000..2c143df095d --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt @@ -0,0 +1,393 @@ +/* + * 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.appnav + +import io.element.android.appnav.di.SyncOrchestrator +import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.libraries.matrix.api.sync.SyncState +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.sync.FakeSyncService +import io.element.android.services.appnavstate.test.FakeAppForegroundStateService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +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.runTest +import org.junit.Rule +import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class SyncOrchestratorTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `when the sync wasn't running before, an initial sync will take place, even with no network`() = runTest { + val startSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply { + startSyncLambda = startSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + ) + + // We start observing with an initial sync + syncOrchestrator.start() + + // Advance the time just enough to make sure the initial sync has run + advanceTimeBy(1.milliseconds) + startSyncRecorder.assertions().isCalledOnce() + } + + @Test + fun `when the sync wasn't running before, an initial sync will take place`() = runTest { + val startSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply { + startSyncLambda = startSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + ) + + // We start observing with an initial sync + syncOrchestrator.start() + + // Advance the time just enough to make sure the initial sync has run + advanceTimeBy(1.milliseconds) + startSyncRecorder.assertions().isCalledOnce() + + // If we wait for a while, the sync will not be started again by the observer since it's already running + advanceTimeBy(10.seconds) + startSyncRecorder.assertions().isCalledOnce() + } + + @Test + fun `when the app goes to background and the sync was running, it will be stopped after a delay`() = runTest { + val stopSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Running).apply { + stopSyncLambda = stopSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) + val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + appForegroundStateService = appForegroundStateService, + ) + + // We start observing + syncOrchestrator.observeStates() + + // Advance the time to make sure the orchestrator has had time to start processing the inputs + advanceTimeBy(100.milliseconds) + + // Stop sync was never called + stopSyncRecorder.assertions().isNeverCalled() + + // Now we send the app to background + appForegroundStateService.isInForeground.value = false + + // Stop sync will be called after some delay + stopSyncRecorder.assertions().isNeverCalled() + advanceTimeBy(10.seconds) + stopSyncRecorder.assertions().isCalledOnce() + } + + @Test + fun `when the app state changes several times in a short while, stop sync is only called once`() = runTest { + val stopSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Running).apply { + stopSyncLambda = stopSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) + val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + appForegroundStateService = appForegroundStateService, + ) + + // We start observing + syncOrchestrator.observeStates() + + // Advance the time to make sure the orchestrator has had time to start processing the inputs + advanceTimeBy(100.milliseconds) + + // Stop sync was never called + stopSyncRecorder.assertions().isNeverCalled() + + // Now we send the app to background + appForegroundStateService.isInForeground.value = false + + // Ensure the stop action wasn't called yet + stopSyncRecorder.assertions().isNeverCalled() + advanceTimeBy(1.seconds) + appForegroundStateService.isInForeground.value = true + advanceTimeBy(1.seconds) + + // Ensure the stop action wasn't called yet either, since we didn't give it enough time to emit after the expected delay + stopSyncRecorder.assertions().isNeverCalled() + + // Now change it again and wait for enough time + appForegroundStateService.isInForeground.value = false + advanceTimeBy(4.seconds) + + // And confirm it's now called + stopSyncRecorder.assertions().isCalledOnce() + } + + @Test + fun `when the app was in background and we receive a notification, a sync will be started then stopped`() = runTest { + val startSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val stopSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply { + startSyncLambda = startSyncRecorder + stopSyncLambda = stopSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) + val appForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = false, + initialIsSyncingNotificationEventValue = false, + ) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + appForegroundStateService = appForegroundStateService, + ) + + // We start observing + syncOrchestrator.observeStates() + + // Advance the time to make sure the orchestrator has had time to start processing the inputs + advanceTimeBy(100.milliseconds) + + // Start sync was never called + startSyncRecorder.assertions().isNeverCalled() + + // Now we receive a notification and need to sync + appForegroundStateService.updateIsSyncingNotificationEvent(true) + + // Start sync will be called shortly after + advanceTimeBy(1.milliseconds) + startSyncRecorder.assertions().isCalledOnce() + + // If the sync is running and we mark the notification sync as no longer necessary, the sync stops after a delay + syncService.emitSyncState(SyncState.Running) + appForegroundStateService.updateIsSyncingNotificationEvent(false) + + advanceTimeBy(10.seconds) + stopSyncRecorder.assertions().isCalledOnce() + } + + @Test + fun `when the app was in background and we join a call, a sync will be started`() = runTest { + val startSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val stopSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply { + startSyncLambda = startSyncRecorder + stopSyncLambda = stopSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) + val appForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = false, + initialIsSyncingNotificationEventValue = false, + ) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + appForegroundStateService = appForegroundStateService, + ) + + // We start observing + syncOrchestrator.observeStates() + + // Advance the time to make sure the orchestrator has had time to start processing the inputs + advanceTimeBy(100.milliseconds) + + // Start sync was never called + startSyncRecorder.assertions().isNeverCalled() + + // Now we join a call + appForegroundStateService.updateIsInCallState(true) + + // Start sync will be called shortly after + advanceTimeBy(1.milliseconds) + startSyncRecorder.assertions().isCalledOnce() + + // If the sync is running and we mark the in-call state as false, the sync stops after a delay + syncService.emitSyncState(SyncState.Running) + appForegroundStateService.updateIsInCallState(false) + + advanceTimeBy(10.seconds) + stopSyncRecorder.assertions().isCalledOnce() + } + + @Test + fun `when the app was in background and we have an incoming ringing call, a sync will be started`() = runTest { + val startSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val stopSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply { + startSyncLambda = startSyncRecorder + stopSyncLambda = stopSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) + val appForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = false, + initialIsSyncingNotificationEventValue = false, + initialHasRingingCall = false, + ) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + appForegroundStateService = appForegroundStateService, + ) + + // We start observing + syncOrchestrator.observeStates() + + // Advance the time to make sure the orchestrator has had time to start processing the inputs + advanceTimeBy(100.milliseconds) + + // Start sync was never called + startSyncRecorder.assertions().isNeverCalled() + + // Now we receive a ringing call + appForegroundStateService.updateHasRingingCall(true) + + // Start sync will be called shortly after + advanceTimeBy(1.milliseconds) + startSyncRecorder.assertions().isCalledOnce() + + // If the sync is running and the ringing call notification is now over, the sync stops after a delay + syncService.emitSyncState(SyncState.Running) + appForegroundStateService.updateHasRingingCall(false) + + advanceTimeBy(10.seconds) + stopSyncRecorder.assertions().isCalledOnce() + } + + @Test + fun `when the app is in foreground, we sync for a notification and a call is ongoing, the sync will only stop when all conditions are false`() = runTest { + val startSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val stopSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Running).apply { + startSyncLambda = startSyncRecorder + stopSyncLambda = stopSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) + val appForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = true, + initialIsSyncingNotificationEventValue = true, + initialIsInCallValue = true, + ) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + appForegroundStateService = appForegroundStateService, + ) + + // We start observing + syncOrchestrator.observeStates() + + // Advance the time to make sure the orchestrator has had time to start processing the inputs + advanceTimeBy(100.milliseconds) + + // Start sync was never called + startSyncRecorder.assertions().isNeverCalled() + + // We send the app to background, it's still syncing + appForegroundStateService.givenIsInForeground(false) + advanceTimeBy(10.seconds) + stopSyncRecorder.assertions().isNeverCalled() + + // We stop the notification sync, it's still syncing + appForegroundStateService.updateIsSyncingNotificationEvent(false) + advanceTimeBy(10.seconds) + stopSyncRecorder.assertions().isNeverCalled() + + // We set the in-call state to false, now it stops syncing after a delay + appForegroundStateService.updateIsInCallState(false) + advanceTimeBy(10.seconds) + stopSyncRecorder.assertions().isCalledOnce() + } + + @Test + fun `if the sync was running, it's set to be stopped but something triggers a sync again, the sync is not stopped`() = runTest { + val stopSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Running).apply { + stopSyncLambda = stopSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected) + val appForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = true, + initialIsSyncingNotificationEventValue = false, + initialIsInCallValue = false, + ) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + appForegroundStateService = appForegroundStateService, + ) + + // We start observing + syncOrchestrator.observeStates() + + // Advance the time to make sure the orchestrator has had time to start processing the inputs + advanceTimeBy(100.milliseconds) + + // This will set the sync to stop + appForegroundStateService.givenIsInForeground(false) + + // But if we reset it quickly before the stop sync takes place, the sync is not stopped + advanceTimeBy(2.seconds) + appForegroundStateService.givenIsInForeground(true) + + advanceTimeBy(10.seconds) + stopSyncRecorder.assertions().isNeverCalled() + } + + @Test + fun `when network is offline, sync service should not start`() = runTest { + val startSyncRecorder = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply { + startSyncLambda = startSyncRecorder + } + val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected) + val syncOrchestrator = createSyncOrchestrator( + syncService = syncService, + networkMonitor = networkMonitor, + ) + + // We start observing + syncOrchestrator.observeStates() + + // This should still not trigger a sync, since there is no network + advanceTimeBy(10.seconds) + startSyncRecorder.assertions().isNeverCalled() + } + + private fun TestScope.createSyncOrchestrator( + syncService: FakeSyncService = FakeSyncService(), + networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(), + appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(), + ) = SyncOrchestrator( + matrixClient = FakeMatrixClient(syncService = syncService, sessionCoroutineScope = backgroundScope), + networkMonitor = networkMonitor, + appForegroundStateService = appForegroundStateService, + dispatchers = testCoroutineDispatchers(), + ) +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixClientsHolderTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixClientsHolderTest.kt deleted file mode 100644 index 23dfc9c5222..00000000000 --- a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixClientsHolderTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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. - */ - -package io.element.android.appnav.di - -import com.bumble.appyx.core.state.MutableSavedStateMapImpl -import com.google.common.truth.Truth.assertThat -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.auth.FakeMatrixAuthenticationService -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class MatrixClientsHolderTest { - @Test - fun `test getOrNull`() { - val fakeAuthenticationService = FakeMatrixAuthenticationService() - val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() - } - - @Test - fun `test getOrRestore`() = runTest { - val fakeAuthenticationService = FakeMatrixAuthenticationService() - val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) - val fakeMatrixClient = FakeMatrixClient() - fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() - assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) - // Do it again to hit the cache - assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) - } - - @Test - fun `test remove`() = runTest { - val fakeAuthenticationService = FakeMatrixAuthenticationService() - val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) - val fakeMatrixClient = FakeMatrixClient() - fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) - assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) - // Remove - matrixClientsHolder.remove(A_SESSION_ID) - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() - } - - @Test - fun `test remove all`() = runTest { - val fakeAuthenticationService = FakeMatrixAuthenticationService() - val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) - val fakeMatrixClient = FakeMatrixClient() - fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) - assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) - // Remove all - matrixClientsHolder.removeAll() - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() - } - - @Test - fun `test save and restore`() = runTest { - val fakeAuthenticationService = FakeMatrixAuthenticationService() - val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) - val fakeMatrixClient = FakeMatrixClient() - fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) - matrixClientsHolder.getOrRestore(A_SESSION_ID) - val savedStateMap = MutableSavedStateMapImpl { true } - matrixClientsHolder.saveIntoSavedState(savedStateMap) - assertThat(savedStateMap.size).isEqualTo(1) - // Test Restore with non-empty map - matrixClientsHolder.restoreWithSavedState(savedStateMap) - // Empty the map - matrixClientsHolder.removeAll() - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() - // Restore again - matrixClientsHolder.restoreWithSavedState(savedStateMap) - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) - } - - @Test - fun `test AuthenticationService listenToNewMatrixClients emits a Client value and we save it`() = runTest { - val fakeAuthenticationService = FakeMatrixAuthenticationService() - val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() - - fakeAuthenticationService.givenMatrixClient(FakeMatrixClient(sessionId = A_SESSION_ID)) - val loginSucceeded = fakeAuthenticationService.login("user", "pass") - - assertThat(loginSucceeded.isSuccess).isTrue() - assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNotNull() - } -} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt new file mode 100644 index 00000000000..47237d5f779 --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt @@ -0,0 +1,129 @@ +/* + * 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. + */ + +package io.element.android.appnav.di + +import com.bumble.appyx.core.state.MutableSavedStateMapImpl +import com.google.common.truth.Truth.assertThat +import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.libraries.matrix.api.MatrixClient +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.auth.FakeMatrixAuthenticationService +import io.element.android.services.appnavstate.test.FakeAppForegroundStateService +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MatrixSessionCacheTest { + @Test + fun `test getOrNull`() = runTest { + val fakeAuthenticationService = FakeMatrixAuthenticationService() + val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory()) + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + } + + @Test + fun `test getSyncOrchestratorOrNull`() = runTest { + val fakeAuthenticationService = FakeMatrixAuthenticationService() + val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory()) + + // With no matrix client there is no sync orchestrator + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + assertThat(matrixSessionCache.getSyncOrchestrator(A_SESSION_ID)).isNull() + + // But as soon as we receive a client, we can get the sync orchestrator + val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope) + fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) + assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) + assertThat(matrixSessionCache.getSyncOrchestrator(A_SESSION_ID)).isNotNull() + } + + @Test + fun `test getOrRestore`() = runTest { + val fakeAuthenticationService = FakeMatrixAuthenticationService() + val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory()) + val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope) + fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) + // Do it again to hit the cache + assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + } + + @Test + fun `test remove`() = runTest { + val fakeAuthenticationService = FakeMatrixAuthenticationService() + val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory()) + val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope) + fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) + assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + // Remove + matrixSessionCache.remove(A_SESSION_ID) + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + } + + @Test + fun `test remove all`() = runTest { + val fakeAuthenticationService = FakeMatrixAuthenticationService() + val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory()) + val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope) + fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) + assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + // Remove all + matrixSessionCache.removeAll() + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + } + + @Test + fun `test save and restore`() = runTest { + val fakeAuthenticationService = FakeMatrixAuthenticationService() + val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory()) + val fakeMatrixClient = FakeMatrixClient(sessionCoroutineScope = backgroundScope) + fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) + matrixSessionCache.getOrRestore(A_SESSION_ID) + val savedStateMap = MutableSavedStateMapImpl { true } + matrixSessionCache.saveIntoSavedState(savedStateMap) + assertThat(savedStateMap.size).isEqualTo(1) + // Test Restore with non-empty map + matrixSessionCache.restoreWithSavedState(savedStateMap) + // Empty the map + matrixSessionCache.removeAll() + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + // Restore again + matrixSessionCache.restoreWithSavedState(savedStateMap) + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + } + + @Test + fun `test AuthenticationService listenToNewMatrixClients emits a Client value and we save it`() = runTest { + val fakeAuthenticationService = FakeMatrixAuthenticationService() + val matrixSessionCache = MatrixSessionCache(fakeAuthenticationService, createSyncOrchestratorFactory()) + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + + fakeAuthenticationService.givenMatrixClient(FakeMatrixClient(sessionId = A_SESSION_ID, sessionCoroutineScope = backgroundScope)) + val loginSucceeded = fakeAuthenticationService.login("user", "pass") + + assertThat(loginSucceeded.isSuccess).isTrue() + assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNotNull() + } + + private fun TestScope.createSyncOrchestratorFactory() = object : SyncOrchestrator.Factory { + override fun create(matrixClient: MatrixClient): SyncOrchestrator { + return SyncOrchestrator( + matrixClient, + appForegroundStateService = FakeAppForegroundStateService(), + networkMonitor = FakeNetworkMonitor(), + dispatchers = testCoroutineDispatchers(), + ) + } + } +} 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 8640fb03c83..e95eb66cc38 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 @@ -12,6 +12,8 @@ import android.content.Intent import android.net.Uri import androidx.core.net.toUri import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.api.LoginParams +import io.element.android.features.login.test.FakeLoginIntentResolver import io.element.android.libraries.deeplink.DeepLinkCreator import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkParser @@ -22,10 +24,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.impl.DefaultOidcIntentResolver -import io.element.android.libraries.oidc.impl.OidcUrlParser +import io.element.android.libraries.oidc.test.FakeOidcIntentResolver import io.element.android.tests.testutils.lambda.lambdaError -import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -115,56 +115,31 @@ class IntentResolverTest { } @Test - fun `test resolve oidc go back`() { - val sut = createIntentResolver() - val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { - action = Intent.ACTION_VIEW - data = "io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO".toUri() - } - val result = sut.resolve(intent) - assertThat(result).isEqualTo( - ResolvedIntent.Oidc( - oidcAction = OidcAction.GoBack - ) + fun `test resolve oidc`() { + val sut = createIntentResolver( + oidcIntentResolverResult = { OidcAction.GoBack }, ) - } - - @Test - fun `test resolve oidc success`() { - val sut = createIntentResolver() val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW - data = "io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri() + data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri() } val result = sut.resolve(intent) assertThat(result).isEqualTo( ResolvedIntent.Oidc( - oidcAction = OidcAction.Success( - url = "io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" - ) + oidcAction = OidcAction.GoBack ) ) } - @Test - fun `test resolve oidc invalid`() { - val sut = createIntentResolver() - val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { - action = Intent.ACTION_VIEW - data = "io.element:/callback/invalid".toUri() - } - assertThrows(IllegalStateException::class.java) { - sut.resolve(intent) - } - } - @Test fun `test resolve external permalink`() { val permalinkData = PermalinkData.UserLink( userId = UserId("@alice:matrix.org") ) val sut = createIntentResolver( - permalinkParserResult = { permalinkData } + loginIntentResolverResult = { null }, + permalinkParserResult = { permalinkData }, + oidcIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -181,7 +156,9 @@ class IntentResolverTest { @Test fun `test resolve external permalink, FallbackLink should be ignored`() { val sut = createIntentResolver( - permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) } + permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }, + loginIntentResolverResult = { null }, + oidcIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -197,7 +174,8 @@ class IntentResolverTest { userId = UserId("@alice:matrix.org") ) val sut = createIntentResolver( - permalinkParserResult = { permalinkData } + permalinkParserResult = { permalinkData }, + oidcIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_BATTERY_LOW @@ -209,7 +187,9 @@ class IntentResolverTest { @Test fun `test incoming share simple`() { - val sut = createIntentResolver() + val sut = createIntentResolver( + oidcIntentResolverResult = { null }, + ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_SEND } @@ -219,7 +199,9 @@ class IntentResolverTest { @Test fun `test incoming share multiple`() { - val sut = createIntentResolver() + val sut = createIntentResolver( + oidcIntentResolverResult = { null }, + ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_SEND_MULTIPLE } @@ -230,7 +212,9 @@ class IntentResolverTest { @Test fun `test resolve invalid`() { val sut = createIntentResolver( - permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) } + permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }, + loginIntentResolverResult = { null }, + oidcIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -240,13 +224,33 @@ class IntentResolverTest { assertThat(result).isNull() } + @Test + fun `test resolve login param`() { + val aLoginParams = LoginParams("accountProvider", null) + val sut = createIntentResolver( + loginIntentResolverResult = { aLoginParams }, + oidcIntentResolverResult = { null }, + ) + val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { + action = Intent.ACTION_VIEW + data = "".toUri() + } + val result = sut.resolve(intent) + assertThat(result).isEqualTo(ResolvedIntent.Login(aLoginParams)) + } + private fun createIntentResolver( - permalinkParserResult: () -> PermalinkData = { lambdaError() } + permalinkParserResult: (String) -> PermalinkData = { lambdaError() }, + loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() }, + oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() }, ): IntentResolver { return IntentResolver( deeplinkParser = DeeplinkParser(), - oidcIntentResolver = DefaultOidcIntentResolver( - oidcUrlParser = OidcUrlParser() + loginIntentResolver = FakeLoginIntentResolver( + parseResult = loginIntentResolverResult, + ), + 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 3f0713cd993..d86c46f1598 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 @@ -5,19 +5,20 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.appnav.loggedin -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.UserProperties +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId 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.SyncState @@ -26,12 +27,11 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService -import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.test.FakePushService import io.element.android.libraries.pushproviders.api.Distributor @@ -45,9 +45,8 @@ import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -59,10 +58,7 @@ class LoggedInPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = createLoggedInPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createLoggedInPresenter().test { val initialState = awaitItem() assertThat(initialState.showSyncSpinner).isFalse() assertThat(initialState.pusherRegistrationState.isUninitialized()).isTrue() @@ -70,13 +66,32 @@ class LoggedInPresenterTest { } } + @Test + fun `present - ensure that account urls are preloaded`() = runTest { + val accountManagementUrlResult = lambdaRecorder> { Result.success("aUrl") } + val matrixClient = FakeMatrixClient( + accountManagementUrlResult = accountManagementUrlResult, + ) + createLoggedInPresenter( + matrixClient = matrixClient, + ).test { + awaitItem() + advanceUntilIdle() + accountManagementUrlResult.assertions().isCalledExactly(2) + .withSequence( + listOf(value(AccountManagementAction.Profile)), + listOf(value(AccountManagementAction.SessionsList)), + ) + } + } + @Test fun `present - show sync spinner`() = runTest { val roomListService = FakeRoomListService() - val presenter = createLoggedInPresenter(roomListService, SyncState.Running) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createLoggedInPresenter( + syncState = SyncState.Running, + matrixClient = FakeMatrixClient(roomListService = roomListService), + ).test { val initialState = awaitItem() assertThat(initialState.showSyncSpinner).isFalse() roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show) @@ -92,18 +107,19 @@ class LoggedInPresenterTest { val roomListService = FakeRoomListService() val verificationService = FakeSessionVerificationService() val encryptionService = FakeEncryptionService() - val presenter = LoggedInPresenter( - matrixClient = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService), + val buildMeta = aBuildMeta() + LoggedInPresenter( + matrixClient = FakeMatrixClient( + roomListService = roomListService, + encryptionService = encryptionService, + ), syncService = FakeSyncService(initialSyncState = SyncState.Running), pushService = FakePushService(), sessionVerificationService = verificationService, analyticsService = analyticsService, encryptionService = encryptionService, - enableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(InMemoryAppPreferencesStore(), this), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + buildMeta = buildMeta, + ).test { encryptionService.emitRecoveryState(RecoveryState.UNKNOWN) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) verificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) @@ -129,13 +145,10 @@ class LoggedInPresenterTest { val verificationService = FakeSessionVerificationService( initialSessionVerifiedStatus = SessionVerifiedStatus.NotVerified ) - val presenter = createLoggedInPresenter( + createLoggedInPresenter( pushService = pushService, sessionVerificationService = verificationService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + ).test { val finalState = awaitFirstItem() assertThat(finalState.pusherRegistrationState.errorOrNull()) .isInstanceOf(PusherRegistrationFailure.AccountNotVerified::class.java) @@ -155,13 +168,13 @@ class LoggedInPresenterTest { val pushService = createFakePushService( registerWithLambda = lambda, ) - val presenter = createLoggedInPresenter( + createLoggedInPresenter( pushService = pushService, sessionVerificationService = sessionVerificationService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + matrixClient = FakeMatrixClient( + accountManagementUrlResult = { Result.success(null) }, + ), + ).test { val finalState = awaitFirstItem() assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue() lambda.assertions() @@ -188,13 +201,13 @@ class LoggedInPresenterTest { val pushService = createFakePushService( registerWithLambda = lambda, ) - val presenter = createLoggedInPresenter( + createLoggedInPresenter( pushService = pushService, sessionVerificationService = sessionVerificationService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + matrixClient = FakeMatrixClient( + accountManagementUrlResult = { Result.success(null) }, + ), + ).test { val finalState = awaitFirstItem() assertThat(finalState.pusherRegistrationState.isFailure()).isTrue() lambda.assertions() @@ -233,13 +246,13 @@ class LoggedInPresenterTest { currentPushProvider = { pushProvider }, registerWithLambda = lambda, ) - val presenter = createLoggedInPresenter( + createLoggedInPresenter( pushService = pushService, sessionVerificationService = sessionVerificationService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + matrixClient = FakeMatrixClient( + accountManagementUrlResult = { Result.success(null) }, + ), + ).test { val finalState = awaitFirstItem() assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue() lambda.assertions() @@ -277,13 +290,13 @@ class LoggedInPresenterTest { currentPushProvider = { pushProvider }, registerWithLambda = lambda, ) - val presenter = createLoggedInPresenter( + createLoggedInPresenter( pushService = pushService, sessionVerificationService = sessionVerificationService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + matrixClient = FakeMatrixClient( + accountManagementUrlResult = { Result.success(null) }, + ), + ).test { val finalState = awaitFirstItem() assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue() lambda.assertions() @@ -317,13 +330,10 @@ class LoggedInPresenterTest { currentPushProvider = { pushProvider }, registerWithLambda = lambda, ) - val presenter = createLoggedInPresenter( + createLoggedInPresenter( pushService = pushService, sessionVerificationService = sessionVerificationService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + ).test { val finalState = awaitFirstItem() assertThat(finalState.pusherRegistrationState.errorOrNull()) .isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java) @@ -345,13 +355,10 @@ class LoggedInPresenterTest { registerWithLambda = lambda, setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda, ) - val presenter = createLoggedInPresenter( + createLoggedInPresenter( pushService = pushService, sessionVerificationService = sessionVerificationService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + ).test { val finalState = awaitFirstItem() assertThat(finalState.pusherRegistrationState.errorOrNull()) .isInstanceOf(PusherRegistrationFailure.NoProvidersAvailable::class.java) @@ -394,13 +401,10 @@ class LoggedInPresenterTest { registerWithLambda = lambda, selectPushProviderLambda = selectPushProviderLambda, ) - val presenter = createLoggedInPresenter( + createLoggedInPresenter( pushService = pushService, sessionVerificationService = sessionVerificationService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + ).test { val finalState = awaitFirstItem() assertThat(finalState.pusherRegistrationState.errorOrNull()) .isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java) @@ -445,13 +449,13 @@ class LoggedInPresenterTest { pushProvider1 = pushProvider1, registerWithLambda = lambda, ) - val presenter = createLoggedInPresenter( + createLoggedInPresenter( pushService = pushService, sessionVerificationService = sessionVerificationService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + matrixClient = FakeMatrixClient( + accountManagementUrlResult = { Result.success(null) }, + ), + ).test { val finalState = awaitFirstItem() assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue() lambda.assertions().isCalledOnce() @@ -505,10 +509,9 @@ class LoggedInPresenterTest { currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) }, availableSlidingSyncVersionsLambda = { Result.success(listOf(SlidingSyncVersion.Native)) }, ) - val presenter = createLoggedInPresenter(matrixClient = matrixClient) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createLoggedInPresenter( + matrixClient = matrixClient, + ).test { val initialState = awaitItem() assertThat(initialState.forceNativeSlidingSyncMigration).isFalse() @@ -518,51 +521,27 @@ class LoggedInPresenterTest { } } - @Test - fun `present - CheckSlidingSyncProxyAvailability will not force the migration if native sliding sync is not supported too`() = runTest { - val matrixClient = FakeMatrixClient( - currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) }, - availableSlidingSyncVersionsLambda = { Result.success(emptyList()) }, - ) - val presenter = createLoggedInPresenter(matrixClient = matrixClient) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.forceNativeSlidingSyncMigration).isFalse() - - initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability) - - expectNoEvents() - } - } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `present - LogoutAndMigrateToNativeSlidingSync enables native sliding sync and logs out the user`() = runTest { - val logoutLambda = lambdaRecorder { userInitiated, ignoreSdkError -> + fun `present - LogoutAndMigrateToNativeSlidingSync logs out the user`() = runTest { + val logoutLambda = lambdaRecorder { userInitiated, ignoreSdkError -> assertThat(userInitiated).isTrue() assertThat(ignoreSdkError).isTrue() - null } - val matrixClient = FakeMatrixClient().apply { + val matrixClient = FakeMatrixClient( + accountManagementUrlResult = { Result.success(null) }, + ).apply { this.logoutLambda = logoutLambda } - val appPreferencesStore = InMemoryAppPreferencesStore() - val enableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(appPreferencesStore, this) - val presenter = createLoggedInPresenter(matrixClient = matrixClient, enableNativeSlidingSyncUseCase = enableNativeSlidingSyncUseCase) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createLoggedInPresenter( + matrixClient = matrixClient, + ).test { val initialState = awaitItem() - assertThat(appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse() - initialState.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync) advanceUntilIdle() - assertThat(appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue() assertThat(logoutLambda.assertions().isCalledOnce()) } } @@ -572,15 +551,16 @@ class LoggedInPresenterTest { return awaitItem() } - private fun TestScope.createLoggedInPresenter( - roomListService: RoomListService = FakeRoomListService(), + private fun createLoggedInPresenter( syncState: SyncState = SyncState.Running, analyticsService: AnalyticsService = FakeAnalyticsService(), sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), encryptionService: EncryptionService = FakeEncryptionService(), pushService: PushService = FakePushService(), - enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(InMemoryAppPreferencesStore(), this), - matrixClient: MatrixClient = FakeMatrixClient(roomListService = roomListService), + matrixClient: MatrixClient = FakeMatrixClient( + accountManagementUrlResult = { Result.success(null) }, + ), + buildMeta: BuildMeta = aBuildMeta(), ): LoggedInPresenter { return LoggedInPresenter( matrixClient = matrixClient, @@ -589,7 +569,7 @@ class LoggedInPresenterTest { sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, encryptionService = encryptionService, - enableNativeSlidingSyncUseCase = enableNativeSlidingSyncUseCase, + buildMeta = buildMeta, ) } } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt new file mode 100644 index 00000000000..459abc64d4d --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt @@ -0,0 +1,162 @@ +/* + * 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. + */ + +@file:Suppress("DEPRECATION") + +package io.element.android.appnav.loggedin + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MediaPreviewConfigMigrationTest { + @Test + fun `when no local data exists, migration does nothing`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore() + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(null) } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify no calls were made to set server config + // since there's nothing to migrate + } + + @Test + fun `when local data exists and server has config, clears local data`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setHideInviteAvatars(true) + setTimelineMediaPreviewValue(MediaPreviewValue.Private) + } + val serverConfig = MediaPreviewConfig( + hideInviteAvatar = false, + mediaPreviewValue = MediaPreviewValue.On + ) + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(serverConfig) } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify local data was cleared + assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isNull() + assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isNull() + } + + @Test + fun `when local hideInviteAvatars exists and server has no config, migrates to server`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setHideInviteAvatars(true) + } + var setHideInviteAvatarsValue: Boolean? = null + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(null) }, + setHideInviteAvatarsResult = { value -> + setHideInviteAvatarsValue = value + Result.success(Unit) + } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify server was updated with local value + assertThat(setHideInviteAvatarsValue).isTrue() + // Verify local data was cleared + assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isNull() + } + + @Test + fun `when local mediaPreviewValue exists and server has no config, migrates to server`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setTimelineMediaPreviewValue(MediaPreviewValue.Private) + } + var setMediaPreviewValue: MediaPreviewValue? = null + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(null) }, + setMediaPreviewValueResult = { value -> + setMediaPreviewValue = value + Result.success(Unit) + } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify server was updated with local value + assertThat(setMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) + // Verify local data was cleared + assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isNull() + } + + @Test + fun `when both local values exist and server has no config, migrates both to server`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setHideInviteAvatars(true) + setTimelineMediaPreviewValue(MediaPreviewValue.Off) + } + var setHideInviteAvatarsValue: Boolean? = null + var setMediaPreviewValue: MediaPreviewValue? = null + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(null) }, + setHideInviteAvatarsResult = { value -> + setHideInviteAvatarsValue = value + Result.success(Unit) + }, + setMediaPreviewValueResult = { value -> + setMediaPreviewValue = value + Result.success(Unit) + } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify server was updated with both local values + assertThat(setHideInviteAvatarsValue).isTrue() + assertThat(setMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + // Verify local data was cleared + assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isNull() + assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isNull() + } + + @Test + fun `when fetch config fails, migration does nothing`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setHideInviteAvatars(true) + setTimelineMediaPreviewValue(MediaPreviewValue.Private) + } + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.failure(Exception("Network error")) } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify local data was not cleared since migration failed + assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isTrue() + assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isEqualTo(MediaPreviewValue.Private) + } + + private fun TestScope.createMigration( + appPreferencesStore: InMemoryAppPreferencesStore, + mediaPreviewService: FakeMediaPreviewService + ) = MediaPreviewConfigMigration( + mediaPreviewService = mediaPreviewService, + appPreferencesStore = appPreferencesStore, + sessionCoroutineScope = this + ) +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendQueuesTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendQueuesTest.kt index 597139c5e97..c117dfb8f76 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendQueuesTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendQueuesTest.kt @@ -10,7 +10,7 @@ package io.element.android.appnav.loggedin import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -35,8 +35,8 @@ class SendQueuesTest { matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> } - val room = FakeMatrixRoom( - setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda + val room = FakeJoinedRoom( + setSendQueueEnabledResult = setRoomSendQueueEnabledLambda ) matrixClient.givenGetRoomResult(room.roomId, room) sut.launchIn(backgroundScope) @@ -61,8 +61,8 @@ class SendQueuesTest { matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda syncService.emitSyncState(SyncState.Offline) val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> } - val room = FakeMatrixRoom( - setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda + val room = FakeJoinedRoom( + setSendQueueEnabledResult = setRoomSendQueueEnabledLambda ) matrixClient.givenGetRoomResult(room.roomId, room) diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt new file mode 100644 index 00000000000..91e0b9f9681 --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt @@ -0,0 +1,69 @@ +/* + * 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. + */ + +package io.element.android.appnav.room + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.roomlist.RoomList +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.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.libraries.matrix.ui.room.LoadingRoomState +import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LoadingBaseRoomStateFlowFactoryTest { + @Test + fun `flow should emit Loading and then Loaded when there is a room in cache`() = runTest { + val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID)) + val matrixClient = FakeMatrixClient(A_SESSION_ID).apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val flowFactory = LoadingRoomStateFlowFactory(matrixClient) + flowFactory + .create(this, A_ROOM_ID) + .test { + assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) + assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room)) + } + } + + @Test + fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest { + val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID)) + val roomListService = FakeRoomListService() + val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) + val flowFactory = LoadingRoomStateFlowFactory(matrixClient) + flowFactory + .create(this, A_ROOM_ID) + .test { + assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) + matrixClient.givenGetRoomResult(A_ROOM_ID, room) + roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) + assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room)) + } + } + + @Test + fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest { + val roomListService = FakeRoomListService() + val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) + val flowFactory = LoadingRoomStateFlowFactory(matrixClient) + flowFactory + .create(this, A_ROOM_ID) + .test { + assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) + roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) + assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error) + } + } +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt deleted file mode 100644 index c94cbd229a1..00000000000 --- a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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. - */ - -package io.element.android.appnav.room - -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import io.element.android.appnav.room.joined.LoadingRoomState -import io.element.android.appnav.room.joined.LoadingRoomStateFlowFactory -import io.element.android.libraries.matrix.api.roomlist.RoomList -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.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class LoadingRoomStateFlowFactoryTest { - @Test - fun `flow should emit Loading and then Loaded when there is a room in cache`() = runTest { - val room = FakeMatrixRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID) - val matrixClient = FakeMatrixClient(A_SESSION_ID).apply { - givenGetRoomResult(A_ROOM_ID, room) - } - val flowFactory = LoadingRoomStateFlowFactory(matrixClient) - flowFactory - .create(this, A_ROOM_ID) - .test { - assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) - assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room)) - } - } - - @Test - fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest { - val room = FakeMatrixRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID) - val roomListService = FakeRoomListService() - val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) - val flowFactory = LoadingRoomStateFlowFactory(matrixClient) - flowFactory - .create(this, A_ROOM_ID) - .test { - assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) - matrixClient.givenGetRoomResult(A_ROOM_ID, room) - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room)) - } - } - - @Test - fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest { - val roomListService = FakeRoomListService() - val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) - val flowFactory = LoadingRoomStateFlowFactory(matrixClient) - flowFactory - .create(this, A_ROOM_ID) - .test { - assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) - roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error) - } - } -} diff --git a/build.gradle.kts b/build.gradle.kts index afdbc03ac5d..0e585a3d6bc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,7 @@ allprojects { } dependencies { detektPlugins("io.nlopez.compose.rules:detekt:0.4.22") + detektPlugins(project(":tests:detekt-rules")) } tasks.withType().configureEach { @@ -97,6 +98,10 @@ allprojects { // Uncomment to suppress Compose Kotlin compiler compatibility warning // freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true")) + + // Fix compilation warning for annotations + // See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details + freeCompilerArgs.add("-Xannotation-default-target=first-only") } } } @@ -166,14 +171,17 @@ allprojects { // Register quality check tasks. tasks.register("runQualityChecks") { dependsOn(":tests:konsist:testDebugUnitTest") + dependsOn(":app:lintGplayDebug") project.subprojects { - // For some reason `findByName("lint")` doesn't work - tasks.findByPath("$path:lint")?.let { dependsOn(it) } + tasks.findByPath("$path:lintDebug")?.let { dependsOn(it) } tasks.findByName("detekt")?.let { dependsOn(it) } tasks.findByName("ktlintCheck")?.let { dependsOn(it) } // tasks.findByName("buildHealth")?.let { dependsOn(it) } } dependsOn(":app:knitCheck") + + // Make sure all checks run even if some fail + gradle.startParameter.isContinueOnFailure = true } // Make sure to delete old screenshots before recording new ones diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 738bd124303..b8141daef82 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -10,7 +10,7 @@ * [Sync](#sync) * [Rust SDK](#rust-sdk) * [Matrix Rust Component Kotlin](#matrix-rust-component-kotlin) - * [Build the SDK locally](#build-the-sdk-locally) + * [Building the SDK locally](#building-the-sdk-locally) * [The Android project](#the-android-project) * [Application](#application) * [Jetpack Compose](#jetpack-compose) @@ -107,7 +107,7 @@ This is the goal of https://github.com/matrix-org/matrix-rust-components-kotlin. This repository is used for distributing kotlin releases of the Matrix Rust SDK. It'll provide the corresponding aar and also publish them on maven. -Most of the time you want to use the releases made on maven with gradle: +Most of the time **you want to use the releases made on maven with gradle**: ```groovy implementation("org.matrix.rustcomponents:sdk-android:latest-version") @@ -115,7 +115,9 @@ implementation("org.matrix.rustcomponents:sdk-android:latest-version") You can also have access to the aars through the [release](https://github.com/matrix-org/matrix-rust-components-kotlin/releases) page. -#### Build the SDK locally +#### Building the SDK locally + +If you want to make changes to the SDK or test them before integrating it with your codebase, you can build the SDK locally too. Prerequisites: * Install the Android NDK (Native Development Kit). To do this from within @@ -273,7 +275,7 @@ Follow these steps to install and configure the plugin and templates: 1. Install the AS plugin for generating modules : [Generate Module from Template](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template) -2. Run the script `tools/templates/generate_templates.sh` to generate the template zip file +2. From repository root, run `./tools/templates/generate_templates.sh` to generate the template zip file 3. Import file templates in AS : - Navigate to File/Manage IDE Settings/Import Settings - Pick the `tmp/file_templates.zip` files diff --git a/docs/oidc.md b/docs/oidc.md index 94b22660c01..23709b608c5 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -11,7 +11,7 @@ Server list: https://github.com/element-hq/oidc-playground Metadata iOS: (from https://github.com/element-hq/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28) clientName: InfoPlistReader.main.bundleDisplayName, -redirectUri: "io.element:/callback", +redirectUri: "io.element.android:/", clientUri: "https://element.io", tosUri: "https://element.io/user-terms-of-service", policyUri: "https://element.io/privacy" @@ -19,7 +19,7 @@ policyUri: "https://element.io/privacy" Android: clientName = "Element", -redirectUri = "io.element:/callback", +redirectUri = "io.element.android:/", clientUri = "https://element.io", tosUri = "https://element.io/user-terms-of-service", policyUri = "https://element.io/privacy" diff --git a/enterprise b/enterprise index b4f0427e359..b7ababb9537 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit b4f0427e3595049d39846aabcdc06e818f2e96ea +Subproject commit b7ababb9537da8bec254b8ed00b5a4122e9f3e3b diff --git a/fastlane/metadata/android/en-US/changelogs/202503000.txt b/fastlane/metadata/android/en-US/changelogs/202503000.txt new file mode 100644 index 00000000000..9f4d0720523 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202503000.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202503010.txt b/fastlane/metadata/android/en-US/changelogs/202503010.txt new file mode 100644 index 00000000000..8e44d1e72fb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202503010.txt @@ -0,0 +1,2 @@ +Main changes in this version: Event cache / Join room by address. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202503020.txt b/fastlane/metadata/android/en-US/changelogs/202503020.txt new file mode 100644 index 00000000000..8955ade6804 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202503020.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202503030.txt b/fastlane/metadata/android/en-US/changelogs/202503030.txt new file mode 100644 index 00000000000..f7a79e068bf --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202503030.txt @@ -0,0 +1,2 @@ +Main changes in this version: improvements to the event cache, Element Call now uses an embedded implementation, several bugfixes. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202503040.txt b/fastlane/metadata/android/en-US/changelogs/202503040.txt new file mode 100644 index 00000000000..21f4248fbc7 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202503040.txt @@ -0,0 +1,2 @@ +Main changes in this version: added a fix for Element Call not being able to report issues. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202504000.txt b/fastlane/metadata/android/en-US/changelogs/202504000.txt new file mode 100644 index 00000000000..8955ade6804 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202504000.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202504010.txt b/fastlane/metadata/android/en-US/changelogs/202504010.txt new file mode 100644 index 00000000000..8955ade6804 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202504010.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202504020.txt b/fastlane/metadata/android/en-US/changelogs/202504020.txt new file mode 100644 index 00000000000..66578477e68 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202504020.txt @@ -0,0 +1,2 @@ +Main changes in this version: security fix. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202504030.txt b/fastlane/metadata/android/en-US/changelogs/202504030.txt new file mode 100644 index 00000000000..a4b397f1bb8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202504030.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/202505000.txt b/fastlane/metadata/android/en-US/changelogs/202505000.txt new file mode 100644 index 00000000000..8955ade6804 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202505000.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202505010.txt b/fastlane/metadata/android/en-US/changelogs/202505010.txt new file mode 100644 index 00000000000..4712e9f21e1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202505010.txt @@ -0,0 +1,2 @@ +Main changes in this version: fix Element Call not working. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202505030.txt b/fastlane/metadata/android/en-US/changelogs/202505030.txt new file mode 100644 index 00000000000..f9194d12665 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202505030.txt @@ -0,0 +1,2 @@ +Main changes in this version: fix issue with Element Call and to-device events. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202505040.txt b/fastlane/metadata/android/en-US/changelogs/202505040.txt new file mode 100644 index 00000000000..8955ade6804 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202505040.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202506000.txt b/fastlane/metadata/android/en-US/changelogs/202506000.txt new file mode 100644 index 00000000000..a4b397f1bb8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202506000.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/202506010.txt b/fastlane/metadata/android/en-US/changelogs/202506010.txt new file mode 100644 index 00000000000..a51bde477f4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202506010.txt @@ -0,0 +1,2 @@ +Main changes in this version: fix audio devices and volume selection in Element Call, improves moderation features. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202506020.txt b/fastlane/metadata/android/en-US/changelogs/202506020.txt new file mode 100644 index 00000000000..8955ade6804 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202506020.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202506030.txt b/fastlane/metadata/android/en-US/changelogs/202506030.txt new file mode 100644 index 00000000000..f52df1a3721 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202506030.txt @@ -0,0 +1,2 @@ +Main changes in this version: add support for tombstoned rooms, improve notification reliability. +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/202507000.txt b/fastlane/metadata/android/en-US/changelogs/202507000.txt new file mode 100644 index 00000000000..3abd90d7c68 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202507000.txt @@ -0,0 +1,2 @@ +Main changes in this version: improve accessibility. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index f3708552d3f..d0b94798861 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,38 +1,28 @@ -Element X brings you both sovereign & seamless collaboration built on Matrix. +Freedom to communicate on your own terms -The collaboration capabilities include chat & video calls with the modern set of features such as: - • public & private channels - • room moderation & access control - • replies, reactions, polls, read receipts, pinned messages, etc. - • simultaneous chat & calls (picture in picture) - • decentralized & federated communication across organizations +For individuals and communities - private communication between family, friends, hobby groups, clubs, etc. -All this comes in a secure & sovereign fashion without compromising responsiveness or overall usability of the app: - • enterprise-grade single sign-on - • easy & secure login & device verification via QR-code - • end to end encryption & zero trust - • protection against MITM & other cyber attacks +Element X gives you fast, secure and private instant messaging and video calls built on Matrix, the open standard for real-time communication. This is a free and open-source app maintained at https://github.com/element-hq/element-x-android. -If you’re a new user, use the new Element X app from the start. Compared to the current Element app you will get: - • greatly enhanced performance, sleek user interface and overall better user experience - • enterprise-grade support for single sign-on (OIDC) - • QR-code based login & device verification - • natively integrated Element Call for video calls - • continuous improvements, bug fixes and new features +Stay in touch with friends, family and communities with: + • Real time messaging & video calls + • Public rooms for open group communication + • Private rooms for closed group communication + • Rich messaging features: emoji reactions, replies, polls, pinned messages and more. + • Video calling while browsing messages. + • Interoperability with other Matrix-based apps such as FluffyChat, Cinny and many more. -If you’re an existing user, using the current Element app - check out the new Element X and start planning your transition. The current Element app will be phased out and will only get critical security updates. +Privacy-first +Unlike some other messengers from Big Tech companies, we don’t mine your data or monitor your communications. -Own your data -Matrix-based, Element X lets you self-host your data or choose from any free public server (the default is matrix.org, but there are plenty of others to choose from). However you host, you have ownership; it’s your data. You’re not the product. You’re in control. +Own your conversations +Choose where to host your data - from any public server (the largest free server is matrix.org, but there are plenty of others to choose from) to creating your own personal server and hosting it on your own domain. This ability to choose a server is a large part of what differentiates us from other real time communication apps. However you host, you have ownership; it’s your data. You’re not the product. You’re in control. -Interoperate natively -Enjoy the freedom of the Matrix open standard! You have native interoperability with any other Matrix-based app. So just like email, it doesn't matter if your friends, partners or customers are on a different Matrix-based app - you can still connect. +Communicate in real time, all the time +Use Element everywhere. Stay in touch wherever you are with fully synchronised message history across all your devices, including on the web at https://app.element.io -Encrypt your data -Enjoy your right to private conversations - free from data mining, ads and all the rest of it - and stay secure. Only the people in your conversation can read your messages. - -Chat across multiple devices -Stay in touch wherever you are with fully synchronized message history across all your devices, even those running Element legacy app, and on the web at https://app.element.io +Element X is our next-generation app +If you’re using the original Element app, it’s time to try Element X! It’s faster, easier to use, and more powerful than the original app. It’s better in every way and we’re adding new features all the time. The application requires the android.permission.REQUEST_INSTALL_PACKAGES permission to enable the installation of applications received as attachments, ensuring seamless and convenient access to new software within the app. diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index a3107af4b1d..325bf570f53 100644 Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt index 8e62bdd3054..647b2057227 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt @@ -13,12 +13,17 @@ open class AnalyticsPreferencesStateProvider : PreviewParameterProvider get() = sequenceOf( aAnalyticsPreferencesState().copy(isEnabled = true), + aAnalyticsPreferencesState().copy(isEnabled = true, policyUrl = ""), ) } -fun aAnalyticsPreferencesState() = AnalyticsPreferencesState( - applicationName = "Element X", - isEnabled = false, - policyUrl = "https://element.io", +fun aAnalyticsPreferencesState( + applicationName: String = "Element X", + isEnabled: Boolean = false, + policyUrl: String = "https://element.io", +) = AnalyticsPreferencesState( + applicationName = applicationName, + isEnabled = isEnabled, + policyUrl = policyUrl, eventSink = {} ) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 2e7ee47736b..70461f22d31 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -36,11 +36,6 @@ fun AnalyticsPreferencesView( id = R.string.screen_analytics_settings_help_us_improve, state.applicationName ) - val linkText = buildAnnotatedStringWithStyledPart( - R.string.screen_analytics_settings_read_terms, - R.string.screen_analytics_settings_read_terms_content_link, - tagAndLink = LINK_TAG to state.policyUrl, - ) Column(modifier) { ListItem( headlineContent = { @@ -57,7 +52,14 @@ fun AnalyticsPreferencesView( onEnabledChanged(!state.isEnabled) } ) - ListSupportingText(annotatedString = linkText) + if (state.policyUrl.isNotEmpty()) { + val linkText = buildAnnotatedStringWithStyledPart( + R.string.screen_analytics_settings_read_terms, + R.string.screen_analytics_settings_read_terms_content_link, + tagAndLink = LINK_TAG to state.policyUrl, + ) + ListSupportingText(annotatedString = linkText) + } } } diff --git a/features/analytics/api/src/main/res/values-cy/translations.xml b/features/analytics/api/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..308d8a07f10 --- /dev/null +++ b/features/analytics/api/src/main/res/values-cy/translations.xml @@ -0,0 +1,7 @@ + + + "Rhannu data defnydd dienw i\'n helpu i nodi problemau." + "Gallwch ddarllen ein holl amodau %1$s." + "yma" + "Rhannu data dadansoddeg" + diff --git a/features/analytics/api/src/main/res/values-da/translations.xml b/features/analytics/api/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..e302bae6267 --- /dev/null +++ b/features/analytics/api/src/main/res/values-da/translations.xml @@ -0,0 +1,7 @@ + + + "Del anonyme brugsdata for at hjælpe os med at identificere problemer." + "Du kan læse alle vores vilkår %1$s." + "her" + "Del analysedata" + diff --git a/features/analytics/api/src/main/res/values-de/translations.xml b/features/analytics/api/src/main/res/values-de/translations.xml index ec26ff08fce..30fd8cd7e5d 100644 --- a/features/analytics/api/src/main/res/values-de/translations.xml +++ b/features/analytics/api/src/main/res/values-de/translations.xml @@ -1,7 +1,7 @@ "Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen." - "Weitere Informationen findest du %1$s ." + "Sie können unsere Bedingungen %1$s lesen." "hier" "Analysedaten teilen" diff --git a/features/analytics/api/src/main/res/values-eu/translations.xml b/features/analytics/api/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..52e18f32ef8 --- /dev/null +++ b/features/analytics/api/src/main/res/values-eu/translations.xml @@ -0,0 +1,7 @@ + + + "Partekatu erabilerari buruzko datu anonimoak arazoak identifikatzen laguntzeko." + "Gure baldintza guztiak irakur ditzakezu %1$s." + "hemen" + "Partekatu analisi-datuak" + diff --git a/features/analytics/api/src/main/res/values-fa/translations.xml b/features/analytics/api/src/main/res/values-fa/translations.xml index 8b672f0f00f..62a0e1cc902 100644 --- a/features/analytics/api/src/main/res/values-fa/translations.xml +++ b/features/analytics/api/src/main/res/values-fa/translations.xml @@ -1,5 +1,7 @@ + "داده های استفاده ناشناس را به اشتراک بگذارید تا به ما در شناسایی مشکلات کمک کند." + "شما می‌توانید تمام شرایط ما را بخوانید%1$s ." "این‌جا" "هم رسانی داده‌های تحلیلی" diff --git a/features/analytics/api/src/main/res/values-lt/translations.xml b/features/analytics/api/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..08dd1553320 --- /dev/null +++ b/features/analytics/api/src/main/res/values-lt/translations.xml @@ -0,0 +1,7 @@ + + + "Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas." + "Galite perskaityti visas mūsų sąlygas %1$s." + "čia" + "Dalytis analitiniais duomenimis" + diff --git a/features/analytics/api/src/main/res/values-pt-rBR/translations.xml b/features/analytics/api/src/main/res/values-pt-rBR/translations.xml index 4e7dc9775f9..ab649cc9fff 100644 --- a/features/analytics/api/src/main/res/values-pt-rBR/translations.xml +++ b/features/analytics/api/src/main/res/values-pt-rBR/translations.xml @@ -1,7 +1,7 @@ "Compartilhe dados de uso anônimos para nos ajudar a identificar problemas." - "Você pode ler todos os nossos termos %1$s ." + "Você pode ler todos os nossos termos %1$s." "aqui" "Compartilhar dados de utilização" diff --git a/features/analytics/api/src/main/res/values-tr/translations.xml b/features/analytics/api/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..7229872f5ac --- /dev/null +++ b/features/analytics/api/src/main/res/values-tr/translations.xml @@ -0,0 +1,7 @@ + + + "Sorunları tanımlamamıza yardımcı olmak için anonim kullanım verilerini paylaşın." + "Tüm şartlarımızı okuyabilirsiniz %1$s." + "burada" + "Analitik verileri paylaşın" + diff --git a/features/analytics/api/src/main/res/values-ur/translations.xml b/features/analytics/api/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..237bb30d9e7 --- /dev/null +++ b/features/analytics/api/src/main/res/values-ur/translations.xml @@ -0,0 +1,7 @@ + + + "مسائل کی نشاندہی کرنے میں ہماری مدد کے لیے گمنام استعمال کے بیانات کا اشتراک کریں۔" + "آپ ہماری تمام شرائط پڑھ سکتے ہیں %1$s۔" + "یہاں" + "تجزیاتی بیانات کا اشتراک کریں" + diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt index 01ec23da385..ba1073d480c 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt @@ -8,9 +8,9 @@ package io.element.android.features.analytics.impl import android.app.Activity +import androidx.activity.compose.LocalActivity 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 @@ -34,7 +34,7 @@ class AnalyticsOptInNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val activity = LocalContext.current as Activity + val activity = requireNotNull(LocalActivity.current) val isDark = ElementTheme.isLightTheme.not() val state = presenter.present() AnalyticsOptInView( diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt index d545d009518..39b99a9257c 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt @@ -9,6 +9,7 @@ package io.element.android.features.analytics.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.appconfig.AnalyticsConfig import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta @@ -36,6 +37,7 @@ class AnalyticsOptInPresenter @Inject constructor( return AnalyticsOptInState( applicationName = buildMeta.applicationName, + hasPolicyLink = AnalyticsConfig.POLICY_LINK.isNotEmpty(), eventSink = ::handleEvents ) } diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt index d7e99e56c1c..a0913bdb4f5 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt @@ -11,5 +11,6 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents data class AnalyticsOptInState( val applicationName: String, + val hasPolicyLink: Boolean, val eventSink: (AnalyticsOptInEvents) -> Unit ) diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt index c159b738a72..e6917c237ee 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt @@ -14,10 +14,14 @@ open class AnalyticsOptInStateProvider @Inject constructor() : PreviewParameterP override val values: Sequence get() = sequenceOf( aAnalyticsOptInState(), + aAnalyticsOptInState(hasPolicyLink = false), ) } -fun aAnalyticsOptInState() = AnalyticsOptInState( +fun aAnalyticsOptInState( + hasPolicyLink: Boolean = true, +) = AnalyticsOptInState( applicationName = "Element X", + hasPolicyLink = hasPolicyLink, eventSink = {} ) diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index f1781f1b08e..a6ae7549448 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.BiasAlignment @@ -96,25 +95,27 @@ private fun AnalyticsOptInHeader( subtitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve), iconStyle = BigIcon.Style.Default(CompoundIcons.Chart()) ) - val text = buildAnnotatedStringWithStyledPart( - R.string.screen_analytics_prompt_read_terms, - R.string.screen_analytics_prompt_read_terms_content_link, - color = Color.Unspecified, - underline = false, - bold = true, - tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK, - ) - ClickableLinkText( - annotatedString = text, - onClick = { onClickTerms() }, - modifier = Modifier - .padding(8.dp), - style = ElementTheme.typography.fontBodyMdRegular - .copy( - color = MaterialTheme.colorScheme.secondary, - textAlign = TextAlign.Center, - ) - ) + if (state.hasPolicyLink) { + val text = buildAnnotatedStringWithStyledPart( + R.string.screen_analytics_prompt_read_terms, + R.string.screen_analytics_prompt_read_terms_content_link, + color = Color.Unspecified, + underline = false, + bold = true, + tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK, + ) + ClickableLinkText( + annotatedString = text, + onClick = { onClickTerms() }, + modifier = Modifier + .padding(8.dp), + style = ElementTheme.typography.fontBodyMdRegular + .copy( + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + ) + } } } diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt index 8212cc6c88b..6904734f264 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt @@ -27,8 +27,7 @@ class AnalyticsPreferencesPresenter @Inject constructor( @Composable override fun present(): AnalyticsPreferencesState { val localCoroutineScope = rememberCoroutineScope() - val isEnabled = analyticsService.getUserConsent() - .collectAsState(initial = false) + val isEnabled = analyticsService.userConsentFlow.collectAsState(initial = false) fun handleEvents(event: AnalyticsOptInEvents) { when (event) { diff --git a/features/analytics/impl/src/main/res/values-cy/translations.xml b/features/analytics/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..1a53e3600ee --- /dev/null +++ b/features/analytics/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,10 @@ + + + "Fyddwn ni ddim yn cofnodi nac yn proffilio unrhyw ddata personol" + "Rhannu data defnydd dienw i\'n helpu i nodi problemau." + "Gallwch ddarllen ein holl amodau %1$s." + "yma" + "Gallwch ddiffodd hwn unrhyw bryd" + "Fyddwn ni ddim yn rhannu eich data gyda thrydydd parti" + "Helpwch i wella %1$s" + diff --git a/features/analytics/impl/src/main/res/values-da/translations.xml b/features/analytics/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..971a12ae37e --- /dev/null +++ b/features/analytics/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,10 @@ + + + "Vi vil ikke registrere eller profilere nogen personlige data" + "Del anonyme brugsdata for at hjælpe os med at identificere problemer." + "Du kan læse alle vores vilkår %1$s." + "her" + "Du kan slå dette fra når som helst" + "Vi deler ikke dine data med tredjeparter" + "Hjælp med at forbedre %1$s" + diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml index e2180092d96..9f3bd7e6527 100644 --- a/features/analytics/impl/src/main/res/values-de/translations.xml +++ b/features/analytics/impl/src/main/res/values-de/translations.xml @@ -2,9 +2,9 @@ "Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile." "Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen." - "Weitere Informationen findest du %1$s ." + "Sie können unsere Bedingungen %1$s lesen." "hier" - "Du kannst diese Funktion jederzeit deaktivieren" + "Sie können dies jederzeit beenden" "Wir geben deine Daten nicht an Dritte weiter" "Hilf uns %1$s zu verbessern" diff --git a/features/analytics/impl/src/main/res/values-eu/translations.xml b/features/analytics/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..a0083af67bf --- /dev/null +++ b/features/analytics/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,9 @@ + + + "Partekatu erabilerari buruzko datu anonimoak arazoak identifikatzen laguntzeko." + "Gure baldintza guztiak irakur ditzakezu %1$s." + "hemen" + "Edozein unetan desaktibatu dezakezu" + "Ez ditugu zure datuak hirugarrenekin partekatuko" + "Lagundu %1$s hobetzen" + diff --git a/features/analytics/impl/src/main/res/values-fa/translations.xml b/features/analytics/impl/src/main/res/values-fa/translations.xml index 713e2636d19..45ac8c1511a 100644 --- a/features/analytics/impl/src/main/res/values-fa/translations.xml +++ b/features/analytics/impl/src/main/res/values-fa/translations.xml @@ -1,5 +1,8 @@ + "ما هیچ گونه اطلاعات شخصی را ضبط یا نمایه‌سازی نمی‌کنیم" + "داده های استفاده ناشناس را به اشتراک بگذارید تا به ما در شناسایی مشکلات کمک کند." + "شما می‌توانید تمام شرایط ما را بخوانید%1$s ." "این‌جا" "می‌توانید در هر زمان خاموشش کنید" "داده‌هایتان را با سوم‌شخص‌ها هم‌نمی‌رسانیم" diff --git a/features/analytics/impl/src/main/res/values-lt/translations.xml b/features/analytics/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..11f2cbc74ef --- /dev/null +++ b/features/analytics/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,10 @@ + + + "Mes nekaupsime ir neprofiliuosime jokių asmens duomenų" + "Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas." + "Galite perskaityti visas mūsų sąlygas %1$s." + "čia" + "Tai galite bet kada išjungti" + "Mes nesidalinsime Jūsų duomenimis su trečiosiomis šalimis" + "Padėkite pagerinti %1$s" + diff --git a/features/analytics/impl/src/main/res/values-pt-rBR/translations.xml b/features/analytics/impl/src/main/res/values-pt-rBR/translations.xml index b2fe6e00bcf..b91ab362565 100644 --- a/features/analytics/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/analytics/impl/src/main/res/values-pt-rBR/translations.xml @@ -2,7 +2,7 @@ "Não registraremos nem criaremos perfil baseado em qualquer dado pessoal" "Compartilhe dados de uso anônimos para nos ajudar a identificar problemas." - "Você pode ler todos os nossos termos %1$s ." + "Você pode ler todos os nossos termos %1$s." "aqui" "Você pode desativar isso a qualquer momento" "Não compartilharemos seus dados com terceiros" diff --git a/features/analytics/impl/src/main/res/values-tr/translations.xml b/features/analytics/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..238e53b8982 --- /dev/null +++ b/features/analytics/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,10 @@ + + + "Hiçbir kişisel veriyi kaydetmeyeceğiz veya profillemeyeceğiz" + "Sorunları tanımlamamıza yardımcı olmak için anonim kullanım verilerini paylaşın." + "Tüm şartlarımızı okuyabilirsiniz %1$s." + "burada" + "Bu özelliği istediğiniz zaman kapatabilirsiniz" + "Verilerinizi üçüncü taraflarla paylaşmayacağız" + "%1$s geliştirilmesine yardımcı olun" + diff --git a/features/analytics/impl/src/main/res/values-ur/translations.xml b/features/analytics/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..4b950335cbd --- /dev/null +++ b/features/analytics/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,10 @@ + + + "ہم کسی بھی ذاتی ڈیٹا کو ثبت یا پروفائل نہیں کریں گے" + "مسائل کی نشاندہی کرنے میں ہماری مدد کے لیے گمنام استعمال کے بیانات کا اشتراک کریں۔" + "آپ ہماری تمام شرائط پڑھ سکتے ہیں %1$s۔" + "یہاں" + "آپ اسے کسی بھی وقت بند کر سکتے ہیں" + "ہم آپکے بیانات کا فریق ثالث کے ساتھ اشتراک نہیں کریں گے" + "%1$s کو بہتر بنانے میں مدد کریں" + diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt index 1353d5ab5f7..e9319fd7bad 100644 --- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt @@ -35,10 +35,10 @@ class AnalyticsOptInPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(analyticsService.didAskUserConsent().first()).isFalse() + assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse() initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true)) - assertThat(analyticsService.didAskUserConsent().first()).isTrue() - assertThat(analyticsService.getUserConsent().first()).isTrue() + assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue() + assertThat(analyticsService.userConsentFlow.first()).isTrue() } } @@ -53,10 +53,10 @@ class AnalyticsOptInPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(analyticsService.didAskUserConsent().first()).isFalse() + assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse() initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false)) - assertThat(analyticsService.didAskUserConsent().first()).isTrue() - assertThat(analyticsService.getUserConsent().first()).isFalse() + assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue() + assertThat(analyticsService.userConsentFlow.first()).isFalse() } } } diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt index 30b64eb8ab4..8ba915f03c8 100644 --- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt @@ -11,6 +11,7 @@ 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.appconfig.AnalyticsConfig import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.services.analytics.test.FakeAnalyticsService @@ -35,7 +36,7 @@ class AnalyticsPreferencesPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.isEnabled).isTrue() - assertThat(initialState.policyUrl).isNotEmpty() + assertThat(initialState.policyUrl).isEqualTo(AnalyticsConfig.POLICY_LINK) } } diff --git a/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt index 16a8a151deb..86e6432cc5d 100644 --- a/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt +++ b/features/cachecleaner/impl/src/main/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleaner.kt @@ -10,8 +10,10 @@ package io.element.android.features.cachecleaner.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.cachecleaner.api.CacheCleaner import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.di.annotations.AppCoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber @@ -23,7 +25,8 @@ import javax.inject.Inject */ @ContributesBinding(AppScope::class) class DefaultCacheCleaner @Inject constructor( - private val scope: CoroutineScope, + @AppCoroutineScope + private val coroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, @CacheDirectory private val cacheDir: File, ) : CacheCleaner { @@ -32,8 +35,8 @@ class DefaultCacheCleaner @Inject constructor( } override fun clearCache() { - scope.launch(dispatchers.io) { - runCatching { + coroutineScope.launch(dispatchers.io) { + runCatchingExceptions { SUBDIRS_TO_CLEANUP.forEach { File(cacheDir.path, it).apply { if (exists()) { diff --git a/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt b/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt index 884d29ac88e..997c0fe7039 100644 --- a/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt +++ b/features/cachecleaner/impl/src/test/kotlin/io/element/android/features/cachecleaner/impl/DefaultCacheCleanerTest.kt @@ -55,7 +55,7 @@ class DefaultCacheCleanerTest { } private fun TestScope.aCacheCleaner() = DefaultCacheCleaner( - scope = this, + coroutineScope = this, dispatchers = this.testCoroutineDispatchers(true), cacheDir = temporaryFolder.root, ) diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt index 0456751ae8d..2c279b77258 100644 --- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt @@ -15,11 +15,19 @@ import kotlinx.parcelize.Parcelize sealed interface CallType : NodeInputs, Parcelable { @Parcelize - data class ExternalUrl(val url: String) : CallType + data class ExternalUrl(val url: String) : CallType { + override fun toString(): String { + return "ExternalUrl" + } + } @Parcelize data class RoomCall( val sessionId: SessionId, val roomId: RoomId, - ) : CallType + ) : CallType { + override fun toString(): String { + return "RoomCall(sessionId=$sessionId, roomId=$roomId)" + } + } } 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 239c960a39a..53e1b8c8461 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 @@ -30,8 +30,9 @@ interface ElementCallEntryPoint { * @param avatarUrl The avatar url of the room or DM. * @param timestamp The timestamp of the event that started the call. * @param notificationChannelId The id of the notification channel to use for the call notification. + * @param textContent The text content of the notification. If null the default content from the system will be used. */ - fun handleIncomingCall( + suspend fun handleIncomingCall( callType: CallType.RoomCall, eventId: EventId, senderId: UserId, @@ -40,5 +41,6 @@ interface ElementCallEntryPoint { avatarUrl: String?, timestamp: Long, notificationChannelId: String, + textContent: String?, ) } diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index 7852b5f53c0..13922366c2c 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -1,3 +1,5 @@ +import extension.buildConfigFieldStr +import extension.readLocalProperty import extension.setupAnvil /* @@ -23,13 +25,49 @@ android { testOptions { unitTests.isIncludeAndroidResources = true } + + defaultConfig { + buildConfigFieldStr( + name = "SENTRY_DSN", + value = System.getenv("ELEMENT_CALL_SENTRY_DSN") + ?: readLocalProperty("features.call.sentry.dsn") + ?: "" + ) + buildConfigFieldStr( + name = "POSTHOG_USER_ID", + value = System.getenv("ELEMENT_CALL_POSTHOG_USER_ID") + ?: readLocalProperty("features.call.posthog.userid") + ?: "" + ) + buildConfigFieldStr( + name = "POSTHOG_API_HOST", + value = System.getenv("ELEMENT_CALL_POSTHOG_API_HOST") + ?: readLocalProperty("features.call.posthog.api.host") + ?: "" + ) + buildConfigFieldStr( + name = "POSTHOG_API_KEY", + value = System.getenv("ELEMENT_CALL_POSTHOG_API_KEY") + ?: readLocalProperty("features.call.posthog.api.key") + ?: "" + ) + buildConfigFieldStr( + name = "RAGESHAKE_URL", + value = System.getenv("ELEMENT_CALL_RAGESHAKE_URL") + ?: readLocalProperty("features.call.regeshake.url") + ?: "" + ) + } } setupAnvil() dependencies { implementation(projects.appconfig) + implementation(projects.features.enterprise.api) implementation(projects.libraries.architecture) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.audio.api) implementation(projects.libraries.core) implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) @@ -40,11 +78,13 @@ dependencies { implementation(projects.libraries.push.api) implementation(projects.libraries.uiStrings) implementation(projects.services.analytics.api) + implementation(projects.services.appnavstate.api) implementation(projects.services.toolbox.api) implementation(libs.androidx.webkit) implementation(libs.coil.compose) implementation(libs.network.retrofit) implementation(libs.serialization.json) + implementation(libs.element.call.embedded) api(projects.features.call.api) testImplementation(libs.coroutines.test) @@ -59,6 +99,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) testImplementation(projects.services.analytics.test) + testImplementation(projects.services.appnavstate.test) testImplementation(projects.tests.testutils) testImplementation(libs.androidx.compose.ui.test.junit) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt index 1e42cb972dd..009840743c5 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt @@ -34,7 +34,7 @@ class DefaultElementCallEntryPoint @Inject constructor( context.startActivity(IntentProvider.createIntent(context, callType)) } - override fun handleIncomingCall( + override suspend fun handleIncomingCall( callType: CallType.RoomCall, eventId: EventId, senderId: UserId, @@ -43,6 +43,7 @@ class DefaultElementCallEntryPoint @Inject constructor( avatarUrl: String?, timestamp: Long, notificationChannelId: String, + textContent: String?, ) { val incomingCallNotificationData = CallNotificationData( sessionId = callType.sessionId, @@ -54,6 +55,7 @@ class DefaultElementCallEntryPoint @Inject constructor( avatarUrl = avatarUrl, timestamp = timestamp, notificationChannelId = notificationChannelId, + textContent = textContent, ) activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt index df518caeeeb..c18fef410ee 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt @@ -30,9 +30,15 @@ data class WidgetMessage( @Serializable enum class Action { + @SerialName("io.element.join") + Join, + @SerialName("im.vector.hangup") HangUp, + @SerialName("io.element.close") + Close, + @SerialName("send_event") SendEvent, } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt index cd9e4c2109b..7c97cc85295 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt @@ -25,4 +25,5 @@ data class CallNotificationData( val avatarUrl: String?, val notificationChannelId: String, val timestamp: Long, + val textContent: String?, ) : Parcelable diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt index 5ccb4aa2278..de31b0b02bb 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt @@ -11,7 +11,7 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.media.AudioManager -import android.media.RingtoneManager +import android.provider.Settings import androidx.core.app.NotificationCompat import androidx.core.app.PendingIntentCompat import androidx.core.app.Person @@ -63,6 +63,7 @@ class RingingCallNotificationCreator @Inject constructor( roomAvatarUrl: String?, notificationChannelId: String, timestamp: Long, + textContent: String?, ): Notification? { val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null val imageLoader = imageLoaderHolder.get(matrixClient) @@ -84,7 +85,8 @@ class RingingCallNotificationCreator @Inject constructor( senderName = senderDisplayName, avatarUrl = roomAvatarUrl, notificationChannelId = notificationChannelId, - timestamp = timestamp + timestamp = timestamp, + textContent = textContent, ) val declineIntent = PendingIntentCompat.getBroadcast( @@ -107,10 +109,8 @@ class RingingCallNotificationCreator @Inject constructor( false ) - // TODO use a fallback ringtone if the default ringtone is not available - val ringtoneUri = runCatching { RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) }.getOrNull() return NotificationCompat.Builder(context, notificationChannelId) - .setSmallIcon(CommonDrawables.ic_notification_small) + .setSmallIcon(CommonDrawables.ic_notification) .setPriority(NotificationCompat.PRIORITY_MAX) .setCategory(NotificationCompat.CATEGORY_CALL) .setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, declineIntent, answerIntent).setIsVideo(true)) @@ -120,10 +120,12 @@ class RingingCallNotificationCreator @Inject constructor( .setOngoing(true) .setShowWhen(false) .apply { - if (ringtoneUri != null) { - setSound(ringtoneUri, AudioManager.STREAM_RING) + if (textContent != null) { + setContentText(textContent) + // Else the content text is set by the style (will be "Incoming call") } } + .setSound(Settings.System.DEFAULT_RINGTONE_URI, AudioManager.STREAM_RING) .setTimeoutAfter(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds.inWholeMilliseconds) .setContentIntent(answerIntent) .setDeleteIntent(declineIntent) 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 cab5858e4db..f2afcd01962 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 @@ -7,16 +7,6 @@ package io.element.android.features.call.impl.pip -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -open class PictureInPictureStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aPictureInPictureState(supportPip = true), - aPictureInPictureState(supportPip = true, isInPictureInPicture = true), - ) -} - fun aPictureInPictureState( supportPip: Boolean = false, isInPictureInPicture: Boolean = false, 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 ab6f07ce494..c857c9e2c86 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 @@ -16,6 +16,9 @@ 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 import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.di.annotations.AppCoroutineScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject /** @@ -27,10 +30,16 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() { } @Inject lateinit var activeCallManager: ActiveCallManager + + @AppCoroutineScope + @Inject lateinit var appCoroutineScope: CoroutineScope + override fun onReceive(context: Context, intent: Intent?) { val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) } ?: return context.bindings().inject(this) - activeCallManager.hungUpCall(callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) + appCoroutineScope.launch { + activeCallManager.hungUpCall(callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) + } } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt index dc4541b2773..b779465a342 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt @@ -24,6 +24,7 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.IconCompat import io.element.android.features.call.impl.R import io.element.android.features.call.impl.ui.ElementCallActivity +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.push.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.NotificationIdProvider @@ -67,7 +68,7 @@ class CallForegroundService : Service() { val callActivityIntent = Intent(this, ElementCallActivity::class.java) val pendingIntent = PendingIntentCompat.getActivity(this, 0, callActivityIntent, 0, false) val notification = NotificationCompat.Builder(this, foregroundServiceChannel.id) - .setSmallIcon(IconCompat.createWithResource(this, CommonDrawables.ic_notification_small)) + .setSmallIcon(IconCompat.createWithResource(this, CommonDrawables.ic_notification)) .setContentTitle(getString(R.string.call_foreground_service_title_android)) .setContentText(getString(R.string.call_foreground_service_message_android)) .setContentIntent(pendingIntent) @@ -78,7 +79,7 @@ class CallForegroundService : Service() { } else { 0 } - runCatching { + runCatchingExceptions { ServiceCompat.startForeground(this, notificationId, notification, serviceType) }.onFailure { Timber.e(it, "Failed to start ongoing call foreground service") 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 048e361ac10..04999c82ab6 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 @@ -32,6 +32,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.RoomId @@ -39,15 +40,17 @@ import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.services.analytics.api.ScreenTracker +import io.element.android.services.appnavstate.api.ActiveRoomsHolder +import io.element.android.services.appnavstate.api.AppForegroundStateService import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import timber.log.Timber import java.util.UUID +import kotlin.time.Duration.Companion.seconds class CallScreenPresenter @AssistedInject constructor( @Assisted private val callType: CallType, @@ -58,9 +61,12 @@ class CallScreenPresenter @AssistedInject constructor( private val dispatchers: CoroutineDispatchers, private val matrixClientsProvider: MatrixClientProvider, private val screenTracker: ScreenTracker, - private val appCoroutineScope: CoroutineScope, private val activeCallManager: ActiveCallManager, private val languageTagProvider: LanguageTagProvider, + private val appForegroundStateService: AppForegroundStateService, + private val activeRoomsHolder: ActiveRoomsHolder, + @AppCoroutineScope + private val appCoroutineScope: CoroutineScope, ) : Presenter { @AssistedFactory interface Factory { @@ -82,11 +88,12 @@ class CallScreenPresenter @AssistedInject constructor( var webViewError by remember { mutableStateOf(null) } val languageTag = languageTagProvider.provideLanguageTag() val theme = if (ElementTheme.isLightTheme) "light" else "dark" + DisposableEffect(Unit) { coroutineScope.launch { // Sets the call as joined activeCallManager.joinedCall(callType) - loadUrl( + fetchRoomCallUrl( inputs = callType, urlState = urlState, callWidgetDriver = callWidgetDriver, @@ -95,7 +102,7 @@ class CallScreenPresenter @AssistedInject constructor( ) } onDispose { - activeCallManager.hungUpCall(callType) + appCoroutineScope.launch { activeCallManager.hungUpCall(callType) } } } @@ -134,19 +141,27 @@ class CallScreenPresenter @AssistedInject constructor( val parsedMessage = parseMessage(it) if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) { - if (parsedMessage.action == WidgetMessage.Action.HangUp) { + if (parsedMessage.action == WidgetMessage.Action.Close) { close(callWidgetDriver.value, navigator) - } else if (parsedMessage.action == WidgetMessage.Action.SendEvent) { - // This event is received when a member joins the call, the first one will be the current one - val type = parsedMessage.data?.jsonObject?.get("type")?.jsonPrimitive?.contentOrNull - if (type == "org.matrix.msc3401.call.member") { - isJoinedCall = true - } + } else if (parsedMessage.action == WidgetMessage.Action.Join) { + isJoinedCall = true } } } .launchIn(this) } + + LaunchedEffect(Unit) { + // Wait for the call to be joined, if it takes too long, we display an error + delay(10.seconds) + + if (!isJoinedCall) { + Timber.w("The call took too long to be joined. Displaying an error before exiting.") + + // This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call + webViewError = "" + } + } } fun handleEvents(event: CallScreenEvents) { @@ -158,6 +173,13 @@ class CallScreenPresenter @AssistedInject constructor( // If the call was joined, we need to hang up first. Then the UI will be dismissed automatically. sendHangupMessage(widgetId, interceptor) isJoinedCall = false + + coroutineScope.launch { + // Wait for a couple of seconds to receive the hangup message + // If we don't get it in time, we close the screen anyway + delay(2.seconds) + close(callWidgetDriver.value, navigator) + } } else { coroutineScope.launch { close(callWidgetDriver.value, navigator) @@ -186,7 +208,7 @@ class CallScreenPresenter @AssistedInject constructor( ) } - private suspend fun loadUrl( + private suspend fun fetchRoomCallUrl( inputs: CallType, urlState: MutableState>, callWidgetDriver: MutableState, @@ -207,6 +229,7 @@ class CallScreenPresenter @AssistedInject constructor( theme = theme, ).getOrThrow() callWidgetDriver.value = result.driver + Timber.d("Call widget driver initialized for sessionId: ${inputs.sessionId}, roomId: ${inputs.roomId}") result.url } } @@ -217,37 +240,53 @@ class CallScreenPresenter @AssistedInject constructor( private fun HandleMatrixClientSyncState() { val coroutineScope = rememberCoroutineScope() DisposableEffect(Unit) { - val client = (callType as? CallType.RoomCall)?.sessionId?.let { - matrixClientsProvider.getOrNull(it) - } ?: return@DisposableEffect onDispose { } + 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: ${roomCallType.sessionId}") client.syncService().syncState .collect { state -> if (state == SyncState.Running) { client.notifyCallStartIfNeeded(callType.roomId) } else { - client.syncService().startSync() + appForegroundStateService.updateIsInCallState(true) } } } onDispose { - // We can't use the local coroutine scope here because it will be disposed before this effect - appCoroutineScope.launch { - client.syncService().run { - if (syncState.value == SyncState.Running) { - stopSync() - } - } - } + 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) } } } private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) { - if (!notifiedCallStart) { - getRoom(roomId)?.sendCallNotificationIfNeeded() - ?.onSuccess { notifiedCallStart = true } + if (notifiedCallStart) return + + val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId) + val sendCallNotificationResult = if (activeRoomForSession != null) { + Timber.d("Notifying call start for room $roomId. Has room call: ${activeRoomForSession.info().hasRoomCall}") + activeRoomForSession.sendCallNotificationIfNeeded() + } else { + // Instantiate the room from the session and roomId and send the notification + getJoinedRoom(roomId)?.use { room -> + Timber.d("Notifying call start for room $roomId. Has room call: ${room.info().hasRoomCall}") + room.sendCallNotificationIfNeeded() + } ?: run { + Timber.w("No room found for session $sessionId and room $roomId, skipping call notification.") + return + } } + + sendCallNotificationResult.fold( + onSuccess = { notifiedCallStart = true }, + onFailure = { error -> + Timber.e(error, "Failed to send call notification for room $roomId.") + } + ) } private fun parseMessage(message: String): WidgetMessage? { 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 7ace349f6fa..88d2e81f8bb 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 @@ -11,6 +11,7 @@ import android.annotation.SuppressLint import android.util.Log import android.view.ViewGroup import android.webkit.ConsoleMessage +import android.webkit.JavascriptInterface import android.webkit.PermissionRequest import android.webkit.WebChromeClient import android.webkit.WebView @@ -19,31 +20,33 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -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.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView -import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.call.impl.R import io.element.android.features.call.impl.pip.PictureInPictureEvents import io.element.android.features.call.impl.pip.PictureInPictureState -import io.element.android.features.call.impl.pip.PictureInPictureStateProvider import io.element.android.features.call.impl.pip.aPictureInPictureState +import io.element.android.features.call.impl.utils.InvalidAudioDeviceReason +import io.element.android.features.call.impl.utils.WebViewAudioManager import io.element.android.features.call.impl.utils.WebViewPipController import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.ProgressDialog -import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.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.designsystem.theme.components.TopAppBar import io.element.android.libraries.ui.strings.CommonStrings import timber.log.Timber @@ -53,7 +56,6 @@ interface CallScreenNavigator { fun close() } -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun CallScreenView( state: CallScreenState, @@ -71,19 +73,6 @@ internal fun CallScreenView( Scaffold( modifier = modifier, - topBar = { - if (!pipState.isInPictureInPicture) { - TopAppBar( - title = { Text(stringResource(R.string.element_call)) }, - navigationIcon = { - BackButton( - imageVector = if (pipState.supportPip) CompoundIcons.ArrowLeft() else CompoundIcons.Close(), - onClick = ::handleBack, - ) - } - ) - } - } ) { padding -> BackHandler { handleBack() @@ -97,6 +86,16 @@ internal fun CallScreenView( onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, ) } else { + var webViewAudioManager by remember { mutableStateOf(null) } + val coroutineScope = rememberCoroutineScope() + + var invalidAudioDeviceReason by remember { mutableStateOf(null) } + invalidAudioDeviceReason?.let { + InvalidAudioDeviceDialog(invalidAudioDeviceReason = it) { + invalidAudioDeviceReason = null + } + } + CallWebView( modifier = Modifier .padding(padding) @@ -109,37 +108,74 @@ internal fun CallScreenView( val callback: RequestPermissionCallback = { request.grant(it) } requestPermissions(androidPermissions.toTypedArray(), callback) }, - onWebViewCreate = { webView -> + 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 -> + 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 } } } } +@Composable +private fun InvalidAudioDeviceDialog( + invalidAudioDeviceReason: InvalidAudioDeviceReason, + onDismiss: () -> Unit, +) { + ErrorDialog( + content = when (invalidAudioDeviceReason) { + InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED -> { + stringResource(R.string.call_invalid_audio_device_bluetooth_devices_disabled) + } + }, + onSubmit = onDismiss, + ) +} + @Composable private fun CallWebView( url: AsyncData, userAgent: String, onPermissionsRequest: (PermissionRequest) -> Unit, - onWebViewCreate: (WebView) -> Unit, + onCreateWebView: (WebView) -> Unit, + onDestroyWebView: (WebView) -> Unit, modifier: Modifier = Modifier, ) { if (LocalInspectionMode.current) { @@ -151,7 +187,7 @@ private fun CallWebView( modifier = modifier, factory = { context -> WebView(context).apply { - onWebViewCreate(this) + onCreateWebView(this) setup(userAgent, onPermissionsRequest) } }, @@ -161,6 +197,7 @@ private fun CallWebView( } }, onRelease = { webView -> + onDestroyWebView(webView) webView.destroy() } ) @@ -200,6 +237,20 @@ private fun WebView.setup( ConsoleMessage.MessageLevel.WARNING -> Log.WARN else -> Log.DEBUG } + + val message = buildString { + append(consoleMessage.sourceId()) + append(":") + append(consoleMessage.lineNumber()) + append(" ") + append(consoleMessage.message()) + } + + if (message.contains("password=")) { + // Avoid logging any messages that contain "password" to prevent leaking sensitive information + return true + } + Timber.tag("WebView").log( priority = priority, message = buildString { @@ -215,6 +266,17 @@ private fun WebView.setup( } } +private fun WebView.addBackHandler(onBackPressed: () -> Unit) { + addJavascriptInterface( + object { + @Suppress("unused") + @JavascriptInterface + fun onBackPressed() = onBackPressed() + }, + "backHandler" + ) +} + @PreviewsDayNight @Composable internal fun CallScreenViewPreview( @@ -229,12 +291,6 @@ internal fun CallScreenViewPreview( @PreviewsDayNight @Composable -internal fun CallScreenPipViewPreview( - @PreviewParameter(PictureInPictureStateProvider::class) state: PictureInPictureState, -) = ElementPreview { - CallScreenView( - state = aCallScreenState(), - pipState = state, - requestPermissions = { _, _ -> }, - ) +internal fun InvalidAudioDeviceDialogPreview() = ElementPreview { + InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {} } 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 65859da60e1..870c2244648 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 @@ -10,10 +10,6 @@ package io.element.android.features.call.impl.ui import android.Manifest import android.app.PictureInPictureParams import android.content.Intent -import android.content.res.Configuration -import android.media.AudioAttributes -import android.media.AudioFocusRequest -import android.media.AudioManager import android.os.Build import android.os.Bundle import android.util.Rational @@ -44,9 +40,13 @@ 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.architecture.Presenter import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.audio.api.AudioFocus +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.preferences.api.store.AppPreferencesStore import timber.log.Timber @@ -61,20 +61,17 @@ class ElementCallActivity : @Inject lateinit var callIntentDataParser: CallIntentDataParser @Inject lateinit var presenterFactory: CallScreenPresenter.Factory @Inject lateinit var appPreferencesStore: AppPreferencesStore + @Inject lateinit var enterpriseService: EnterpriseService @Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter + @Inject lateinit var buildMeta: BuildMeta + @Inject lateinit var audioFocus: AudioFocus private lateinit var presenter: Presenter - private lateinit var audioManager: AudioManager - private var requestPermissionCallback: RequestPermissionCallback? = null - private var audiofocusRequest: AudioFocusRequest? = null - private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null - private val requestPermissionsLauncher = registerPermissionResultLauncher() - private var isDarkMode = false private val webViewTarget = mutableStateOf(null) private var eventSink: ((CallScreenEvents) -> Unit)? = null @@ -84,13 +81,14 @@ class ElementCallActivity : applicationContext.bindings().inject(this) - @Suppress("DEPRECATION") - window.addFlags( - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or - WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - ) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true) + } else { + @Suppress("DEPRECATION") + window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + } 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 @@ -98,18 +96,18 @@ class ElementCallActivity : return } - if (savedInstanceState == null) { - updateUiMode(resources.configuration) - } - pictureInPicturePresenter.setPipView(this) - audioManager = getSystemService(AUDIO_SERVICE) as AudioManager + Timber.d("Created ElementCallActivity with call type: ${webViewTarget.value}") setContent { val pipState = pictureInPicturePresenter.present() ListenToAndroidEvents(pipState) - ElementThemeApp(appPreferencesStore) { + ElementThemeApp( + appPreferencesStore = appPreferencesStore, + enterpriseService = enterpriseService, + buildMeta = buildMeta, + ) { val state = presenter.present() eventSink = state.eventSink LaunchedEffect(state.isCallActive, state.isInWidgetMode) { @@ -131,7 +129,13 @@ class ElementCallActivity : } private fun setCallIsActive() { - requestAudioFocus() + audioFocus.requestAudioFocus( + requester = AudioFocusRequester.ElementCall, + onFocusLost = { + // If the audio focus is lost, we do not stop the call. + Timber.tag(loggerTag.value).w("Audio focus lost") + } + ) CallForegroundService.start(this) } @@ -166,11 +170,6 @@ class ElementCallActivity : } } - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - updateUiMode(newConfig) - } - override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setCallType(intent) @@ -178,7 +177,7 @@ class ElementCallActivity : override fun onDestroy() { super.onDestroy() - releaseAudioFocus() + audioFocus.releaseAudioFocus() CallForegroundService.stop(this) pictureInPicturePresenter.setPipView(null) } @@ -244,50 +243,6 @@ class ElementCallActivity : } } - @Suppress("DEPRECATION") - private fun requestAudioFocus() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val audioAttributes = AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .build() - val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) - .setAudioAttributes(audioAttributes) - .build() - audioManager.requestAudioFocus(request) - audiofocusRequest = request - } else { - val listener = AudioManager.OnAudioFocusChangeListener { } - audioManager.requestAudioFocus( - listener, - AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, - ) - audioFocusChangeListener = listener - } - } - - @Suppress("DEPRECATION") - private fun releaseAudioFocus() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - audiofocusRequest?.let { audioManager.abandonAudioFocusRequest(it) } - } else { - audioFocusChangeListener?.let { audioManager.abandonAudioFocus(it) } - } - } - - private fun updateUiMode(configuration: Configuration) { - val prevDarkMode = isDarkMode - val currentNightMode = configuration.uiMode and Configuration.UI_MODE_NIGHT_YES - isDarkMode = currentNightMode != 0 - if (prevDarkMode != isDarkMode) { - if (isDarkMode) { - window.setBackgroundDrawableResource(android.R.drawable.screen_background_dark) - } else { - window.setBackgroundDrawableResource(android.R.drawable.screen_background_light) - } - } - } - @RequiresApi(Build.VERSION_CODES.O) override fun setPipParams() { setPictureInPictureParams(getPictureInPictureParams()) 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 7dd4b560577..dc77af9b774 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,12 +19,17 @@ 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 import io.element.android.features.call.impl.utils.CallState +import io.element.android.features.enterprise.api.EnterpriseService 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.preferences.api.store.AppPreferencesStore +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import javax.inject.Inject /** @@ -47,6 +52,15 @@ class IncomingCallActivity : AppCompatActivity() { @Inject lateinit var appPreferencesStore: AppPreferencesStore + @Inject + lateinit var enterpriseService: EnterpriseService + + @Inject + lateinit var buildMeta: BuildMeta + + @AppCoroutineScope + @Inject lateinit var appCoroutineScope: CoroutineScope + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -64,7 +78,11 @@ class IncomingCallActivity : AppCompatActivity() { val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) } if (notificationData != null) { setContent { - ElementThemeApp(appPreferencesStore) { + ElementThemeApp( + appPreferencesStore = appPreferencesStore, + enterpriseService = enterpriseService, + buildMeta = buildMeta, + ) { IncomingCallScreen( notificationData = notificationData, onAnswer = ::onAnswer, @@ -90,6 +108,8 @@ class IncomingCallActivity : AppCompatActivity() { private fun onCancel() { val activeCall = activeCallManager.activeCall.value ?: return - activeCallManager.hungUpCall(callType = activeCall.callType) + appCoroutineScope.launch { + activeCallManager.hungUpCall(callType = activeCall.callType) + } } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt index 5c9057a847b..954fa3568f3 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.designsystem.background.OnboardingBackground 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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon @@ -74,7 +75,8 @@ internal fun IncomingCallScreen( name = notificationData.senderName, url = notificationData.avatarUrl, size = AvatarSize.IncomingCall, - ) + ), + avatarType = AvatarType.User, ) Spacer(modifier = Modifier.height(24.dp)) Text( @@ -100,7 +102,7 @@ internal fun IncomingCallScreen( ActionButton( size = 64.dp, onClick = { onAnswer(notificationData) }, - icon = CompoundIcons.VoiceCall(), + icon = CompoundIcons.VoiceCallSolid(), title = stringResource(CommonStrings.action_accept), backgroundColor = ElementTheme.colors.iconSuccessPrimary, borderColor = ElementTheme.colors.borderSuccessSubtle @@ -173,6 +175,7 @@ internal fun IncomingCallScreenPreview() = ElementPreview { avatarUrl = null, notificationChannelId = "incoming_call", timestamp = 0L, + textContent = null, ), onAnswer = {}, onCancel = {}, 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 421403d34f1..d4f5abb59db 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 @@ -8,20 +8,31 @@ package io.element.android.features.call.impl.utils import android.annotation.SuppressLint +import android.content.Context +import android.os.PowerManager import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationManagerCompat +import androidx.core.content.getSystemService +import coil3.SingletonImageLoader +import coil3.annotation.DelicateCoilApi import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.ElementCallConfig 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 +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.push.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler +import io.element.android.services.appnavstate.api.AppForegroundStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -38,6 +49,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import timber.log.Timber import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @@ -55,72 +68,108 @@ interface ActiveCallManager { * Registers an incoming call if there isn't an existing active call and posts a [CallState.Ringing] notification. * @param notificationData The data for the incoming call notification. */ - fun registerIncomingCall(notificationData: CallNotificationData) + suspend fun registerIncomingCall(notificationData: CallNotificationData) /** * Called when the active call has been hung up. It will remove any existing UI and the active call. * @param callType The type of call that the user hung up, either an external url one or a room one. */ - fun hungUpCall(callType: CallType) + suspend fun hungUpCall(callType: CallType) /** * Called after the user joined a call. It will remove any existing UI and set the call state as [CallState.InCall]. * * @param callType The type of call that the user joined, either an external url one or a room one. */ - fun joinedCall(callType: CallType) + suspend fun joinedCall(callType: CallType) } @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultActiveCallManager @Inject constructor( + @ApplicationContext context: Context, + @AppCoroutineScope private val coroutineScope: CoroutineScope, private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler, private val ringingCallNotificationCreator: RingingCallNotificationCreator, private val notificationManagerCompat: NotificationManagerCompat, private val matrixClientProvider: MatrixClientProvider, private val defaultCurrentCallService: DefaultCurrentCallService, + private val appForegroundStateService: AppForegroundStateService, + private val imageLoaderHolder: ImageLoaderHolder, ) : ActiveCallManager { + private val tag = "DefaultActiveCallManager" private var timedOutCallJob: Job? = null + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val activeWakeLock: PowerManager.WakeLock? = context.getSystemService() + ?.takeIf { it.isWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK) } + ?.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "${context.packageName}:IncomingCallWakeLock") + override val activeCall = MutableStateFlow(null) + private val mutex = Mutex() + init { observeRingingCall() observeCurrentCall() } - override fun registerIncomingCall(notificationData: CallNotificationData) { - if (activeCall.value != null) { - displayMissedCallNotification(notificationData) - Timber.w("Already have an active call, ignoring incoming call: $notificationData") - return - } - activeCall.value = ActiveCall( - callType = CallType.RoomCall( - sessionId = notificationData.sessionId, - roomId = notificationData.roomId, - ), - callState = CallState.Ringing(notificationData), - ) + override suspend fun registerIncomingCall(notificationData: CallNotificationData) { + mutex.withLock { + appForegroundStateService.updateHasRingingCall(true) + Timber.tag(tag).d("Received incoming call for room id: ${notificationData.roomId}") + if (activeCall.value != null) { + displayMissedCallNotification(notificationData) + Timber.tag(tag).w("Already have an active call, ignoring incoming call: $notificationData") + return + } + activeCall.value = ActiveCall( + callType = CallType.RoomCall( + sessionId = notificationData.sessionId, + roomId = notificationData.roomId, + ), + callState = CallState.Ringing(notificationData), + ) + + timedOutCallJob = coroutineScope.launch { + setUpCoil(notificationData.sessionId) + showIncomingCallNotification(notificationData) - timedOutCallJob = coroutineScope.launch { - showIncomingCallNotification(notificationData) + // Wait for the ringing call to time out + delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds) + incomingCallTimedOut(displayMissedCallNotification = true) + } - // Wait for the ringing call to time out - delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds) - incomingCallTimedOut(displayMissedCallNotification = true) + // Acquire a wake lock to keep the device awake during the incoming call, so we can process the room info data + if (activeWakeLock?.isHeld == false) { + Timber.tag(tag).d("Acquiring partial wakelock") + activeWakeLock.acquire(ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L) + } } } + @OptIn(DelicateCoilApi::class) + private suspend fun setUpCoil(sessionId: SessionId) { + val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return + // Ensure that the image loader is set, else the IncomingCallActivity will not be able to render the caller avatar + SingletonImageLoader.setUnsafe(imageLoaderHolder.get(matrixClient)) + } + /** * Called when the incoming call timed out. It will remove the active call and remove any associated UI, adding a 'missed call' notification. */ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun incomingCallTimedOut(displayMissedCallNotification: Boolean) { + suspend fun incomingCallTimedOut(displayMissedCallNotification: Boolean) = mutex.withLock { + Timber.tag(tag).d("Incoming call timed out") + val previousActiveCall = activeCall.value ?: return val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return activeCall.value = null + if (activeWakeLock?.isHeld == true) { + Timber.tag(tag).d("Releasing partial wakelock after timeout") + activeWakeLock.release() + } cancelIncomingCallNotification() @@ -129,18 +178,31 @@ class DefaultActiveCallManager @Inject constructor( } } - override fun hungUpCall(callType: CallType) { + override suspend fun hungUpCall(callType: CallType) = mutex.withLock { if (activeCall.value?.callType != callType) { - Timber.w("Call type $callType does not match the active call type, ignoring") + Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring") return } + + Timber.tag(tag).d("Hung up call: $callType") + cancelIncomingCallNotification() + if (activeWakeLock?.isHeld == true) { + Timber.tag(tag).d("Releasing partial wakelock after hang up") + activeWakeLock.release() + } timedOutCallJob?.cancel() activeCall.value = null } - override fun joinedCall(callType: CallType) { + 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") + activeWakeLock.release() + } timedOutCallJob?.cancel() activeCall.value = ActiveCall( @@ -151,6 +213,7 @@ class DefaultActiveCallManager @Inject constructor( @SuppressLint("MissingPermission") private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) { + Timber.tag(tag).d("Displaying ringing call notification") val notification = ringingCallNotificationCreator.createNotification( sessionId = notificationData.sessionId, roomId = notificationData.roomId, @@ -160,9 +223,10 @@ class DefaultActiveCallManager @Inject constructor( senderDisplayName = notificationData.senderName ?: notificationData.senderId.value, roomAvatarUrl = notificationData.avatarUrl, notificationChannelId = notificationData.notificationChannelId, - timestamp = notificationData.timestamp + timestamp = notificationData.timestamp, + textContent = notificationData.textContent, ) ?: return - runCatching { + runCatchingExceptions { notificationManagerCompat.notify( NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL), notification, @@ -173,10 +237,13 @@ class DefaultActiveCallManager @Inject constructor( } private fun cancelIncomingCallNotification() { + appForegroundStateService.updateHasRingingCall(false) + Timber.tag(tag).d("Ringing call notification cancelled") notificationManagerCompat.cancel(NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL)) } private fun displayMissedCallNotification(notificationData: CallNotificationData) { + Timber.tag(tag).d("Displaying missed call notification") coroutineScope.launch { onMissedCallNotificationHandler.addMissedCallNotification( sessionId = notificationData.sessionId, @@ -196,13 +263,14 @@ class DefaultActiveCallManager @Inject constructor( .flatMapLatest { activeCall -> val callType = activeCall.callType as CallType.RoomCall // Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room - matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() - ?.getRoom(callType.roomId) - ?.roomInfoFlow - ?.map { - it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants) - } - ?: flowOf() + 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 (callType.sessionId in it.activeRoomCallParticipants) + } } // We only want to check if the room active call status changes .distinctUntilChanged() 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 index 22b9f6bef76..c81b87746e6 100644 --- 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 @@ -8,35 +8,37 @@ package io.element.android.features.call.impl.utils import android.net.Uri +import androidx.core.net.toUri import javax.inject.Inject class CallIntentDataParser @Inject constructor() { private val validHttpSchemes = sequenceOf("https") + private val knownHosts = sequenceOf( + "call.element.io", + ) fun parse(data: String?): String? { - val parsedUrl = data?.let { Uri.parse(data) } ?: return null + val parsedUrl = data?.toUri() ?: return null val scheme = parsedUrl.scheme return when { - scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> parsedUrl + scheme in validHttpSchemes -> parsedUrl scheme == "element" && parsedUrl.host == "call" -> { - // We use this custom scheme to load arbitrary URLs for other instances of Element Call, - // so we can only verify it's an HTTP/HTTPs URL with a non-empty host parsedUrl.getUrlParameter() } scheme == "io.element.call" && parsedUrl.host == null -> { - // We use this custom scheme to load arbitrary URLs for other instances of Element Call, - // so we can only verify it's an HTTP/HTTPs URL with a non-empty host parsedUrl.getUrlParameter() } // This should never be possible, but we still need to take into account the possibility else -> null - }?.withCustomParameters() + } + ?.takeIf { it.host in knownHosts } + ?.withCustomParameters() } private fun Uri.getUrlParameter(): Uri? { return getQueryParameter("url") ?.let { urlParameter -> - Uri.parse(urlParameter).takeIf { uri -> + urlParameter.toUri().takeIf { uri -> uri.scheme in validHttpSchemes && !uri.host.isNullOrBlank() } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt new file mode 100644 index 00000000000..c978aba7e8b --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt @@ -0,0 +1,23 @@ +/* + * 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.utils + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.call.impl.BuildConfig +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.widget.CallAnalyticCredentialsProvider +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultCallAnalyticCredentialsProvider @Inject constructor() : CallAnalyticCredentialsProvider { + override val posthogUserId: String? = BuildConfig.POSTHOG_USER_ID.takeIf { it.isNotBlank() } + override val posthogApiHost: String? = BuildConfig.POSTHOG_API_HOST.takeIf { it.isNotBlank() } + override val posthogApiKey: String? = BuildConfig.POSTHOG_API_KEY.takeIf { it.isNotBlank() } + override val rageshakeSubmitUrl: String? = BuildConfig.RAGESHAKE_URL.takeIf { it.isNotBlank() } + override val sentryDsn: String? = BuildConfig.SENTRY_DSN.takeIf { it.isNotBlank() } +} 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 097e8a65317..d87826fe460 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 @@ -8,23 +8,25 @@ package io.element.android.features.call.impl.utils import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appconfig.ElementCallConfig +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider 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.widget.CallWidgetSettingsProvider import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.services.appnavstate.api.ActiveRoomsHolder import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject +private const val EMBEDDED_CALL_WIDGET_BASE_URL = "https://appassets.androidplatform.net/element-call/index.html" + @ContributesBinding(AppScope::class) class DefaultCallWidgetProvider @Inject constructor( private val matrixClientsProvider: MatrixClientProvider, private val appPreferencesStore: AppPreferencesStore, private val callWidgetSettingsProvider: CallWidgetSettingsProvider, - private val elementCallBaseUrlProvider: ElementCallBaseUrlProvider, + private val activeRoomsHolder: ActiveRoomsHolder, ) : CallWidgetProvider { override suspend fun getWidget( sessionId: SessionId, @@ -32,22 +34,29 @@ class DefaultCallWidgetProvider @Inject constructor( clientId: String, languageTag: String?, theme: String?, - ): Result = runCatching { + ): Result = runCatchingExceptions { val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow() - val room = matrixClient.getRoom(roomId) ?: error("Room not found") - val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() - ?: elementCallBaseUrlProvider.provides(matrixClient) - ?: ElementCallConfig.DEFAULT_BASE_URL - val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = room.isEncrypted) + val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId) + ?: matrixClient.getJoinedRoom(roomId) + ?: error("Room not found") + + val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() + val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL + + val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() + val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted) val callUrl = room.generateWidgetWebViewUrl( widgetSettings = widgetSettings, clientId = clientId, languageTag = languageTag, theme = theme, ).getOrThrow() + + val driver = room.getWidgetDriver(widgetSettings).getOrThrow() + CallWidgetProvider.GetWidgetResult( - driver = room.getWidgetDriver(widgetSettings).getOrThrow(), - url = callUrl + driver = driver, + url = callUrl, ) } } 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 new file mode 100644 index 00000000000..b7d1fdf466f --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt @@ -0,0 +1,503 @@ +/* + * 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.utils + +import android.content.Context +import android.media.AudioDeviceCallback +import android.media.AudioDeviceInfo +import android.media.AudioManager +import android.os.Build +import android.os.PowerManager +import android.webkit.JavascriptInterface +import android.webkit.WebView +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.Json +import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.milliseconds + +/** + * This class manages the audio devices for a WebView. + * + * It listens for audio device changes and updates the WebView with the available devices. + * It also handles the selection of the audio device by the user in the WebView and the default audio device based on the device type. + * + * See also: [Element Call controls docs.](https://github.com/element-hq/element-call/blob/livekit/docs/controls.md#audio-devices) + */ +class WebViewAudioManager( + private val webView: WebView, + private val coroutineScope: CoroutineScope, + private val onInvalidAudioDeviceAdded: (InvalidAudioDeviceReason) -> Unit, +) { + /** + * Whether to disable bluetooth audio devices. This must be done on Android versions lower than Android 12, + * since the WebView approach breaks when using the legacy Bluetooth audio APIs. + */ + private val disableBluetoothAudioDevices = Build.VERSION.SDK_INT < Build.VERSION_CODES.S + + /** + * This flag indicates whether the WebView audio is enabled or not. By default, it is enabled. + */ + private val isWebViewAudioEnabled = AtomicBoolean(true) + + /** + * The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication. + */ + private val wantedDeviceTypes = listOf( + // Paired bluetooth device with microphone + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + // USB devices which can play or record audio + AudioDeviceInfo.TYPE_USB_HEADSET, + AudioDeviceInfo.TYPE_USB_DEVICE, + AudioDeviceInfo.TYPE_USB_ACCESSORY, + // Wired audio devices + AudioDeviceInfo.TYPE_WIRED_HEADSET, + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, + // The built-in speaker of the device + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + // The built-in earpiece of the device + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, + ) + + private val audioManager = webView.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + /** + * This wake lock is used to turn off the screen when the proximity sensor is triggered during a call, + * if the selected audio device is the built-in earpiece. + */ + private val proximitySensorWakeLock by lazy { + webView.context.getSystemService() + ?.takeIf { it.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK) } + ?.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "${webView.context.packageName}:ProximitySensorCallWakeLock") + } + + /** + * This listener tracks the current communication device and updates the WebView when it changes. + */ + @get:RequiresApi(Build.VERSION_CODES.S) + private val commsDeviceChangedListener by lazy { + AudioManager.OnCommunicationDeviceChangedListener { device -> + 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) + } + } + } + + /** + * This callback is used to listen for audio device changes coming from the OS. + */ + private val audioDeviceCallback = object : AudioDeviceCallback() { + override fun onAudioDevicesAdded(addedDevices: Array?) { + val validNewDevices = addedDevices.orEmpty().filter { it.type in wantedDeviceTypes && it.isSink } + if (validNewDevices.isEmpty()) return + + // We need to calculate the available devices ourselves, since calling `listAudioDevices` will return an outdated list + val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id } + 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 + 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. + */ + private var previousSelectedDevice: AudioDeviceInfo? = null + + private var hasRegisteredCallbacks = false + + /** + * Marks if the WebView audio is in call mode or not. + */ + val isInCallMode = AtomicBoolean(false) + + init { + // Apparently, registering the javascript interface takes a while, so registering and immediately using it doesn't work + // We register it ahead of time to avoid this issue + registerWebViewDeviceSelectedCallback() + } + + /** + * Call this method when the call starts to enable in-call audio mode. + * + * It'll set the audio mode to [AudioManager.MODE_IN_COMMUNICATION] if possible, register the audio device callback and set the available audio devices. + */ + fun onCallStarted() { + if (!isInCallMode.compareAndSet(false, true)) { + Timber.w("Audio: tried to enable webview in-call audio mode while already in it") + return + } + + Timber.d("Audio: enabling webview in-call audio mode") + + audioManager.mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // Set 'voice call' mode so volume keys actually control the call volume + AudioManager.MODE_IN_COMMUNICATION + } else { + // Workaround for Android 12 and lower, otherwise changing the audio device doesn't work + AudioManager.MODE_NORMAL + } + + setWebViewAndroidNativeBridge() + } + + /** + * Call this method when the call stops to disable in-call audio mode. + * + * It's the counterpart of [onCallStarted], and should be called as a pair with it once the call has ended. + */ + fun onCallStopped() { + if (!isInCallMode.compareAndSet(true, false)) { + Timber.w("Audio: tried to disable webview in-call audio mode while already disabled") + return + } + + if (proximitySensorWakeLock?.isHeld == true) { + proximitySensorWakeLock?.release() + } + + audioManager.mode = AudioManager.MODE_NORMAL + + if (!hasRegisteredCallbacks) { + Timber.w("Audio: tried to disable webview in-call audio mode without registering callbacks") + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioManager.clearCommunicationDevice() + audioManager.removeOnCommunicationDeviceChangedListener(commsDeviceChangedListener) + } + + audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) + } + + /** + * Registers the WebView audio device selected callback. + * + * This should be called when the WebView is created to ensure that the callback is set before any audio device selection is made. + */ + private fun registerWebViewDeviceSelectedCallback() { + val webViewAudioDeviceSelectedCallback = AndroidWebViewAudioBridge( + onAudioDeviceSelected = { selectedDeviceId -> + Timber.d("Audio device selected in webview, id: $selectedDeviceId") + previousSelectedDevice = listAudioDevices().find { it.id.toString() == selectedDeviceId } + audioManager.selectAudioDevice(selectedDeviceId) + }, + onAudioPlaybackStarted = { + coroutineScope.launch(Dispatchers.Main) { + // Even with the callback, it seems like starting the audio takes a bit on the webview side, + // so we add an extra delay here to make sure it's ready + delay(500.milliseconds) + + // Calling this ahead of time makes the default audio device to not use the right audio stream + setAvailableAudioDevices() + + // Registering the audio devices changed callback will also set the default audio device + audioManager.registerAudioDeviceCallback(audioDeviceCallback, null) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioManager.addOnCommunicationDeviceChangedListener(Executors.newSingleThreadExecutor(), commsDeviceChangedListener) + } + + hasRegisteredCallbacks = true + } + } + ) + Timber.d("Setting androidNativeBridge javascript interface in webview") + webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge") + } + + /** + * Assigns the callback in the WebView to be called when the user selects an audio device. + * + * It should be called with some delay after [registerWebViewDeviceSelectedCallback]. + */ + private fun setWebViewAndroidNativeBridge() { + Timber.d("Adding callback in controls.onAudioPlaybackStarted") + webView.evaluateJavascript("controls.onAudioPlaybackStarted = () => { androidNativeBridge.onTrackReady(); };", null) + Timber.d("Adding callback in controls.onOutputDeviceSelect") + webView.evaluateJavascript("controls.onOutputDeviceSelect = (id) => { androidNativeBridge.setOutputDevice(id); };", null) + } + + /** + * Returns the list of available audio devices. + * + * On Android 11 ([Build.VERSION_CODES.R]) and lower, it returns the list of output devices as a fallback. + */ + private fun listAudioDevices(): List { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioManager.availableCommunicationDevices + } else { + val rawAudioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + rawAudioDevices.filter { it.type in wantedDeviceTypes && it.isSink } + } + } + + /** + * Sets the available audio devices in the WebView. + * + * @param devices The list of audio devices to set. If not provided, it will use the current list of audio devices. + */ + private fun setAvailableAudioDevices( + devices: List = listAudioDevices().map(SerializableAudioDevice::fromAudioDeviceInfo), + ) { + Timber.d("Updating available audio devices") + val jsonSerializer = Json { + encodeDefaults = true + explicitNulls = false + } + val deviceList = jsonSerializer.encodeToString(devices) + webView.evaluateJavascript("controls.setAvailableOutputDevices($deviceList);", { + Timber.d("Audio: setAvailableOutputDevices result: $it") + }) + } + + /** + * Selects the default audio device based on the 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 + .minByOrNull { + wantedDeviceTypes.indexOf(it.type).let { index -> + // If the device type is not in the wantedDeviceTypes list, we give it a low priority + if (index == -1) Int.MAX_VALUE else index + } + } + + 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. + * + * It will select the device only if it is available in the list of audio devices. + * + * @param device The id of the audio device to select. + */ + private fun AudioManager.selectAudioDevice(device: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val audioDevice = availableCommunicationDevices.find { it.id.toString() == device } + selectAudioDevice(audioDevice) + } else { + val rawAudioDevices = getDevices(AudioManager.GET_DEVICES_OUTPUTS) + val audioDevice = rawAudioDevices.find { it.id.toString() == device } + selectAudioDevice(audioDevice) + } + } + + /** + * Selects the audio device on the OS based on the provided device info. + * + * @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) { + Timber.d("Setting communication device: ${device.id} - ${deviceName(device.type, device.productName.toString())}") + setCommunicationDevice(device) + } else { + audioManager.clearCommunicationDevice() + } + } else { + // On Android 11 and lower, we don't have the concept of communication devices + // We have to call the right methods based on the device type + if (device != null) { + if (device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO && disableBluetoothAudioDevices) { + Timber.w("Bluetooth audio devices are disabled on this Android version") + setAudioEnabled(false) + onInvalidAudioDeviceAdded(InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) + return + } + setAudioEnabled(true) + isSpeakerphoneOn = device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER + isBluetoothScoOn = device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO + } else { + isSpeakerphoneOn = false + isBluetoothScoOn = false + } + } + + expectedNewCommunicationDeviceId = null + + @Suppress("WakeLock", "WakeLockTimeout") + 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() + } + } + + /** + * Sets whether the audio is enabled for Element Call in the WebView. + * It will only perform the change if the audio state has changed. + */ + private fun setAudioEnabled(enabled: Boolean) { + coroutineScope.launch(Dispatchers.Main) { + Timber.d("Setting audio enabled in Element Call: $enabled") + if (isWebViewAudioEnabled.getAndSet(enabled) != enabled) { + webView.evaluateJavascript("controls.setAudioEnabled($enabled);", null) + } + } + } +} + +/** + * This class is used to handle the audio device selection in the WebView. + * It listens for the audio device selection event and calls the callback with the selected device ID. + */ +private class AndroidWebViewAudioBridge( + private val onAudioDeviceSelected: (String) -> Unit, + private val onAudioPlaybackStarted: () -> Unit, +) { + @JavascriptInterface + fun setOutputDevice(id: String) { + Timber.d("Audio device selected in webview, id: $id") + onAudioDeviceSelected(id) + } + + @JavascriptInterface + fun onTrackReady() { + // This method can be used to notify the WebView that the audio track is ready + // It can be used to start playing audio or to update the UI + Timber.d("Audio track is ready") + + onAudioPlaybackStarted() + } +} + +private fun deviceName(type: Int, name: String): String { + // TODO maybe translate these? + val typePart = when (type) { + AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth" + AudioDeviceInfo.TYPE_USB_ACCESSORY -> "USB accessory" + AudioDeviceInfo.TYPE_USB_DEVICE -> "USB device" + AudioDeviceInfo.TYPE_USB_HEADSET -> "USB headset" + AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired headset" + AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "Wired headphones" + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "Built-in speaker" + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "Built-in earpiece" + else -> "Unknown" + } + return if (isBuiltIn(type)) { + typePart + } else { + "$typePart - $name" + } +} + +private fun isBuiltIn(type: Int): Boolean = when (type) { + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, + AudioDeviceInfo.TYPE_BUILTIN_MIC, + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE -> true + else -> false +} + +enum class InvalidAudioDeviceReason { + BT_AUDIO_DEVICE_DISABLED, +} + +/** + * This class is used to serialize the audio device information to JSON. + */ +@Suppress("unused") +@Serializable +internal data class SerializableAudioDevice( + val id: String, + val name: String, + @Transient val type: Int = 0, + // These have to be part of the constructor for the JSON serializer to pick them up + val isEarpiece: Boolean = type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, + val isSpeaker: Boolean = type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + val isExternalHeadset: Boolean = type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO, +) { + companion object { + fun fromAudioDeviceInfo(audioDeviceInfo: AudioDeviceInfo): SerializableAudioDevice { + return SerializableAudioDevice( + id = audioDeviceInfo.id.toString(), + name = deviceName(type = audioDeviceInfo.type, name = audioDeviceInfo.productName.toString()), + type = audioDeviceInfo.type, + ) + } + } +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt index 8fea797425a..f46db3ca2b6 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt @@ -11,6 +11,10 @@ import android.webkit.WebView import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +/** + * Documentation about the `controls` command can be found here: + * https://github.com/element-hq/element-call/blob/livekit/docs/controls.md#picture-in-picture + */ class WebViewPipController( private val webView: WebView, ) : PipController { 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 c1013afa6e7..55adc246e9e 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 @@ -16,6 +16,8 @@ import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient +import androidx.core.net.toUri +import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import io.element.android.features.call.impl.BuildConfig @@ -24,6 +26,7 @@ import timber.log.Timber class WebViewWidgetMessageInterceptor( private val webView: WebView, + private val onUrlLoaded: (String) -> Unit, private val onError: (String?) -> Unit, ) : WidgetMessageInterceptor { companion object { @@ -37,14 +40,18 @@ class WebViewWidgetMessageInterceptor( override val interceptedMessages = MutableSharedFlow(extraBufferCapacity = 10) init { + val assetLoader = WebViewAssetLoader.Builder() + .addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(webView.context)) + .build() + webView.webViewClient = object : WebViewClient() { - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) // Due to https://github.com/element-hq/element-x-android/issues/4097 // we need to supply a logging implementation that correctly includes // objects in log lines. - view?.evaluateJavascript( + view.evaluateJavascript( """ function logFn(consoleLogFn, ...args) { consoleLogFn( @@ -66,7 +73,7 @@ class WebViewWidgetMessageInterceptor( // This listener will receive both messages: // - EC widget API -> Element X (message.data.api == "fromWidget") // - Element X -> EC widget API (message.data.api == "toWidget"), we should ignore these - view?.evaluateJavascript( + view.evaluateJavascript( """ window.addEventListener('message', function(event) { let message = {data: event.data, origin: event.origin} @@ -84,6 +91,10 @@ class WebViewWidgetMessageInterceptor( ) } + override fun onPageFinished(view: WebView, url: String) { + onUrlLoaded(url) + } + override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { // No network for instance, transmit the error Timber.e("onReceivedError error: ${error?.errorCode} ${error?.description}") @@ -117,6 +128,14 @@ class WebViewWidgetMessageInterceptor( super.onReceivedSslError(view, handler, error) } + + override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest): WebResourceResponse? { + return assetLoader.shouldInterceptRequest(request.url) + } + + override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? { + return assetLoader.shouldInterceptRequest(url.toUri()) + } } // Create a WebMessageListener, which will receive messages from the WebView and reply to them diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt index a78965a87f8..a21cadab7a8 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt @@ -8,13 +8,14 @@ package io.element.android.features.call.impl.utils import io.element.android.features.call.impl.data.WidgetMessage +import io.element.android.libraries.core.extensions.runCatchingExceptions import kotlinx.serialization.json.Json object WidgetMessageSerializer { private val coder = Json { ignoreUnknownKeys = true } fun deserialize(message: String): Result { - return runCatching { coder.decodeFromString(WidgetMessage.serializer(), message) } + return runCatchingExceptions { coder.decodeFromString(WidgetMessage.serializer(), message) } } fun serialize(message: WidgetMessage): String { diff --git a/features/call/impl/src/main/res/values-cs/translations.xml b/features/call/impl/src/main/res/values-cs/translations.xml index 6272a9e0ec9..5d458dcc1b2 100644 --- a/features/call/impl/src/main/res/values-cs/translations.xml +++ b/features/call/impl/src/main/res/values-cs/translations.xml @@ -3,5 +3,6 @@ "Probíhající hovor" "Klepněte pro návrat k hovoru" "☎️ Probíhá hovor" + "Element Call nepodporuje používání Bluetooth zvukových zařízení v této verzi systému Android. Vyberte jiné zvukové zařízení." "Příchozí Element Call" diff --git a/features/call/impl/src/main/res/values-cy/translations.xml b/features/call/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..cd36fed48ff --- /dev/null +++ b/features/call/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,8 @@ + + + "Galwad cyfredol" + "Tapio i ddychwelyd i\'r alwad" + "☎️ Galwad ar y gweill" + "Nid yw Element Call yn cefnogi defnyddio dyfeisiau sain Bluetooth yn y fersiwn Android hon. Dewiswch ddyfais sain wahanol." + "Galwad Element" + diff --git a/features/call/impl/src/main/res/values-da/translations.xml b/features/call/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..9bb8c30af2f --- /dev/null +++ b/features/call/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,8 @@ + + + "Igangværende opkald" + "Tryk for at vende tilbage til opkaldet" + "☎️ Opkald i gang" + "Element Call understøtter desværre ikke brug af Bluetooth-lydenheder i denne Android-version. Vælg venligst en anden lydenhed." + "Indgående Element opkald" + diff --git a/features/call/impl/src/main/res/values-de/translations.xml b/features/call/impl/src/main/res/values-de/translations.xml index 6429dbe9560..9e2ef1cea44 100644 --- a/features/call/impl/src/main/res/values-de/translations.xml +++ b/features/call/impl/src/main/res/values-de/translations.xml @@ -3,5 +3,6 @@ "Laufender Anruf" "Tippen, um zum Anruf zurückzukehren" "☎️ Anruf läuft" + "In dieser Android-Version unterstützt Element Call derzeit keine Bluetooth-Audiogeräte. Bitte wählen Sie ein anderes Audiogerät aus." "Eingehender Element Call" diff --git a/features/call/impl/src/main/res/values-el/translations.xml b/features/call/impl/src/main/res/values-el/translations.xml index 3dd54c238a8..fa80c689da5 100644 --- a/features/call/impl/src/main/res/values-el/translations.xml +++ b/features/call/impl/src/main/res/values-el/translations.xml @@ -3,5 +3,6 @@ "Συνεχής κλήση" "Πάτα για να επιστρέψεις στην κλήση" "☎️ Κλήση σε εξέλιξη" + "Το Element Call δεν υποστηρίζει τη χρήση συσκευών ήχου Bluetooth σε αυτήν την έκδοση Android. Επέλεξε μια διαφορετική συσκευή ήχου." "Εισερχόμενη κλήση Element" diff --git a/features/call/impl/src/main/res/values-es/translations.xml b/features/call/impl/src/main/res/values-es/translations.xml index 2e931859265..b0c3092efaa 100644 --- a/features/call/impl/src/main/res/values-es/translations.xml +++ b/features/call/impl/src/main/res/values-es/translations.xml @@ -3,4 +3,5 @@ "Llamada en curso" "Pulsa para regresar a la llamada" "☎️ Llamada en curso" + "Llamada de Element Call entrante" 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 05b36cedbc3..becc9e55657 100644 --- a/features/call/impl/src/main/res/values-et/translations.xml +++ b/features/call/impl/src/main/res/values-et/translations.xml @@ -3,5 +3,6 @@ "Käimasolev kõne" "Kõne juurde naasmiseks klõpsa" "☎️ Kõne on pooleli" + "Element Call ei võimalda selles Androidi versioonis Bluetoothi heliseadmete kasutamist. Palun vali mõni muu heliseade." "Sissetulev Element Calli kõne" diff --git a/features/call/impl/src/main/res/values-eu/translations.xml b/features/call/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..9985c5633e5 --- /dev/null +++ b/features/call/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,7 @@ + + + "Abian den deia" + "Sakatu deira itzultzeko." + "☎️ Deia abian" + "Element deia jasotzen" + diff --git a/features/call/impl/src/main/res/values-fr/translations.xml b/features/call/impl/src/main/res/values-fr/translations.xml index bd04a5b3622..bab8f6376dd 100644 --- a/features/call/impl/src/main/res/values-fr/translations.xml +++ b/features/call/impl/src/main/res/values-fr/translations.xml @@ -3,5 +3,6 @@ "Appel en cours" "Cliquez pour retourner à l’appel." "☎️ Appel en cours" + "Element Call ne prend pas en charge l’utilisation d’accessoires Bluetooth dans cette version d’Android. Sélectionnez une autre sortie audio." "Appel Element entrant" diff --git a/features/call/impl/src/main/res/values-hu/translations.xml b/features/call/impl/src/main/res/values-hu/translations.xml index 18f735cff7f..62d359c2bbd 100644 --- a/features/call/impl/src/main/res/values-hu/translations.xml +++ b/features/call/impl/src/main/res/values-hu/translations.xml @@ -3,5 +3,6 @@ "Folyamatban lévő hívás" "Koppintson a híváshoz való visszatéréshez" "☎️ Hívás folyamatban" + "Az Element Call nem támogatja a Bluetooth hangeszközök használatát ebben az Android-verzióban. Válasszon másik hangeszközt." "Bejövő Element hívás" diff --git a/features/call/impl/src/main/res/values-in/translations.xml b/features/call/impl/src/main/res/values-in/translations.xml index e900f8fe9af..7da23d6fb7a 100644 --- a/features/call/impl/src/main/res/values-in/translations.xml +++ b/features/call/impl/src/main/res/values-in/translations.xml @@ -3,5 +3,6 @@ "Panggilan berlangsung" "Ketuk untuk kembali ke panggilan" "☎️ Panggilan sedang berlangsung" + "Element Call tidak mendukung penggunaan perangkat audio Bluetooth di versi Android ini. Silakan pilih perangkat audio yang berbeda." "Element Call Masuk" diff --git a/features/call/impl/src/main/res/values-it/translations.xml b/features/call/impl/src/main/res/values-it/translations.xml index f88f6f6162c..086fc98f55c 100644 --- a/features/call/impl/src/main/res/values-it/translations.xml +++ b/features/call/impl/src/main/res/values-it/translations.xml @@ -3,5 +3,6 @@ "Chiamata in corso" "Tocca per tornare alla chiamata" "☎️ Chiamata in corso" + "Element Call non supporta l\'uso di dispositivi audio Bluetooth in questa versione di Android. Seleziona un dispositivo audio diverso." "Chiamata Element Call in arrivo" diff --git a/features/call/impl/src/main/res/values-nb/translations.xml b/features/call/impl/src/main/res/values-nb/translations.xml index c27cfd0fa86..b93abc6d879 100644 --- a/features/call/impl/src/main/res/values-nb/translations.xml +++ b/features/call/impl/src/main/res/values-nb/translations.xml @@ -1,4 +1,8 @@ + "Pågående samtale" + "Trykk for å gå tilbake til samtalen" + "☎️ Samtale pågår" + "Element Call støtter ikke bruk av Bluetooth-lydenheter i denne Android-versjonen. Velg en annen lydenhet." "Innkommende Element-anrop" diff --git a/features/call/impl/src/main/res/values-pt-rBR/translations.xml b/features/call/impl/src/main/res/values-pt-rBR/translations.xml index c3d90e4bb03..b0895845ce4 100644 --- a/features/call/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/call/impl/src/main/res/values-pt-rBR/translations.xml @@ -3,4 +3,6 @@ "Chamada em andamento" "Toque para retornar à chamada" "☎️ Chamada em andamento" + "O Element Call não tem suporte a dispositivos de áudio Bluetooth nesta versão do Android. Por favor, selecione um dispositivo de áudio diferente." + "Chamada do Element recebida" diff --git a/features/call/impl/src/main/res/values-pt/translations.xml b/features/call/impl/src/main/res/values-pt/translations.xml index 58ee9d523f3..639726bd07b 100644 --- a/features/call/impl/src/main/res/values-pt/translations.xml +++ b/features/call/impl/src/main/res/values-pt/translations.xml @@ -3,5 +3,6 @@ "Chamada em curso" "Toca para voltar à chamada" "☎️ Chamada em curso" + "As chamadas do Element não permitem o uso de dispositivos de áudio Bluetooth nesta versão do Android. Por favor, seleciona outro dispositivo." "A receber chamada da Element" diff --git a/features/call/impl/src/main/res/values-sk/translations.xml b/features/call/impl/src/main/res/values-sk/translations.xml index 71188379c2a..4e5b7db9f71 100644 --- a/features/call/impl/src/main/res/values-sk/translations.xml +++ b/features/call/impl/src/main/res/values-sk/translations.xml @@ -3,5 +3,6 @@ "Prebiehajúci hovor" "Ťuknutím sa vrátite k hovoru" "☎️ Prebieha hovor" + "Element Call nepodporuje používanie zvukových zariadení Bluetooth v tejto verzii systému Android. Vyberte iné zvukové zariadenie." "Prichádzajúci hovor Element Call" diff --git a/features/call/impl/src/main/res/values-tr/translations.xml b/features/call/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..37607db690e --- /dev/null +++ b/features/call/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,7 @@ + + + "Devam eden çağrı" + "Aramaya geri dönmek için dokunun" + "☎️ Çağrı devam ediyor" + "Gelen Element Call" + diff --git a/features/call/impl/src/main/res/values-uk/translations.xml b/features/call/impl/src/main/res/values-uk/translations.xml index 9d63049ddca..f03df23bbed 100644 --- a/features/call/impl/src/main/res/values-uk/translations.xml +++ b/features/call/impl/src/main/res/values-uk/translations.xml @@ -3,5 +3,6 @@ "Поточний виклик" "Торкніться, щоб повернутися до виклику" "☎️ Триває виклик" + "Element Call не підтримує використання аудіопристроїв Bluetooth у цій версії Android. Виберіть інший аудіопристрій." "Вхідний виклик Element" diff --git a/features/call/impl/src/main/res/values-ur/translations.xml b/features/call/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..d0cafe91c6d --- /dev/null +++ b/features/call/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,7 @@ + + + "جاری مکالمہ" + "مکالمہ پر واپس جانے کے لیے تھپتھپائیں" + "☎️ مکالمہ جاری ہے" + "ورودی ایلیمنٹ کال" + diff --git a/features/call/impl/src/main/res/values/localazy.xml b/features/call/impl/src/main/res/values/localazy.xml index 5a386b24164..5ab2caa4fda 100644 --- a/features/call/impl/src/main/res/values/localazy.xml +++ b/features/call/impl/src/main/res/values/localazy.xml @@ -3,5 +3,6 @@ "Ongoing call" "Tap to return to the call" "☎️ Call in progress" + "Element Call does not support using Bluetooth audio devices in this Android version. Please select a different audio device." "Incoming Element Call" 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 56fac5fc3ba..86d9b80d653 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 @@ -20,16 +20,21 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.Shadows.shadowOf +import kotlin.time.Duration.Companion.seconds @RunWith(RobolectricTestRunner::class) class DefaultElementCallEntryPointTest { @Test - fun `startCall - starts ElementCallActivity setup with the needed extras`() { + fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest { val entryPoint = createEntryPoint() entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID)) @@ -39,8 +44,9 @@ class DefaultElementCallEntryPointTest { assertThat(intent.extras?.containsKey("EXTRA_CALL_TYPE")).isTrue() } + @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `handleIncomingCall - registers the incoming call using ActiveCallManager`() { + fun `handleIncomingCall - registers the incoming call using ActiveCallManager`() = runTest { val registerIncomingCallLambda = lambdaRecorder {} val activeCallManager = FakeActiveCallManager(registerIncomingCallResult = registerIncomingCallLambda) val entryPoint = createEntryPoint(activeCallManager = activeCallManager) @@ -54,12 +60,15 @@ class DefaultElementCallEntryPointTest { avatarUrl = "avatarUrl", timestamp = 0, notificationChannelId = "notificationChannelId", + textContent = "textContent", ) + advanceTimeBy(1.seconds) + registerIncomingCallLambda.assertions().isCalledOnce() } - private fun createEntryPoint( + private fun TestScope.createEntryPoint( activeCallManager: FakeActiveCallManager = FakeActiveCallManager(), ) = DefaultElementCallEntryPoint( context = InstrumentationRegistry.getInstrumentation().targetContext, diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/ui/CallScreenViewTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/ui/CallScreenViewTest.kt deleted file mode 100644 index 55ed644c47e..00000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/ui/CallScreenViewTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.call.impl.ui - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.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.tests.testutils.EventsRecorder -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 CallScreenViewTest { - @get:Rule val rule = createAndroidComposeRule() - - @Test - fun `clicking on back when pip is not supported hangs up`() { - val eventsRecorder = EventsRecorder() - val pipEventsRecorder = EventsRecorder() - rule.setCallScreenView( - aCallScreenState( - eventSink = eventsRecorder - ), - aPictureInPictureState( - supportPip = false, - eventSink = pipEventsRecorder, - ), - ) - rule.pressBack() - eventsRecorder.assertSize(2) - eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels } - eventsRecorder.assertTrue(1) { it == CallScreenEvents.Hangup } - pipEventsRecorder.assertSize(1) - pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController } - } - - @Test - fun `clicking on back when pip is supported enables PiP`() { - val eventsRecorder = EventsRecorder() - val pipEventsRecorder = EventsRecorder() - rule.setCallScreenView( - aCallScreenState( - eventSink = eventsRecorder - ), - aPictureInPictureState( - supportPip = true, - eventSink = pipEventsRecorder, - ), - ) - rule.pressBack() - eventsRecorder.assertSize(1) - eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels } - pipEventsRecorder.assertSize(2) - pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController } - pipEventsRecorder.assertTrue(1) { it == PictureInPictureEvents.EnterPictureInPicture } - } -} - -private fun AndroidComposeTestRule.setCallScreenView( - state: CallScreenState, - pipState: PictureInPictureState, - requestPermissions: (Array, RequestPermissionCallback) -> Unit = { _, _ -> }, -) { - setContent { - CallScreenView( - state = state, - pipState = pipState, - requestPermissions = requestPermissions, - ) - } -} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt index 9160ae92d8a..f36267a353e 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt @@ -9,7 +9,7 @@ package io.element.android.features.call.notifications import androidx.core.graphics.drawable.IconCompat import androidx.test.platform.app.InstrumentationRegistry -import coil.ImageLoader +import coil3.ImageLoader import com.google.common.truth.Truth.assertThat import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -73,6 +73,7 @@ class RingingCallNotificationCreatorTest { roomAvatarUrl = "https://example.com/avatar.jpg", notificationChannelId = "channelId", timestamp = 0L, + textContent = "textContent", ) private fun createRingingCallNotificationCreator( 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 2ab7156878b..7709066dd04 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 @@ -26,16 +26,16 @@ 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.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.analytics.test.FakeScreenTracker +import io.element.android.services.appnavstate.api.ActiveRoomsHolder +import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.consumeItemsUntilTimeout -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.testCoroutineDispatchers @@ -45,12 +45,14 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.seconds -class CallScreenPresenterTest { +@OptIn(ExperimentalCoroutinesApi::class) class CallScreenPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -67,7 +69,8 @@ class CallScreenPresenterTest { presenter.present() }.test { // Wait until the URL is loaded - skipItems(1) + advanceTimeBy(1.seconds) + skipItems(2) val initialState = awaitItem() assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io")) assertThat(initialState.webViewError).isNull() @@ -80,9 +83,9 @@ class CallScreenPresenterTest { @Test fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest { - val sendCallNotificationIfNeededLambda = lambdaRecorder> { Result.success(Unit) } + val sendCallNotificationIfNeededLambda = lambdaRecorder> { Result.success(true) } val syncService = FakeSyncService(SyncState.Running) - val fakeRoom = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda) + val fakeRoom = FakeJoinedRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda) val client = FakeMatrixClient(syncService = syncService).apply { givenGetRoomResult(A_ROOM_ID, fakeRoom) } @@ -102,16 +105,23 @@ class CallScreenPresenterTest { presenter.present() }.test { // Wait until the URL is loaded + advanceTimeBy(1.seconds) skipItems(1) + joinedCallLambda.assertions().isCalledOnce() val initialState = awaitItem() - assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java) + 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)) sendCallNotificationIfNeededLambda.assertions().isCalledOnce() + + // Wait until the WidgetDriver is loaded + skipItems(1) + + assertThat(awaitItem().urlState).isInstanceOf(AsyncData.Success::class.java) } } @@ -127,6 +137,9 @@ class CallScreenPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { + // Give it time to load the URL and WidgetDriver + advanceTimeBy(1.seconds) + val initialState = awaitItem() initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) @@ -142,7 +155,6 @@ class CallScreenPresenterTest { } } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) { val navigator = FakeCallScreenNavigator() @@ -159,11 +171,15 @@ class CallScreenPresenterTest { presenter.present() }.test { val initialState = awaitItem() + + // Give it time to load the URL and WidgetDriver + advanceTimeBy(1.seconds) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) initialState.eventSink(CallScreenEvents.Hangup) - // Let background coroutines run + // Let background coroutines run and the widget drive be received runCurrent() assertThat(navigator.closeCalled).isTrue() @@ -173,9 +189,8 @@ class CallScreenPresenterTest { } } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) { + fun `present - a received close message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( @@ -190,11 +205,16 @@ class CallScreenPresenterTest { presenter.present() }.test { val initialState = awaitItem() + + // Give it time to load the URL and WidgetDriver + advanceTimeBy(1.seconds) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) - messageInterceptor.givenInterceptedMessage("""{"action":"im.vector.hangup","api":"fromWidget","widgetId":"1","requestId":"1"}""") + messageInterceptor.givenInterceptedMessage("""{"action":"io.element.close","api":"fromWidget","widgetId":"1","requestId":"1"}""") // Let background coroutines run + advanceTimeBy(1.seconds) runCurrent() assertThat(navigator.closeCalled).isTrue() @@ -205,7 +225,7 @@ class CallScreenPresenterTest { } @Test - fun `present - a received room member message makes the call to be active`() = runTest { + fun `present - a received 'joined' action makes the call to be active`() = runTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( @@ -219,66 +239,72 @@ class CallScreenPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) + // Give it time to load the URL and WidgetDriver + advanceTimeBy(1.seconds) + skipItems(2) val initialState = awaitItem() assertThat(initialState.isCallActive).isFalse() initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) messageInterceptor.givenInterceptedMessage( """ { - "action":"send_event", + "action":"io.element.join", "api":"fromWidget", "widgetId":"1", - "requestId":"1", - "data":{ - "type":"org.matrix.msc3401.call.member" - } + "requestId":"1" } """.trimIndent() ) - skipItems(1) + skipItems(2) val finalState = awaitItem() assertThat(finalState.isCallActive).isTrue() } } @Test - fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest { + fun `present - if in room mode and no join action is received an error is displayed`() = runTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() - val startSyncLambda = lambdaRecorder> { Result.success(Unit) } - val syncService = FakeSyncService(SyncState.Idle).apply { - this.startSyncLambda = startSyncLambda - } - val matrixClient = FakeMatrixClient(syncService = syncService) val presenter = createCallScreenPresenter( callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), - matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }), screenTracker = FakeScreenTracker {}, ) + val messageInterceptor = FakeWidgetMessageInterceptor() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - consumeItemsUntilTimeout() + // Give it time to load the URL and WidgetDriver + advanceTimeBy(1.seconds) + skipItems(2) + val initialState = awaitItem() + assertThat(initialState.isCallActive).isFalse() + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + skipItems(2) - assert(startSyncLambda).isCalledOnce() + // Wait for the timeout to trigger + advanceTimeBy(10.seconds) - cancelAndIgnoreRemainingEvents() + val finalState = awaitItem() + assertThat(finalState.isCallActive).isFalse() + // The error dialog that will force the user to leave the call is displayed + assertThat(finalState.webViewError).isNotNull() + assertThat(finalState.webViewError).isEmpty() } } @Test - fun `present - automatically stops the Matrix client sync on dispose`() = runTest { + fun `present - automatically sets the isInCall state when starting the call and disposing the screen`() = runTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() - val stopSyncLambda = lambdaRecorder> { Result.success(Unit) } - val syncService = FakeSyncService(SyncState.Running).apply { - this.stopSyncLambda = stopSyncLambda + val startSyncLambda = lambdaRecorder> { Result.success(Unit) } + val syncService = FakeSyncService(SyncState.Idle).apply { + this.startSyncLambda = startSyncLambda } val matrixClient = FakeMatrixClient(syncService = syncService) + val appForegroundStateService = FakeAppForegroundStateService() val presenter = createCallScreenPresenter( callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), widgetDriver = widgetDriver, @@ -286,6 +312,7 @@ class CallScreenPresenterTest { dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }), screenTracker = FakeScreenTracker {}, + appForegroundStateService = appForegroundStateService, ) val hasRun = Mutex(true) val job = launch { @@ -296,11 +323,25 @@ class CallScreenPresenterTest { } } - hasRun.lock() + appForegroundStateService.isInCall.test { + // The initial isInCall state will always be false + assertThat(awaitItem()).isFalse() + + // Wait until the call starts + hasRun.lock() + + // Then it'll be true once the call is active + assertThat(awaitItem()).isTrue() + + // If we dispose the screen + job.cancelAndJoin() - job.cancelAndJoin() + // The isInCall state is now false + assertThat(awaitItem()).isFalse() - assert(stopSyncLambda).isCalledOnce() + // And there are no more events + ensureAllEventsConsumed() + } } @Test @@ -313,7 +354,8 @@ class CallScreenPresenterTest { presenter.present() }.test { // Wait until the URL is loaded - skipItems(1) + advanceTimeBy(1.seconds) + skipItems(2) val initialState = awaitItem() initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error")) val finalState = awaitItem() @@ -342,6 +384,8 @@ class CallScreenPresenterTest { initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error")) val finalState = awaitItem() assertThat(finalState.webViewError).isNull() + + cancelAndIgnoreRemainingEvents() } } @@ -354,6 +398,8 @@ class CallScreenPresenterTest { matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), activeCallManager: FakeActiveCallManager = FakeActiveCallManager(), screenTracker: ScreenTracker = FakeScreenTracker(), + appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(), + activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(), ): CallScreenPresenter { val userAgentProvider = object : UserAgentProvider { override fun provide(): String { @@ -369,10 +415,12 @@ class CallScreenPresenterTest { clock = clock, dispatchers = dispatchers, matrixClientsProvider = matrixClientsProvider, - appCoroutineScope = this, activeCallManager = activeCallManager, screenTracker = screenTracker, languageTagProvider = FakeLanguageTagProvider("en-US"), + appForegroundStateService = appForegroundStateService, + appCoroutineScope = backgroundScope, + activeRoomsHolder = activeRoomsHolder, ) } } 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 index 098f396d222..bafbe3f5703 100644 --- 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 @@ -45,6 +45,17 @@ class CallIntentDataParserTest { 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( @@ -64,7 +75,7 @@ class CallIntentDataParserTest { @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, testEmbedded = false) + doTest("https://app.element.io", null) doTest("http://", null) doTest("https://", null) } @@ -193,20 +204,18 @@ class CallIntentDataParserTest { ) } - private fun doTest(url: String, expectedResult: String?, testEmbedded: Boolean = true) { + private fun doTest(url: String, expectedResult: String?) { // Test direct parsing assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult) - if (testEmbedded) { - // 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 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) - } + // Test embedded url, scheme 2 + val urlScheme2 = "io.element.call:/?url=$encodedUrl" + assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult) } companion object { 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 ea1357c0e90..84084c38fe2 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 @@ -7,7 +7,9 @@ package io.element.android.features.call.utils +import android.os.PowerManager import androidx.core.app.NotificationManagerCompat +import androidx.core.content.getSystemService import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import io.element.android.features.call.api.CallType @@ -26,22 +28,19 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID_2 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.FakeMatrixClientProvider -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.push.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader +import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent @@ -49,6 +48,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf @RunWith(RobolectricTestRunner::class) class DefaultActiveCallManagerTest { @@ -57,29 +57,30 @@ class DefaultActiveCallManagerTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `registerIncomingCall - sets the incoming call as active`() = runTest { + setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) - inCancellableScope { - val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) - - assertThat(manager.activeCall.value).isNull() - - val callNotificationData = aCallNotificationData() - manager.registerIncomingCall(callNotificationData) - - assertThat(manager.activeCall.value).isEqualTo( - ActiveCall( - callType = CallType.RoomCall( - sessionId = callNotificationData.sessionId, - roomId = callNotificationData.roomId, - ), - callState = CallState.Ringing(callNotificationData) - ) + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) + + assertThat(manager.activeWakeLock?.isHeld).isFalse() + assertThat(manager.activeCall.value).isNull() + + val callNotificationData = aCallNotificationData() + manager.registerIncomingCall(callNotificationData) + + assertThat(manager.activeCall.value).isEqualTo( + ActiveCall( + callType = CallType.RoomCall( + sessionId = callNotificationData.sessionId, + roomId = callNotificationData.roomId, + ), + callState = CallState.Ringing(callNotificationData) ) + ) - runCurrent() + runCurrent() - verify { notificationManagerCompat.notify(notificationId, any()) } - } + assertThat(manager.activeWakeLock?.isHeld).isTrue() + verify { notificationManagerCompat.notify(notificationId, any()) } } @OptIn(ExperimentalCoroutinesApi::class) @@ -87,210 +88,198 @@ class DefaultActiveCallManagerTest { fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest { val addMissedCallNotificationLambda = lambdaRecorder { _, _, _ -> } val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda) - inCancellableScope { - val manager = createActiveCallManager( - onMissedCallNotificationHandler = onMissedCallNotificationHandler, - ) + val manager = createActiveCallManager( + onMissedCallNotificationHandler = onMissedCallNotificationHandler, + ) - // Register existing call - val callNotificationData = aCallNotificationData() - manager.registerIncomingCall(callNotificationData) - val activeCall = manager.activeCall.value + // Register existing call + val callNotificationData = aCallNotificationData() + manager.registerIncomingCall(callNotificationData) + val activeCall = manager.activeCall.value - // Now add a new call - manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2)) + // Now add a new call + manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2)) - assertThat(manager.activeCall.value).isEqualTo(activeCall) - assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2) + assertThat(manager.activeCall.value).isEqualTo(activeCall) + assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2) - advanceTimeBy(1) + advanceTimeBy(1) - addMissedCallNotificationLambda.assertions() - .isCalledOnce() - .with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID)) - } + addMissedCallNotificationLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID)) } @Test fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest { val addMissedCallNotificationLambda = lambdaRecorder { _, _, _ -> } - inCancellableScope { - val manager = createActiveCallManager( - onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda) - ) + val manager = createActiveCallManager( + onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda) + ) - manager.incomingCallTimedOut(displayMissedCallNotification = true) + manager.incomingCallTimedOut(displayMissedCallNotification = true) - addMissedCallNotificationLambda.assertions().isNeverCalled() - } + addMissedCallNotificationLambda.assertions().isNeverCalled() } @OptIn(ExperimentalCoroutinesApi::class) @Test fun `incomingCallTimedOut - when there is an active call removes it and adds a missed call notification`() = runTest { + setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) val addMissedCallNotificationLambda = lambdaRecorder { _, _, _ -> } - inCancellableScope { - val manager = createActiveCallManager( - onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda), - notificationManagerCompat = notificationManagerCompat, - ) - - manager.registerIncomingCall(aCallNotificationData()) - assertThat(manager.activeCall.value).isNotNull() - - manager.incomingCallTimedOut(displayMissedCallNotification = true) - advanceTimeBy(1) - - assertThat(manager.activeCall.value).isNull() - addMissedCallNotificationLambda.assertions().isCalledOnce() - verify { notificationManagerCompat.cancel(notificationId) } - } + val manager = createActiveCallManager( + onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda), + notificationManagerCompat = notificationManagerCompat, + ) + + manager.registerIncomingCall(aCallNotificationData()) + assertThat(manager.activeCall.value).isNotNull() + assertThat(manager.activeWakeLock?.isHeld).isTrue() + + manager.incomingCallTimedOut(displayMissedCallNotification = true) + advanceTimeBy(1) + + assertThat(manager.activeCall.value).isNull() + assertThat(manager.activeWakeLock?.isHeld).isFalse() + addMissedCallNotificationLambda.assertions().isCalledOnce() + verify { notificationManagerCompat.cancel(notificationId) } } @Test fun `hungUpCall - removes existing call if the CallType matches`() = runTest { + setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) - // Create a cancellable coroutine scope to cancel the test when needed - inCancellableScope { - val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) - val notificationData = aCallNotificationData() - manager.registerIncomingCall(notificationData) - assertThat(manager.activeCall.value).isNotNull() + val notificationData = aCallNotificationData() + manager.registerIncomingCall(notificationData) + assertThat(manager.activeCall.value).isNotNull() + assertThat(manager.activeWakeLock?.isHeld).isTrue() - manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) - assertThat(manager.activeCall.value).isNull() + manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) + assertThat(manager.activeCall.value).isNull() + assertThat(manager.activeWakeLock?.isHeld).isFalse() - verify { notificationManagerCompat.cancel(notificationId) } - } + verify { notificationManagerCompat.cancel(notificationId) } } @Test fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest { + setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) - // Create a cancellable coroutine scope to cancel the test when needed - inCancellableScope { - val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) - manager.registerIncomingCall(aCallNotificationData()) - assertThat(manager.activeCall.value).isNotNull() + manager.registerIncomingCall(aCallNotificationData()) + assertThat(manager.activeCall.value).isNotNull() + assertThat(manager.activeWakeLock?.isHeld).isTrue() - manager.hungUpCall(CallType.ExternalUrl("https://example.com")) - assertThat(manager.activeCall.value).isNotNull() + manager.hungUpCall(CallType.ExternalUrl("https://example.com")) + assertThat(manager.activeCall.value).isNotNull() + assertThat(manager.activeWakeLock?.isHeld).isTrue() - verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) } - } + verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) } } @OptIn(ExperimentalCoroutinesApi::class) @Test fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest { val notificationManagerCompat = mockk(relaxed = true) - inCancellableScope { - val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) - assertThat(manager.activeCall.value).isNull() - - manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID)) - assertThat(manager.activeCall.value).isEqualTo( - ActiveCall( - callType = CallType.RoomCall( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID, - ), - callState = CallState.InCall, - ) + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) + assertThat(manager.activeCall.value).isNull() + + manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID)) + assertThat(manager.activeCall.value).isEqualTo( + ActiveCall( + callType = CallType.RoomCall( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + ), + callState = CallState.InCall, ) + ) - runCurrent() + runCurrent() - verify { notificationManagerCompat.cancel(notificationId) } - } + verify { notificationManagerCompat.cancel(notificationId) } } @OptIn(ExperimentalCoroutinesApi::class) @Test fun `observeRingingCalls - will cancel the active ringing call if the call is cancelled`() = runTest { - val room = FakeMatrixRoom().apply { + val room = FakeBaseRoom().apply { givenRoomInfo(aRoomInfo()) } val client = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } - // Create a cancellable coroutine scope to cancel the test when needed - inCancellableScope { - val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }) - val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider) + val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }) + val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider) - manager.registerIncomingCall(aCallNotificationData()) + manager.registerIncomingCall(aCallNotificationData()) - // Call is active (the other user join the call) - room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) - advanceTimeBy(1) - // Call is cancelled (the other user left the call) - room.givenRoomInfo(aRoomInfo(hasRoomCall = false)) - advanceTimeBy(1) + // Call is active (the other user join the call) + room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) + advanceTimeBy(1) + // Call is cancelled (the other user left the call) + room.givenRoomInfo(aRoomInfo(hasRoomCall = false)) + advanceTimeBy(1) - assertThat(manager.activeCall.value).isNull() - } + assertThat(manager.activeCall.value).isNull() } @OptIn(ExperimentalCoroutinesApi::class) @Test fun `observeRingingCalls - will do nothing if either the session or the room are not found`() = runTest { - val room = FakeMatrixRoom().apply { + val room = FakeBaseRoom().apply { givenRoomInfo(aRoomInfo()) } val client = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } - // Create a cancellable coroutine scope to cancel the test when needed - inCancellableScope { - val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) }) - val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider) + val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) }) + val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider) - // No matrix client + // No matrix client - manager.registerIncomingCall(aCallNotificationData()) + manager.registerIncomingCall(aCallNotificationData()) - room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) - advanceTimeBy(1) - room.givenRoomInfo(aRoomInfo(hasRoomCall = false)) - advanceTimeBy(1) + room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) + advanceTimeBy(1) + room.givenRoomInfo(aRoomInfo(hasRoomCall = false)) + advanceTimeBy(1) - // The call should still be active - assertThat(manager.activeCall.value).isNotNull() + // The call should still be active + assertThat(manager.activeCall.value).isNotNull() - // No room - client.givenGetRoomResult(A_ROOM_ID, null) - matrixClientProvider.getClient = { Result.success(client) } + // No room + client.givenGetRoomResult(A_ROOM_ID, null) + matrixClientProvider.getClient = { Result.success(client) } - manager.registerIncomingCall(aCallNotificationData()) + manager.registerIncomingCall(aCallNotificationData()) - room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) - advanceTimeBy(1) - room.givenRoomInfo(aRoomInfo(hasRoomCall = false)) - advanceTimeBy(1) + room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) + advanceTimeBy(1) + room.givenRoomInfo(aRoomInfo(hasRoomCall = false)) + advanceTimeBy(1) - // The call should still be active - assertThat(manager.activeCall.value).isNotNull() - } + // The call should still be active + assertThat(manager.activeCall.value).isNotNull() } - private fun TestScope.inCancellableScope(block: suspend CoroutineScope.() -> Unit) { - launch(SupervisorJob()) { - block() - cancel() + private fun setupShadowPowerManager() { + shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService()).apply { + setIsWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK, true) } } - private fun CoroutineScope.createActiveCallManager( + private fun TestScope.createActiveCallManager( matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(), notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true), - coroutineScope: CoroutineScope = this, ) = DefaultActiveCallManager( - coroutineScope = coroutineScope, + context = InstrumentationRegistry.getInstrumentation().targetContext, + coroutineScope = backgroundScope, onMissedCallNotificationHandler = onMissedCallNotificationHandler, ringingCallNotificationCreator = RingingCallNotificationCreator( context = InstrumentationRegistry.getInstrumentation().targetContext, @@ -301,5 +290,7 @@ class DefaultActiveCallManagerTest { notificationManagerCompat = notificationManagerCompat, matrixClientProvider = matrixClientProvider, defaultCurrentCallService = DefaultCurrentCallService(), + appForegroundStateService = FakeAppForegroundStateService(), + imageLoaderHolder = FakeImageLoaderHolder(), ) } diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt index 67f59713eea..a93dd5f91c9 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt @@ -9,21 +9,19 @@ package io.element.android.features.call.utils import com.google.common.truth.Truth.assertThat import io.element.android.features.call.impl.utils.DefaultCallWidgetProvider -import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider 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.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +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.widget.FakeCallWidgetSettingsProvider import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value +import io.element.android.services.appnavstate.api.ActiveRoomsHolder import kotlinx.coroutines.test.runTest import org.junit.Test @@ -45,7 +43,7 @@ class DefaultCallWidgetProviderTest { @Test fun `getWidget - fails if it can't generate the URL for the widget`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.failure(Exception("Can't generate URL for widget")) } ) val client = FakeMatrixClient().apply { @@ -57,7 +55,7 @@ class DefaultCallWidgetProviderTest { @Test fun `getWidget - fails if it can't get the widget driver`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") }, getWidgetDriverResult = { Result.failure(Exception("Can't get a widget driver")) } ) @@ -70,7 +68,7 @@ class DefaultCallWidgetProviderTest { @Test fun `getWidget - returns a widget driver when all steps are successful`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") }, getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) }, ) @@ -82,64 +80,60 @@ class DefaultCallWidgetProviderTest { } @Test - fun `getWidget - will use a custom base url if it exists`() = runTest { - val room = FakeMatrixRoom( - generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") }, - getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) }, - ) + fun `getWidget - reuses the active room if possible`() = runTest { val client = FakeMatrixClient().apply { - givenGetRoomResult(A_ROOM_ID, room) + // No room from the client + givenGetRoomResult(A_ROOM_ID, null) } - val preferencesStore = InMemoryAppPreferencesStore().apply { - setCustomElementCallBaseUrl("https://custom.element.io") + val activeRoomsHolder = ActiveRoomsHolder().apply { + // A current active room with the same room id + addRoom( + FakeJoinedRoom( + baseRoom = FakeBaseRoom(roomId = A_ROOM_ID), + generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") }, + getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) }, + ) + ) } - val settingsProvider = FakeCallWidgetSettingsProvider() val provider = createProvider( matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }, - callWidgetSettingsProvider = settingsProvider, - appPreferencesStore = preferencesStore, + activeRoomsHolder = activeRoomsHolder ) - provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme") - - assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io") + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isSuccess).isTrue() } @Test - fun `getWidget - will use a wellknown base url if it exists`() = runTest { - val aCustomUrl = "https://custom.element.io" - val providesLambda = lambdaRecorder { _ -> aCustomUrl } - val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { matrixClient -> - providesLambda(matrixClient) - } - val room = FakeMatrixRoom( + fun `getWidget - will use a custom base url if it exists`() = runTest { + val room = FakeJoinedRoom( generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") }, getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) }, ) val client = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } + val preferencesStore = InMemoryAppPreferencesStore().apply { + setCustomElementCallBaseUrl("https://custom.element.io") + } val settingsProvider = FakeCallWidgetSettingsProvider() val provider = createProvider( matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }, callWidgetSettingsProvider = settingsProvider, - elementCallBaseUrlProvider = elementCallBaseUrlProvider, + appPreferencesStore = preferencesStore, ) provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme") - assertThat(settingsProvider.providedBaseUrls).containsExactly(aCustomUrl) - providesLambda.assertions() - .isCalledOnce() - .with(value(client)) + + assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io") } private fun createProvider( matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(), - elementCallBaseUrlProvider: ElementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { _ -> null }, + activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(), ) = DefaultCallWidgetProvider( matrixClientsProvider = matrixClientProvider, appPreferencesStore = appPreferencesStore, callWidgetSettingsProvider = callWidgetSettingsProvider, - elementCallBaseUrlProvider = elementCallBaseUrlProvider, + activeRoomsHolder = activeRoomsHolder, ) } 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 3024ce9f2f4..90527370e7b 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 @@ -11,6 +11,7 @@ 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 +import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.MutableStateFlow class FakeActiveCallManager( @@ -20,15 +21,15 @@ class FakeActiveCallManager( ) : ActiveCallManager { override val activeCall = MutableStateFlow(null) - override fun registerIncomingCall(notificationData: CallNotificationData) { + override suspend fun registerIncomingCall(notificationData: CallNotificationData) = simulateLongTask { registerIncomingCallResult(notificationData) } - override fun hungUpCall(callType: CallType) { + override suspend fun hungUpCall(callType: CallType) = simulateLongTask { hungUpCallResult(callType) } - override fun joinedCall(callType: CallType) { + override suspend fun joinedCall(callType: CallType) = simulateLongTask { joinedCallResult(callType) } diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt deleted file mode 100644 index 4ac716f22d0..00000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.call.utils - -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider -import io.element.android.tests.testutils.lambda.lambdaError - -class FakeElementCallBaseUrlProvider( - private val providesLambda: (MatrixClient) -> String? = { lambdaError() } -) : ElementCallBaseUrlProvider { - override suspend fun provides(matrixClient: MatrixClient): String? { - return providesLambda(matrixClient) - } -} diff --git a/features/call/test/build.gradle.kts b/features/call/test/build.gradle.kts index d40d5c8c8c0..4fe9b95384f 100644 --- a/features/call/test/build.gradle.kts +++ b/features/call/test/build.gradle.kts @@ -22,4 +22,5 @@ dependencies { implementation(projects.features.call.impl) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.test) + implementation(projects.tests.testutils) } diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt index 3514cf425d1..8e6ee00a169 100644 --- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt +++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt @@ -30,6 +30,7 @@ fun aCallNotificationData( avatarUrl: String? = AN_AVATAR_URL, notificationChannelId: String = "channel_id", timestamp: Long = 0L, + textContent: String? = null, ): CallNotificationData = CallNotificationData( sessionId = sessionId, roomId = roomId, @@ -40,4 +41,5 @@ fun aCallNotificationData( avatarUrl = avatarUrl, notificationChannelId = notificationChannelId, timestamp = timestamp, + textContent = textContent, ) 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 9a720bd2e75..09a12692597 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 @@ -11,16 +11,26 @@ 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: (CallType) -> Unit = {}, - var handleIncomingCallResult: (CallType.RoomCall, EventId, UserId, String?, String?, String?, String) -> Unit = { _, _, _, _, _, _, _ -> } + var startCallResult: (CallType) -> Unit = { lambdaError() }, + var handleIncomingCallResult: ( + CallType.RoomCall, + EventId, + UserId, + String?, + String?, + String?, + String, + String?, + ) -> Unit = { _, _, _, _, _, _, _, _ -> lambdaError() } ) : ElementCallEntryPoint { override fun startCall(callType: CallType) { startCallResult(callType) } - override fun handleIncomingCall( + override suspend fun handleIncomingCall( callType: CallType.RoomCall, eventId: EventId, senderId: UserId, @@ -28,8 +38,18 @@ class FakeElementCallEntryPoint( senderName: String?, avatarUrl: String?, timestamp: Long, - notificationChannelId: String + notificationChannelId: String, + textContent: String?, ) { - handleIncomingCallResult(callType, eventId, senderId, roomName, senderName, avatarUrl, notificationChannelId) + handleIncomingCallResult( + callType, + eventId, + senderId, + roomName, + senderName, + avatarUrl, + notificationChannelId, + textContent, + ) } } diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/ConfirmingStartDmWithMatrixUser.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/ConfirmingStartDmWithMatrixUser.kt new file mode 100644 index 00000000000..af19408324d --- /dev/null +++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/ConfirmingStartDmWithMatrixUser.kt @@ -0,0 +1,15 @@ +/* + * 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.createroom.api + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.user.MatrixUser + +data class ConfirmingStartDmWithMatrixUser( + val matrixUser: MatrixUser, +) : AsyncAction.Confirming diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt index b37c8047015..3b35d0a3e1a 100644 --- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt +++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt @@ -11,7 +11,7 @@ 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.RoomIdOrAlias interface CreateRoomEntryPoint : FeatureEntryPoint { fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder @@ -21,6 +21,7 @@ interface CreateRoomEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onSuccess(roomId: RoomId) + fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) + fun onOpenRoomDirectory() } } diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.kt index 50c67ba9563..e64be9f9230 100644 --- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.kt +++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.kt @@ -10,13 +10,19 @@ package io.element.android.features.createroom.api import androidx.compose.runtime.MutableState import io.element.android.libraries.architecture.AsyncAction 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 interface StartDMAction { /** * Try to find an existing DM with the given user, or create one if none exists. - * @param userId The user to start a DM with. + * @param matrixUser The user to start a DM with. + * @param createIfDmDoesNotExist If true, create a DM if one does not exist. If false and the DM + * does not exist, the action will fail with the value [ConfirmingStartDmWithMatrixUser]. * @param actionState The state to update with the result of the action. */ - suspend fun execute(userId: UserId, actionState: MutableState>) + suspend fun execute( + matrixUser: MatrixUser, + createIfDmDoesNotExist: Boolean, + actionState: MutableState>, + ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt new file mode 100644 index 00000000000..69eac7d3694 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt @@ -0,0 +1,48 @@ +/* + * 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.createroom + +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import io.element.android.features.createroom.impl.CreateRoomFlowNode.NavTarget +import io.element.android.libraries.architecture.overlay.Overlay +import io.element.android.libraries.architecture.overlay.operation.hide +import io.element.android.libraries.architecture.overlay.operation.show +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias + +interface CreateRoomNavigator : Plugin { + fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) + fun onCreateNewRoom() + fun onShowJoinRoomByAddress() + fun onDismissJoinRoomByAddress() + fun onOpenRoomDirectory() +} + +class DefaultCreateRoomNavigator( + private val backstack: BackStack, + private val overlay: Overlay, + private val openRoom: (RoomIdOrAlias, List) -> Unit, + private val openRoomDirectory: () -> Unit, +) : CreateRoomNavigator { + override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) = openRoom(roomIdOrAlias, serverNames) + + override fun onOpenRoomDirectory() = openRoomDirectory() + + override fun onCreateNewRoom() { + backstack.push(NavTarget.NewRoom) + } + + override fun onShowJoinRoomByAddress() { + overlay.show(NavTarget.JoinByAddress) + } + + override fun onDismissJoinRoomByAddress() { + overlay.hide() + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt index 1a2916b16a3..a7678f130f7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt @@ -19,6 +19,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.createroom.CreateRoomNavigator import io.element.android.features.createroom.impl.addpeople.AddPeopleNode import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode import io.element.android.features.createroom.impl.di.CreateRoomComponent @@ -46,6 +47,7 @@ class ConfigureRoomFlowNode @AssistedInject constructor( private val component by lazy { parent!!.bindings().createRoomComponentBuilder().build() } + private val navigator = plugins().first() override val daggerComponent: Any get() = component @@ -69,8 +71,7 @@ class ConfigureRoomFlowNode @AssistedInject constructor( createNode(buildContext = buildContext, plugins = listOf(callback)) } NavTarget.ConfigureRoom -> { - val callbacks = plugins() - createNode(buildContext = buildContext, plugins = callbacks) + createNode(buildContext = buildContext, plugins = listOf(navigator)) } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt index 5f629d51eb0..29b1525f511 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt @@ -39,7 +39,7 @@ class CreateRoomDataStore @Inject constructor( } val createRoomConfigWithInvites: Flow = combine( - selectedUserListDataStore.selectedUsers(), + selectedUserListDataStore.selectedUsers, createRoomConfigFlow, ) { selectedUsers, config -> config.copy(invites = selectedUsers.toImmutableList()) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index 9e06bf60809..b8b755a0b33 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -8,25 +8,28 @@ package io.element.android.features.createroom.impl import android.os.Parcelable +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack -import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.createroom.DefaultCreateRoomNavigator import io.element.android.features.createroom.api.CreateRoomEntryPoint -import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode +import io.element.android.features.createroom.impl.joinbyaddress.JoinRoomByAddressNode import io.element.android.features.createroom.impl.root.CreateRoomRootNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.OverlayView import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -47,35 +50,41 @@ class CreateRoomFlowNode @AssistedInject constructor( @Parcelize data object NewRoom : NavTarget + + @Parcelize + data object JoinByAddress : NavTarget } + private val navigator = DefaultCreateRoomNavigator( + backstack = backstack, + overlay = overlay, + openRoom = { roomIdOrAlias, viaServers -> + plugins().forEach { it.onOpenRoom(roomIdOrAlias, viaServers) } + }, + openRoomDirectory = { + plugins().forEach { it.onOpenRoomDirectory() } + } + ) + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { - val callback = object : CreateRoomRootNode.Callback { - override fun onCreateNewRoom() { - backstack.push(NavTarget.NewRoom) - } - - override fun onStartChatSuccess(roomId: RoomId) { - plugins().forEach { it.onSuccess(roomId) } - } - } - createNode(buildContext = buildContext, plugins = listOf(callback)) + createNode(buildContext = buildContext, plugins = listOf(navigator)) } NavTarget.NewRoom -> { - val callback = object : ConfigureRoomNode.Callback { - override fun onCreateRoomSuccess(roomId: RoomId) { - plugins().forEach { it.onSuccess(roomId) } - } - } - createNode(buildContext = buildContext, plugins = listOf(callback)) + createNode(buildContext = buildContext, plugins = listOf(navigator)) + } + NavTarget.JoinByAddress -> { + createNode(buildContext = buildContext, plugins = listOf(navigator)) } } } @Composable override fun View(modifier: Modifier) { - BackstackView() + Box(modifier = modifier) { + BackstackView() + OverlayView(transitionHandler = remember { JumpToEndTransitionHandler() }) + } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.kt index d48712c400c..c9b60786bde 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.kt @@ -10,14 +10,15 @@ package io.element.android.features.createroom.impl import androidx.compose.runtime.MutableState import com.squareup.anvil.annotations.ContributesBinding import im.vector.app.features.analytics.plan.CreatedRoom +import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.createroom.api.StartDMAction import io.element.android.libraries.architecture.AsyncAction 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.core.UserId import io.element.android.libraries.matrix.api.room.StartDMResult import io.element.android.libraries.matrix.api.room.startDM +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.services.analytics.api.AnalyticsService import javax.inject.Inject @@ -26,9 +27,13 @@ class DefaultStartDMAction @Inject constructor( private val matrixClient: MatrixClient, private val analyticsService: AnalyticsService, ) : StartDMAction { - override suspend fun execute(userId: UserId, actionState: MutableState>) { + override suspend fun execute( + matrixUser: MatrixUser, + createIfDmDoesNotExist: Boolean, + actionState: MutableState>, + ) { actionState.value = AsyncAction.Loading - when (val result = matrixClient.startDM(userId)) { + when (val result = matrixClient.startDM(matrixUser.userId, createIfDmDoesNotExist)) { is StartDMResult.Success -> { if (result.isNew) { analyticsService.capture(CreatedRoom(isDM = true)) @@ -38,6 +43,9 @@ class DefaultStartDMAction @Inject constructor( is StartDMResult.Failure -> { actionState.value = AsyncAction.Failure(result.throwable) } + StartDMResult.DmDoesNotExist -> { + actionState.value = ConfirmingStartDmWithMatrixUser(matrixUser = matrixUser) + } } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index a26abd20fd4..d39ac4d2500 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.compound.theme.ElementTheme import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.components.UserListView import io.element.android.features.createroom.impl.userlist.UserListEvents @@ -23,9 +22,7 @@ import io.element.android.features.createroom.impl.userlist.UserListState 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.aliasScreenTitle 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.ui.strings.CommonStrings @@ -74,12 +71,7 @@ private fun AddPeopleViewTopBar( onNextClick: () -> Unit, ) { TopAppBar( - title = { - Text( - text = stringResource(id = R.string.screen_create_room_add_people_title), - style = ElementTheme.typography.aliasScreenTitle - ) - }, + titleStr = stringResource(id = R.string.screen_create_room_add_people_title), navigationIcon = { BackButton(onClick = onBackClick) }, actions = { val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt index 966369e6e3d..e7188251633 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt @@ -18,8 +18,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.createroom.CreateRoomNavigator import io.element.android.features.createroom.impl.di.CreateRoomScope -import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(CreateRoomScope::class) @@ -29,6 +30,8 @@ class ConfigureRoomNode @AssistedInject constructor( private val presenter: ConfigureRoomPresenter, private val analyticsService: AnalyticsService, ) : Node(buildContext, plugins = plugins) { + private val navigator = plugins().first() + init { lifecycle.subscribe( onResume = { @@ -37,14 +40,6 @@ class ConfigureRoomNode @AssistedInject constructor( ) } - interface Callback : Plugin { - fun onCreateRoomSuccess(roomId: RoomId) - } - - private fun onCreateRoomSuccess(roomId: RoomId) { - plugins().forEach { it.onCreateRoomSuccess(roomId) } - } - @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -52,7 +47,9 @@ class ConfigureRoomNode @AssistedInject constructor( state = state, modifier = modifier, onBackClick = this::navigateUp, - onCreateRoomSuccess = this::onCreateRoomSuccess, + onCreateRoomSuccess = { + navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList()) + }, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index f79bd1e25c2..3a7ec00e3ef 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -31,6 +31,7 @@ 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.room.alias.RoomAliasHelper +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity @@ -65,7 +66,9 @@ class ConfigureRoomPresenter @Inject constructor( val cameraPermissionState = cameraPermissionPresenter.present() val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig()) val homeserverName = remember { matrixClient.userIdServerName() } - val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false) + val isKnockFeatureEnabled by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) + }.collectAsState(initial = false) val roomAddressValidity = remember { mutableStateOf(RoomAddressValidity.Unknown) } @@ -175,6 +178,7 @@ class ConfigureRoomPresenter @Inject constructor( isEncrypted = config.roomVisibility is RoomVisibilityState.Private, isDirect = false, visibility = RoomVisibility.Private, + historyVisibilityOverride = RoomHistoryVisibility.Invited, preset = RoomPreset.PRIVATE_CHAT, invite = config.invites.map { it.userId }, avatar = avatarUrl, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 2684c8334dd..9c468539ab8 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -40,13 +40,14 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap 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.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text @@ -187,12 +188,7 @@ private fun ConfigureRoomToolbar( onNextClick: () -> Unit, ) { TopAppBar( - title = { - Text( - text = stringResource(R.string.screen_create_room_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(R.string.screen_create_room_title), navigationIcon = { BackButton(onClick = onBackClick) }, actions = { TextButton( @@ -219,6 +215,8 @@ private fun RoomNameWithAvatar( ) { UnsavedAvatar( avatarUri = avatarUri, + avatarSize = AvatarSize.EditRoomDetails, + avatarType = AvatarType.Room(), modifier = Modifier.clickable(onClick = onAvatarClick), ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressEvents.kt new file mode 100644 index 00000000000..dbbcef56fa7 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressEvents.kt @@ -0,0 +1,14 @@ +/* + * 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.createroom.impl.joinbyaddress + +sealed interface JoinRoomByAddressEvents { + data object Dismiss : JoinRoomByAddressEvents + data object Continue : JoinRoomByAddressEvents + data class UpdateAddress(val address: String) : JoinRoomByAddressEvents +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressNode.kt new file mode 100644 index 00000000000..d6338ab43c9 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressNode.kt @@ -0,0 +1,39 @@ +/* + * 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.createroom.impl.joinbyaddress + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.createroom.CreateRoomNavigator +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class JoinRoomByAddressNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: JoinRoomByAddressPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + private val navigator = plugins().first() + private val presenter = presenterFactory.create(navigator) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + JoinRoomByAddressView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt new file mode 100644 index 00000000000..cf4788cbbfd --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt @@ -0,0 +1,138 @@ +/* + * 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.createroom.impl.joinbyaddress + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.createroom.CreateRoomNavigator +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +import kotlinx.coroutines.delay +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.time.Duration.Companion.seconds + +private const val ADDRESS_RESOLVE_TIMEOUT_IN_SECONDS = 10 + +class JoinRoomByAddressPresenter @AssistedInject constructor( + @Assisted private val navigator: CreateRoomNavigator, + private val client: MatrixClient, + private val roomAliasHelper: RoomAliasHelper, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(navigator: CreateRoomNavigator): JoinRoomByAddressPresenter + } + + @Composable + override fun present(): JoinRoomByAddressState { + var address by remember { mutableStateOf("") } + var internalAddressState by remember { mutableStateOf(RoomAddressState.Unknown) } + var validateAddress: Boolean by remember { mutableStateOf(false) } + + fun handleEvents(event: JoinRoomByAddressEvents) { + when (event) { + JoinRoomByAddressEvents.Continue -> { + when (val currentState = internalAddressState) { + is RoomAddressState.RoomFound -> onRoomFound(currentState) + else -> validateAddress = true + } + } + JoinRoomByAddressEvents.Dismiss -> navigator.onDismissJoinRoomByAddress() + is JoinRoomByAddressEvents.UpdateAddress -> { + validateAddress = false + address = event.address.trim() + } + } + } + + RoomAddressStateEffect( + fullAddress = address, + onRoomAddressStateChange = { addressState -> + internalAddressState = addressState + if (addressState is RoomAddressState.RoomFound && validateAddress) { + onRoomFound(addressState) + } + } + ) + + val addressState by remember { + derivedStateOf { + // We only want to show the "RoomFound" state as long as the user didn't validate the address. + if (validateAddress || internalAddressState is RoomAddressState.RoomFound) { + internalAddressState + } else { + RoomAddressState.Unknown + } + } + } + + return JoinRoomByAddressState( + address = address, + addressState = addressState, + eventSink = ::handleEvents + ) + } + + private fun onRoomFound(state: RoomAddressState.RoomFound) { + navigator.onDismissJoinRoomByAddress() + navigator.onOpenRoom( + roomIdOrAlias = state.resolved.roomId.toRoomIdOrAlias(), + serverNames = state.resolved.servers + ) + } + + @Composable + private fun RoomAddressStateEffect( + fullAddress: String, + onRoomAddressStateChange: (RoomAddressState) -> Unit, + ) { + val onChange by rememberUpdatedState(onRoomAddressStateChange) + LaunchedEffect(fullAddress) { + // Whenever the address changes, reset the state to unknown + onChange(RoomAddressState.Unknown) + // debounce the room address resolution + delay(300) + val roomAlias = tryOrNull { RoomAlias(fullAddress) } + if (roomAlias != null && roomAliasHelper.isRoomAliasValid(roomAlias)) { + onChange(RoomAddressState.Resolving) + onChange(client.resolveRoomAddress(roomAlias)) + } else { + onChange(RoomAddressState.Invalid) + } + } + } + + private suspend fun MatrixClient.resolveRoomAddress(roomAlias: RoomAlias): RoomAddressState { + return withTimeoutOrNull(ADDRESS_RESOLVE_TIMEOUT_IN_SECONDS.seconds) { + resolveRoomAlias(roomAlias) + .fold( + onSuccess = { resolved -> + if (resolved.isPresent) { + RoomAddressState.RoomFound(resolved.get()) + } else { + RoomAddressState.RoomNotFound + } + }, + onFailure = { _ -> RoomAddressState.RoomNotFound } + ) + } ?: RoomAddressState.RoomNotFound + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt new file mode 100644 index 00000000000..11791181e1b --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt @@ -0,0 +1,26 @@ +/* + * 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.createroom.impl.joinbyaddress + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias + +data class JoinRoomByAddressState( + val address: String, + val addressState: RoomAddressState, + val eventSink: (JoinRoomByAddressEvents) -> Unit +) + +@Immutable +sealed interface RoomAddressState { + data object Unknown : RoomAddressState + data object Invalid : RoomAddressState + data object Resolving : RoomAddressState + data object RoomNotFound : RoomAddressState + data class RoomFound(val resolved: ResolvedRoomAlias) : RoomAddressState +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt new file mode 100644 index 00000000000..6281a8e8e38 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt @@ -0,0 +1,37 @@ +/* + * 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.createroom.impl.joinbyaddress + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias + +open class JoinRoomByAddressStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aJoinRoomByAddressState(), + aJoinRoomByAddressState(address = "#room-"), + aJoinRoomByAddressState(address = "#room-", addressState = RoomAddressState.Invalid), + aJoinRoomByAddressState(address = "#room-name:matrix.org", addressState = RoomAddressState.Resolving), + aJoinRoomByAddressState(address = "#room-name-none:matrix.org", addressState = RoomAddressState.RoomNotFound), + aJoinRoomByAddressState( + address = "#room-name:matrix.org", + addressState = RoomAddressState.RoomFound(ResolvedRoomAlias(RoomId("!aRoom:id"), emptyList())), + ), + ) +} + +fun aJoinRoomByAddressState( + address: String = "", + addressState: RoomAddressState = RoomAddressState.Unknown, + eventSink: (JoinRoomByAddressEvents) -> Unit = {}, +) = JoinRoomByAddressState( + address = address, + addressState = addressState, + eventSink = eventSink +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt new file mode 100644 index 00000000000..c256f5140c8 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt @@ -0,0 +1,134 @@ +/* + * 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.createroom.impl.joinbyaddress + +import androidx.compose.foundation.layout.Column +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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.createroom.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.Button +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TextFieldValidity +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun JoinRoomByAddressView( + state: JoinRoomByAddressState, + modifier: Modifier = Modifier, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + modifier = modifier, + sheetState = sheetState, + onDismissRequest = { + state.eventSink(JoinRoomByAddressEvents.Dismiss) + }, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + RoomAddressField( + address = state.address, + addressState = state.addressState, + requestFocus = sheetState.isVisible, + onAddressChange = { + state.eventSink(JoinRoomByAddressEvents.UpdateAddress(it)) + }, + onContinue = { + state.eventSink(JoinRoomByAddressEvents.Continue) + }, + ) + Spacer(modifier = Modifier.height(24.dp)) + Button( + text = stringResource(CommonStrings.action_continue), + modifier = Modifier.fillMaxWidth(), + showProgress = state.addressState is RoomAddressState.Resolving, + onClick = { + state.eventSink(JoinRoomByAddressEvents.Continue) + } + ) + } + } +} + +@Composable +private fun RoomAddressField( + address: String, + addressState: RoomAddressState, + requestFocus: Boolean, + onAddressChange: (String) -> Unit, + onContinue: () -> Unit, + modifier: Modifier = Modifier, +) { + val focusRequester = remember { FocusRequester() } + if (requestFocus) { + LaunchedEffect(Unit) { focusRequester.requestFocus() } + } + TextField( + modifier = modifier.focusRequester(focusRequester), + value = address, + label = stringResource(R.string.screen_start_chat_join_room_by_address_action), + placeholder = stringResource(R.string.screen_start_chat_join_room_by_address_placeholder), + supportingText = when (addressState) { + RoomAddressState.Invalid -> stringResource(R.string.screen_start_chat_join_room_by_address_invalid_address) + is RoomAddressState.RoomFound -> stringResource(R.string.screen_start_chat_join_room_by_address_room_found) + RoomAddressState.RoomNotFound -> stringResource(R.string.screen_start_chat_join_room_by_address_room_not_found) + RoomAddressState.Unknown, RoomAddressState.Resolving -> stringResource(R.string.screen_start_chat_join_room_by_address_supporting_text) + }, + validity = when (addressState) { + RoomAddressState.Unknown, RoomAddressState.Resolving -> TextFieldValidity.None + RoomAddressState.Invalid, RoomAddressState.RoomNotFound -> TextFieldValidity.Invalid + is RoomAddressState.RoomFound -> TextFieldValidity.Valid + }, + onValueChange = onAddressChange, + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Go + ), + keyboardActions = KeyboardActions( + onGo = { onContinue() } + ) + ) +} + +@PreviewsDayNight +@Composable +internal fun JoinRoomByAddressViewPreview( + @PreviewParameter(JoinRoomByAddressStateProvider::class) state: JoinRoomByAddressState +) = ElementPreview { + JoinRoomByAddressView(state = state) +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt index aef27b94dbf..f76a5d5c615 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt @@ -8,9 +8,9 @@ package io.element.android.features.createroom.impl.root import android.app.Activity +import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -20,9 +20,10 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.createroom.CreateRoomNavigator import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(SessionScope::class) @@ -33,18 +34,7 @@ class CreateRoomRootNode @AssistedInject constructor( private val analyticsService: AnalyticsService, private val inviteFriendsUseCase: InviteFriendsUseCase, ) : Node(buildContext, plugins = plugins) { - interface Callback : Plugin { - fun onCreateNewRoom() - fun onStartChatSuccess(roomId: RoomId) - } - - private fun onCreateNewRoom() { - plugins().forEach { it.onCreateNewRoom() } - } - - private fun onStartChatSuccess(roomId: RoomId) { - plugins().forEach { it.onStartChatSuccess(roomId) } - } + private val navigator = plugins().first() init { lifecycle.subscribe( @@ -55,14 +45,18 @@ class CreateRoomRootNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = presenter.present() - val activity = LocalContext.current as Activity + val activity = requireNotNull(LocalActivity.current) CreateRoomRootView( state = state, modifier = modifier, onCloseClick = this::navigateUp, - onNewRoomClick = ::onCreateNewRoom, - onOpenDM = ::onStartChatSuccess, - onInviteFriendsClick = { invitePeople(activity) } + onNewRoomClick = navigator::onCreateNewRoom, + onOpenDM = { + navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList()) + }, + onJoinByAddressClick = navigator::onShowJoinRoomByAddress, + onInviteFriendsClick = { invitePeople(activity) }, + onRoomDirectorySearchClick = navigator::onOpenRoomDirectory ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 2d5ebc87e18..ea8be45e6b7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -9,6 +9,8 @@ package io.element.android.features.createroom.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 @@ -20,6 +22,8 @@ import io.element.android.features.createroom.impl.userlist.UserListPresenterArg 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 @@ -31,6 +35,7 @@ class CreateRoomRootPresenter @Inject constructor( userListDataStore: UserListDataStore, private val startDMAction: StartDMAction, private val buildMeta: BuildMeta, + private val featureFlagService: FeatureFlagService, ) : Presenter { private val presenter = presenterFactory.create( UserListPresenterArgs( @@ -47,10 +52,18 @@ class CreateRoomRootPresenter @Inject constructor( val localCoroutineScope = rememberCoroutineScope() val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + val isRoomDirectorySearchEnabled by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch) + }.collectAsState(initial = false) + fun handleEvents(event: CreateRoomRootEvents) { when (event) { is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch { - startDMAction.execute(event.matrixUser.userId, startDmActionState) + startDMAction.execute( + matrixUser = event.matrixUser, + createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming, + actionState = startDmActionState, + ) } CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized } @@ -60,6 +73,7 @@ class CreateRoomRootPresenter @Inject constructor( applicationName = buildMeta.applicationName, userListState = userListState, startDmAction = startDmActionState.value, + isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt index 8e1c66b7df6..7a6d651db39 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt @@ -15,5 +15,6 @@ data class CreateRoomRootState( val applicationName: String, val userListState: UserListState, val startDmAction: AsyncAction, + val isRoomDirectorySearchEnabled: Boolean, val eventSink: (CreateRoomRootEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index 6c453255762..9f2d59e3b6e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.createroom.impl.userlist.UserListState import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList import io.element.android.features.createroom.impl.userlist.aUserListState @@ -34,7 +35,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, + isRoomDirectorySearchEnabled: Boolean = false, eventSink: (CreateRoomRootEvents) -> Unit = {}, ) = CreateRoomRootState( applicationName = applicationName, userListState = userListState, startDmAction = startDmAction, + isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, eventSink = eventSink, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 0e33cf8b6e7..248d3319859 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -28,6 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.components.UserListView import io.element.android.libraries.designsystem.components.async.AsyncActionView @@ -36,13 +36,13 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.persistentListOf @@ -54,6 +54,8 @@ fun CreateRoomRootView( onNewRoomClick: () -> Unit, onOpenDM: (RoomId) -> Unit, onInviteFriendsClick: () -> Unit, + onJoinByAddressClick: () -> Unit, + onRoomDirectorySearchClick: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -88,6 +90,8 @@ fun CreateRoomRootView( state = state, onNewRoomClick = onNewRoomClick, onInvitePeopleClick = onInviteFriendsClick, + onJoinByAddressClick = onJoinByAddressClick, + onRoomDirectorySearchClick = onRoomDirectorySearchClick, onDmClick = onOpenDM, ) } @@ -110,6 +114,19 @@ fun CreateRoomRootView( ?: state.eventSink(CreateRoomRootEvents.CancelStartDM) }, onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, + confirmationDialog = { data -> + if (data is ConfirmingStartDmWithMatrixUser) { + CreateDmConfirmationBottomSheet( + matrixUser = data.matrixUser, + onSendInvite = { + state.eventSink(CreateRoomRootEvents.StartDM(data.matrixUser)) + }, + onDismiss = { + state.eventSink(CreateRoomRootEvents.CancelStartDM) + }, + ) + } + }, ) } @@ -119,12 +136,7 @@ private fun CreateRoomRootViewTopBar( onCloseClick: () -> Unit, ) { TopAppBar( - title = { - Text( - text = stringResource(id = CommonStrings.action_start_chat), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(id = CommonStrings.action_start_chat), navigationIcon = { BackButton( imageVector = CompoundIcons.Close(), @@ -139,6 +151,8 @@ private fun CreateRoomActionButtonsList( state: CreateRoomRootState, onNewRoomClick: () -> Unit, onInvitePeopleClick: () -> Unit, + onJoinByAddressClick: () -> Unit, + onRoomDirectorySearchClick: () -> Unit, onDmClick: (RoomId) -> Unit, ) { LazyColumn { @@ -149,6 +163,15 @@ private fun CreateRoomActionButtonsList( onClick = onNewRoomClick, ) } + if (state.isRoomDirectorySearchEnabled) { + item { + CreateRoomActionButton( + iconRes = CompoundDrawables.ic_compound_list_bulleted, + text = stringResource(id = R.string.screen_room_directory_search_title), + onClick = onRoomDirectorySearchClick, + ) + } + } item { CreateRoomActionButton( iconRes = CompoundDrawables.ic_compound_share_android, @@ -156,6 +179,13 @@ private fun CreateRoomActionButtonsList( onClick = onInvitePeopleClick, ) } + item { + CreateRoomActionButton( + iconRes = CompoundDrawables.ic_compound_room, + text = stringResource(R.string.screen_start_chat_join_room_by_address_action), + onClick = onJoinByAddressClick, + ) + } if (state.userListState.recentDirectRooms.isNotEmpty()) { item { ListSectionHeader( @@ -196,7 +226,7 @@ private fun CreateRoomActionButton( ) { Icon( modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.secondary, + tint = ElementTheme.colors.iconSecondary, resourceId = iconRes, contentDescription = null, ) @@ -216,6 +246,8 @@ internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProv onCloseClick = {}, onNewRoomClick = {}, onOpenDM = {}, + onJoinByAddressClick = {}, onInviteFriendsClick = {}, + onRoomDirectorySearchClick = {}, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt index ad9b18f6241..32d5767cc6a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt @@ -54,7 +54,7 @@ class DefaultUserListPresenter @AssistedInject constructor( recentDirectRooms = matrixClient.getRecentDirectRooms() } var isSearchActive by rememberSaveable { mutableStateOf(false) } - val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList()) + val selectedUsers by userListDataStore.selectedUsers.collectAsState(emptyList()) var searchQuery by rememberSaveable { mutableStateOf("") } var searchResults: SearchBarResultState> by remember { mutableStateOf(SearchBarResultState.Initial()) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListDataStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListDataStore.kt index 3be9868c980..a500e3a05fc 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListDataStore.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListDataStore.kt @@ -8,22 +8,22 @@ package io.element.android.features.createroom.impl.userlist import io.element.android.libraries.matrix.api.user.MatrixUser -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject class UserListDataStore @Inject constructor() { - private val selectedUsers: MutableStateFlow> = MutableStateFlow(emptyList()) + private val _selectedUsers: MutableStateFlow> = MutableStateFlow(emptyList()) fun selectUser(user: MatrixUser) { - if (!selectedUsers.value.contains(user)) { - selectedUsers.tryEmit(selectedUsers.value.plus(user)) + if (!_selectedUsers.value.contains(user)) { + _selectedUsers.tryEmit(_selectedUsers.value.plus(user)) } } fun removeUserFromSelection(user: MatrixUser) { - selectedUsers.tryEmit(selectedUsers.value.minus(user)) + _selectedUsers.tryEmit(_selectedUsers.value.minus(user)) } - fun selectedUsers(): Flow> = selectedUsers + val selectedUsers = _selectedUsers.asStateFlow() } diff --git a/features/createroom/impl/src/main/res/values-be/translations.xml b/features/createroom/impl/src/main/res/values-be/translations.xml index 861fca4d057..8acb990b023 100644 --- a/features/createroom/impl/src/main/res/values-be/translations.xml +++ b/features/createroom/impl/src/main/res/values-be/translations.xml @@ -3,15 +3,17 @@ "Новы пакой" "Запрасіць карыстальнікаў" "Пры стварэнні пакоя адбылася памылка" - "Паведамленні ў гэтым пакоі зашыфраваны. Гэта шыфраванне нельга адключыць." - "Прыватны пакой (толькі па запрашэнні)" - "Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней." - "Публічны пакой (для ўсіх)" + "Толькі запрошаныя людзі могуць атрымаць доступ да гэтага пакоя. Усе паведамленні абаронены end-to-end шыфраваннем." + "Прыватны пакой" + "Любы можа знайсці гэты пакой. +Вы можаце змяніць гэта ў любы час у наладах пакоя." + "Публічны пакой" "Хто заўгодна" "Доступ у пакой" "Папрасіце далучыцца" "Назва пакоя" "Стварыце пакой" "Тэма (неабавязкова)" + "Каталог пакояў" "Пры спробе пачаць чат адбылася памылка" diff --git a/features/createroom/impl/src/main/res/values-bg/translations.xml b/features/createroom/impl/src/main/res/values-bg/translations.xml index 28f42b4156a..fa6dae371c5 100644 --- a/features/createroom/impl/src/main/res/values-bg/translations.xml +++ b/features/createroom/impl/src/main/res/values-bg/translations.xml @@ -3,11 +3,21 @@ "Нова стая" "Поканване на хора" "Възникна грешка при създаването на стаята" - "Съобщенията в тази стая са шифровани. Шифроването не може да бъде изключено впоследствие." - "Частна стая (само с покана)" - "Съобщенията не са шифровани и всеки може да ги прочете. Можете да активирате шифроването на по-късна дата." - "Публична стая (всеки)" + "Само поканени хора имат достъп до тази стая. Всички съобщения са шифровани от край до край." + "Частна стая" + "Всеки може да намери тази стая. +Можете да промените това по всяко време в настройките на стаята." + "Общодостъпна стая" + "Всеки може да се присъедини към тази стая" + "Всеки" + "За да бъде тази стая видима в директорията на общодостъпните стаи, ще ви е необходим адрес на стаята." "Име на стаята" + "Видимост на стаята" "Създаване на стая" "Тема за разговор (незадължително)" + "Присъединяване към стая по адрес" + "Не е валиден адрес" + "Въведете…" + "Стаята не е намерена" + "напр. #room-name:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-cs/translations.xml b/features/createroom/impl/src/main/res/values-cs/translations.xml index 697120201d8..53592e0db0a 100644 --- a/features/createroom/impl/src/main/res/values-cs/translations.xml +++ b/features/createroom/impl/src/main/res/values-cs/translations.xml @@ -19,5 +19,12 @@ To můžete kdykoli změnit v nastavení místnosti." "Viditelnost místnosti" "Vytvořit místnost" "Téma (nepovinné)" + "Adresář místností" "Při pokusu o zahájení chatu došlo k chybě" + "Vstoupit do místnosti pomocí adresy" + "Neplatná adresa" + "Zadejte…" + "Odpovídající místnost nalezena" + "Místnost nebyla nalezena" + "např. #room-name:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-cy/translations.xml b/features/createroom/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..a3a9e30e354 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,30 @@ + + + "Ystafell newydd" + "Gwahodd pobl" + "Bu gwall wrth greu\'r ystafell" + "Dim ond pobl wahoddwyd all gael mynediad i\'r ystafell hon. Mae pob neges wedi\'i hamgryptio o\'r dechrau i\'r diwedd." + "Ystafell breifat" + "Gall unrhyw un ddod o hyd i\'r ystafell hon. +Gallwch newid hyn unrhyw bryd yng ngosodiadau ystafell." + "Ystafell gyhoeddus" + "Gall unrhyw un ymuno â\'r ystafell hon" + "Unrhyw un" + "Mynediad i\'r Ystafell" + "Gall unrhyw un ofyn am gael ymuno â\'r ystafell ond bydd rhaid i weinyddwr neu gymedrolwr dderbyn y cais" + "Gofyn i gael ymuno" + "Er mwyn i\'r ystafell hon fod yn weladwy yn y cyfeiriadur ystafelloedd cyhoeddus, bydd angen cyfeiriad ystafell arnoch." + "Cyfeiriad yr ystafell" + "Enw\'r ystafell" + "Gwelededd yr ystafell" + "Creu ystafell" + "Pwnc (dewisol)" + "Cyfeiriadur ystafelloedd" + "Digwyddodd gwall wrth geisio cychwyn sgwrs" + "Ymuno â\'r ystafell yn ôl cyfeiriad" + "Ddim yn gyfeiriad dilys" + "Ewch i mewn…" + "Cafwyd hyd i ystafell gyfatebol" + "Heb ganfod yr ystafell" + "e.e. #enw-ystafell:matrix.org" + diff --git a/features/createroom/impl/src/main/res/values-da/translations.xml b/features/createroom/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..c9c182b474b --- /dev/null +++ b/features/createroom/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,30 @@ + + + "Nyt rum" + "Invitér folk" + "Der opstod en fejl ved oprettelsen af rummet" + "Kun inviterede personer kan få adgang til dette rum. Alle meddelelser er ende-til-ende krypteret." + "Privat rum" + "Alle kan finde dette rum. +Du kan ændre dette når som helst i rummets indstillinger." + "Offentligt rum" + "Alle kan deltage i dette rum" + "Enhver" + "Adgang til rummet" + "Alle kan bede om at deltage i rummet, men en administrator eller en moderator skal acceptere anmodningen" + "Spørg om at deltage" + "Hvis dette rum skal være synligt i det offentlige register, skal du bruge en rum-adresse." + "Rummets adresse" + "Navn på rum" + "Rummets synlighed" + "Opret et rum" + "Emne (valgfrit)" + "Register over rum" + "Der opstod en fejl under forsøget på at starte en samtale" + "Tilslut dig rummet med adressen" + "Ikke en gyldig adresse" + "Indtast…" + "Matchende rum fundet" + "Rum ikke fundet" + "f.eks. #rummets-navn:matrix.org" + 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 10cda04d9ed..90438aa38c1 100644 --- a/features/createroom/impl/src/main/res/values-de/translations.xml +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -1,23 +1,30 @@ "Neuer Raum" - "Personen einladen" + "Nutzer einladen" "Beim Erstellen des Chats ist ein Fehler aufgetreten" - "Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind durchgehend verschlüsselt." + "Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind Ende-zu-Ende verschlüsselt." "Privater Chatroom" - "Jeder kann diesen Chatroom finden. + "Alle können diesen Chatroom finden. Sie können dies aber jederzeit in den Chatroomeinstellungen ändern." - "Öffentlicher Chatroom" - "Jeder kann diesem Chatroom beitreten" - "Jemand" - "Chatroom Zugang" - "Jeder kann darum bitten, dem Chatroom beizutreten, aber ein Administrator oder ein Moderator muss die Anfrage akzeptieren." + "Öffentlicher Raum" + "Jeder darf diesen Raum betreten" + "Jeder" + "Chatroomzugang" + "Jeder kann den Zutritt zum Raum beantragen, aber ein Moderator muss die Anfrage akzeptieren." "Beitritt beantragen" "Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse." - "Chatroom Adresse" + "Chatroomadresse" "Raumname" " Sichtbarkeit des Chatrooms" "Raum erstellen" "Thema (optional)" + "Raum-Verzeichnis" "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten" + "Raum per Adresse betreten" + "Keine gültige Adresse" + "Eintreten…" + "Passender Raum gefunden" + "Raum nicht gefunden" + "z. B. #room -name:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-el/translations.xml b/features/createroom/impl/src/main/res/values-el/translations.xml index 4dc3acaaa32..c036b43a05e 100644 --- a/features/createroom/impl/src/main/res/values-el/translations.xml +++ b/features/createroom/impl/src/main/res/values-el/translations.xml @@ -1,23 +1,30 @@ - "Νέο δωμάτιο" + "Νέα αίθουσα" "Πρόσκληση ατόμων" - "Παρουσιάστηκε σφάλμα κατά τη δημιουργία του δωματίου" - "Μόνο άτομα που έχουν προσκληθεί μπορούν να έχουν πρόσβαση σε αυτό το δωμάτιο. Όλα τα μηνύματα είναι κρυπτογραφημένα από άκρο σε άκρο." - "Ιδιωτικό δωμάτιο" - "Ο καθένας μπορεί να βρει αυτό το δωμάτιο. -Μπορείς να το αλλάξεις ανά πάσα στιγμή στις ρυθμίσεις δωματίου." - "Δημόσιο δωμάτιο" - "Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτό το δωμάτιο" + "Προέκυψε σφάλμα κατά τη δημιουργία της αίθουσας" + "Μόνο τα άτομα που έχουν προσκληθεί μπορούν να έχουν πρόσβαση σε αυτή την αίθουσα. Όλα τα μηνύματα είναι κρυπτογραφημένα από άκρο σε άκρο." + "Ιδιωτική αίθουσα" + "Ο καθένας μπορεί να βρει αυτή την αίθουσα. +Αυτό μπορείτε να το αλλάξετε ανά πάσα στιγμή στις ρυθμίσεις της αίθουσας." + "Δημόσια αίθουσα" + "Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτή την αίθουσα" "Οποιοσδήποτε" - "Πρόσβαση Δωματίου" - "Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα" + "Πρόσβαση στην Αίθουσα" + "Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στην αίθουσα, αλλά ένας διαχειριστής ή ένας συντονιστής θα πρέπει να αποδεχτεί το αίτημα" "Αίτημα συμμετοχής" - "Για να είναι ορατό αυτό το δωμάτιο στον κατάλογο των δημόσιων δωματίων, θα χρειαστείς μια διεύθυνση δωματίου." - "Διεύθυνση δωματίου" - "Όνομα δωματίου" - "Ορατότητα δωματίου" - "Δημιούργησε ένα δωμάτιο" + "Για να είναι ορατή αυτή η αίθουσα στον δημόσιο κατάλογο αιθουσών, θα χρειαστείτε μια διεύθυνση αίθουσας." + "Διεύθυνση αίθουσας" + "Όνομα αίθουσας" + "Ορατότητα αίθουσας" + "Δημιουργία αίθουσας" "Θέμα (προαιρετικό)" + "Κατάλογος αιθουσών" "Παρουσιάστηκε σφάλμα κατά την προσπάθεια έναρξης μιας συνομιλίας" + "Συμμετοχή σε αίθουσα μέσω διεύθυνσης" + "Μη έγκυρη διεύθυνση" + "Εισάγετε…" + "Βρέθηκε η αντίστοιχη αίθουσα" + "Η αίθουσα δεν βρέθηκε" + "π.χ. #όνομα-αίθουσας:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml index 5946dab1024..c11f08fc141 100644 --- a/features/createroom/impl/src/main/res/values-es/translations.xml +++ b/features/createroom/impl/src/main/res/values-es/translations.xml @@ -3,12 +3,28 @@ "Nueva sala" "Invitar personas" "Se ha producido un error al crear la sala" - "Los mensajes de esta sala están cifrados. La encriptación no se puede desactivar después." - "Sala privada (sólo con invitación)" - "Los mensajes no están cifrados y cualquiera puede leerlos. Puedes activar la encriptación más adelante." - "Sala pública (cualquiera)" + "Solo las personas invitadas pueden acceder a esta sala. Todos los mensajes están cifrados de extremo a extremo." + "Sala privada" + "Cualquiera puede encontrar esta sala. +Puedes cambiar esto en cualquier momento en los ajustes de la sala." + "Sala pública" + "Cualquiera puede unirse a esta sala" + "Cualquiera" + "Acceso a la sala" + "Cualquiera puede solicitar unirse a la sala, pero un administrador o un moderador tendrá que aceptar la solicitud" + "Solicitud para unirse" + "Para que esta sala sea visible en el directorio de salas públicas, necesitarás una dirección de sala." + "Dirección de la sala" "Nombre de la sala" + "Visibilidad de la sala" "Crear una sala" "Tema (opcional)" + "Directorio de salas" "Se ha producido un error al intentar iniciar un chat" + "Unirse a una sala por su dirección" + "Dirección no válida" + "Introducir…" + "Sala encontrada" + "No se encontró la sala" + "p. ej., #nombre-de-la-sala:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-et/translations.xml b/features/createroom/impl/src/main/res/values-et/translations.xml index 67db887f2c6..57a26106b3f 100644 --- a/features/createroom/impl/src/main/res/values-et/translations.xml +++ b/features/createroom/impl/src/main/res/values-et/translations.xml @@ -19,5 +19,12 @@ Sa võid seda jututoa seadistustest alati muuta." "Jututoa nähtavus" "Loo jututuba" "Teema (kui soovid lisada)" + "Jututubade kataloog" "Vestluse alustamisel tekkis viga" + "Liitu jututoaga aadressi alusel" + "See pole kehtiv aadress" + "Sisene…" + "Leidsime vastava jututoa" + "Jututuba ei leidu" + "nt. #jututoa-nimi:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-eu/translations.xml b/features/createroom/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..43f67e429c1 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,23 @@ + + + "Gela berria" + "Gonbidatu jendea" + "Errorea gertatu da gela sortzean" + "Gonbidatutako jendea soilik sar daiteke gelara. Mezu guztiak daude ertzetik ertzera zifratuta." + "Gela pribatua" + "Edonork aurki dezake gela hau. +Gelaren ezarpenetan aldatu dezakezu hobespena." + "Gela publikoa" + "Edonor sar daiteke gela honetara" + "Edonork" + "Gelarako sarbidea" + "Gelaren helbidea" + "Gelaren izena" + "Gelaren ikusgarritasuna" + "Sortu gela" + "Mintzagaia (aukerakoa)" + "Gelen direktorioa" + "Errorea gertatu da txata hasten saiatzean" + "Sartu…" + "Ez da gela aurkitu" + 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 467fae53271..03bc5c3f58e 100644 --- a/features/createroom/impl/src/main/res/values-fa/translations.xml +++ b/features/createroom/impl/src/main/res/values-fa/translations.xml @@ -3,11 +3,26 @@ "اتاق جدید" "دعوت افراد" "هنگام ایجاد اتاق خطایی رخ داد" - "پیام‌های این اتاق رمز شده‌اند. رمزنگاری نمی‌تواند از این پس تغییر کند." - "اتاق خصوصی (فقط دعوت)" - "پیام‌ها رمزنگاری نشده و هرکسی می‌تواند بخواندشان. می‌توانید بعداً رمزنگاری را به کار بیندازید." - "اتاق عمومی (هرکسی)" + "تنها افراد دعوت شده می‌توانند به این اتاق دسترسی داشته باشند. همهٔ پیام‌ها رمزنگاری سرتاسری شده‌اند." + "اتاق خصوصی" + "هرکسی می‌تواند اتاق را بیابد. +می‌توانید بعداً در تظیمات اتاق عوضش کنید." + "اتاق عمومی" + "هرکسی می‌تواند به این اتاق بپیوندد" + "هرکسی" + "دسترسی اتاق" + "درخواست دعوت" + "نشانی اتاق" "نام اتاق" + "نمایانی اتاق" "ایجاد اتاق" "موضوع (اختیاری)" + "فهرست اتاق‌ها" + "هنگام تلاش برای شروع چت خطایی روی داد" + "پیوستن به اتاق با نشانی" + "نشانی معتبری نیست" + "ورود…" + "اتاق مطابق پیدا شد" + "اتاق پیدا نشد" + "نمونه: ‪#room-name:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-fi/translations.xml b/features/createroom/impl/src/main/res/values-fi/translations.xml index 39b0d54646d..cdb6d04d598 100644 --- a/features/createroom/impl/src/main/res/values-fi/translations.xml +++ b/features/createroom/impl/src/main/res/values-fi/translations.xml @@ -19,5 +19,12 @@ Voit muuttaa tämän milloin tahansa huoneen asetuksista." "Huoneen näkyvyys" "Luo huone" "Aihe (valinnainen)" + "Huoneluettelo" "Keskustelun aloituksessa tapahtui virhe" + "Liity huoneeseen osoitteella" + "Osoite ei ole kelvollinen" + "Syötä…" + "Täsmäävä huone löytyi" + "Huonetta ei löytynyt" + "esim. #huoneen-nimi:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-fr/translations.xml b/features/createroom/impl/src/main/res/values-fr/translations.xml index af40d1f9fef..3f9e01dc85a 100644 --- a/features/createroom/impl/src/main/res/values-fr/translations.xml +++ b/features/createroom/impl/src/main/res/values-fr/translations.xml @@ -19,5 +19,12 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon.""Visibilité du salon" "Créer un salon" "Sujet (facultatif)" + "Annuaire des salons" "Une erreur s’est produite lors de la tentative de création de la discussion" + "Saisir une adresse de salon" + "Ce n’est pas une adresse valide" + "Saisir…" + "Ce salon existe" + "Salon non trouvé" + "ex: #nom-du-salon:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-hu/translations.xml b/features/createroom/impl/src/main/res/values-hu/translations.xml index ee935c8c60e..1e4a7212793 100644 --- a/features/createroom/impl/src/main/res/values-hu/translations.xml +++ b/features/createroom/impl/src/main/res/values-hu/translations.xml @@ -14,10 +14,17 @@ Ezt bármikor módosíthatja a szobabeállításokban." "Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést" "Csatlakozás kérése" "Ahhoz, hogy ez a szoba látható legyen a nyilvános szobák címtárában, meg kell adnia a szoba címét." - "Szoba címe" + "A szoba címe" "Szoba neve" "Szoba láthatósága" "Szoba létrehozása" "Téma (nem kötelező)" + "Szobakatalógus" "Hiba történt a csevegés indításakor" + "Csatlakozás a szobához cím szerint" + "Nem érvényes cím" + "Írja be…" + "Megfelelő szoba található" + "Szoba nem található" + "pl. #szoba-neve:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-in/translations.xml b/features/createroom/impl/src/main/res/values-in/translations.xml index b1c7aeef1cf..65f135e617e 100644 --- a/features/createroom/impl/src/main/res/values-in/translations.xml +++ b/features/createroom/impl/src/main/res/values-in/translations.xml @@ -19,5 +19,12 @@ Anda dapat mengubah ini kapan pun dalam pengaturan ruangan." "Keterlihatan ruangan" "Buat ruangan" "Topik (opsional)" + "Direktori ruangan" "Terjadi kesalahan saat mencoba memulai obrolan" + "Bergabung dalam ruangan berdasarkan alamat" + "Bukan alamat yang valid" + "Masuk…" + "Ruangan yang cocok ditemukan" + "Ruangan tidak ditemukan" + "mis. #nama-ruangan:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml index 9cc30fc0e65..701ebc967f0 100644 --- a/features/createroom/impl/src/main/res/values-it/translations.xml +++ b/features/createroom/impl/src/main/res/values-it/translations.xml @@ -19,5 +19,12 @@ Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza." "Visibilità della stanza" "Crea una stanza" "Argomento (facoltativo)" + "Elenco delle stanze" "Si è verificato un errore durante il tentativo di avviare una chat" + "Accedi alla stanza tramite indirizzo" + "Indirizzo non valido" + "Inserisci…" + "Stanza trovata" + "Stanza non trovata" + "ad esempio #room -name:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-ka/translations.xml b/features/createroom/impl/src/main/res/values-ka/translations.xml index 45e87809b25..bb4a4303209 100644 --- a/features/createroom/impl/src/main/res/values-ka/translations.xml +++ b/features/createroom/impl/src/main/res/values-ka/translations.xml @@ -7,9 +7,9 @@ "კერძო ოთახი" "ყველას ამ ოთახის მოძებნა შეუძლია. თქვენ ნებისმიერ დროს შეგიძლიათ ამის შეცვლა ოთახის პარამეტრებში." - "საჯარო ოთახი" "ოთახის სახელი" "ოთახის შექმნა" "თემა (სურვილისამებრ)" + "ოთახის კატალოგი" "ჩატის დაწყების მცდელობისას შეცდომა მოხდა" diff --git a/features/createroom/impl/src/main/res/values-lt/translations.xml b/features/createroom/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..2f7a4438ea7 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,14 @@ + + + "Naujas kambarys" + "Pakviesti žmonių" + "Kuriant kambarį įvyko klaida" + "Į šį kambarį gali patekti tik pakviesti žmonės. Visi pranešimai yra užšifruoti nuo pradžios iki galo." + "Privatus kambarys" + "Bet kas gali rasti šį kambarį. +Tai galite bet kada pakeisti kambario nustatymuose." + "Kambario pavadinimas" + "Kurti kambarį" + "Tema (nebūtina)" + "Bandant pradėti pokalbį įvyko klaida" + diff --git a/features/createroom/impl/src/main/res/values-nb/translations.xml b/features/createroom/impl/src/main/res/values-nb/translations.xml index 4692daad04b..9266993fd4e 100644 --- a/features/createroom/impl/src/main/res/values-nb/translations.xml +++ b/features/createroom/impl/src/main/res/values-nb/translations.xml @@ -8,8 +8,23 @@ "Alle kan finne dette rommet. Du kan endre dette når som helst i rominnstillingene." "Offentlig rom" + "Alle kan bli med i dette rommet" + "Alle" + "Tilgang til rom" + "Alle kan be om å få bli med i rommet, men en administrator eller moderator må godta forespørselen" + "Be om å bli med" + "For at dette rommet skal være synlig i den offentlige romkatalogen, trenger du en romadresse." + "Romadresse" "Romnavn" + "Romsynlighet" "Opprett et rom" "Emne (valgfritt)" + "Romkatalog" "Det oppstod en feil når du prøvde å starte en chat" + "Bli med i rommet med adresse" + "Ikke en gyldig adresse" + "Gå inn…" + "Matchende rom funnet" + "Rom ikke funnet" + "f.eks. #rom-navn:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-nl/translations.xml b/features/createroom/impl/src/main/res/values-nl/translations.xml index c1f0fd92f5a..29d070a2638 100644 --- a/features/createroom/impl/src/main/res/values-nl/translations.xml +++ b/features/createroom/impl/src/main/res/values-nl/translations.xml @@ -3,7 +3,7 @@ "Nieuwe kamer" "Mensen uitnodigen" "Er is een fout opgetreden bij het aanmaken van de kamer" - "Berichten in deze kamer zijn versleuteld. Versleuteling kan achteraf niet worden uitgeschakeld." + "Alleen uitgenodigde personen hebben toegang tot deze kamer. Alle berichten zijn end-to-end versleuteld." "Privé kamer" "Iedereen kan deze kamer vinden. Je kunt dit op elk gewenst moment wijzigen in de kamerinstellingen." @@ -16,5 +16,6 @@ Je kunt dit op elk gewenst moment wijzigen in de kamerinstellingen." "Naam van de kamer" "Creëer een kamer" "Onderwerp (optioneel)" + "Kamergids" "Er is een fout opgetreden bij het starten van een chat" 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 7902fb4c3c8..3ed917b7bb9 100644 --- a/features/createroom/impl/src/main/res/values-pl/translations.xml +++ b/features/createroom/impl/src/main/res/values-pl/translations.xml @@ -19,5 +19,12 @@ Możesz to zmienić w ustawieniach pokoju." "Widoczność pomieszczenia" "Utwórz pokój" "Temat (opcjonalnie)" + "Katalog pokoi" "Wystąpił błąd podczas próby rozpoczęcia czatu" + "Dołącz do pokoju za pomocą adresu" + "Nieprawidłowy adres" + "Wprowadź…" + "Znaleziono pasujący pokój" + "Nie znaleziono pokoju" + "np. #room-name:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml b/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml index b10b86f5a07..52c9f569af5 100644 --- a/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/createroom/impl/src/main/res/values-pt-rBR/translations.xml @@ -4,12 +4,27 @@ "Convidar pessoas" "Ocorreu um erro ao criar a sala" "Apenas as pessoas convidadas podem aceder a esta sala. Todas as mensagens são criptografadas de ponta a ponta." - "Sala privativa (somente por convite)" + "Sala privada" "Qualquer um pode encontrar esta sala. Você pode mudar isso a qualquer momento nas configurações da sala." - "Sala pública (qualquer pessoa)" + "Sala pública" + "Qualquer pessoa pode entrar nesta sala" + "Qualquer pessoa" + "Acesso à sala" + "Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou moderador terá de aceitar a solicitação" + "Pedir para entrar" + "Para que esta sala fique visível no diretório público de salas, você precisará de um endereço de sala." + "Endereço da sala" "Nome da sala" + "Visibilidade da sala" "Criar uma sala" "Tópico (opcional)" + "Diretório de salas" "Ocorreu um erro ao tentar iniciar um chat" + "Entrar na sala pelo endereço" + "Não é um endereço válido" + "Entrar…" + "Foi encontrada uma sala correspondente" + "Sala não encontrada" + "Por exemplo, #nome-da-sala:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-pt/translations.xml b/features/createroom/impl/src/main/res/values-pt/translations.xml index 60ee753b133..5e5f5fc9c3f 100644 --- a/features/createroom/impl/src/main/res/values-pt/translations.xml +++ b/features/createroom/impl/src/main/res/values-pt/translations.xml @@ -3,7 +3,7 @@ "Nova sala" "Convidar pessoas" "Ocorreu um erro ao criar a sala" - "Apenas as pessoas convidadas podem aceder a esta sala. Todas as mensagens são encriptadas ponta a ponta." + "Apenas as pessoas convidadas podem aceder a esta sala. Todas as mensagens são cifradas ponta-a-ponta." "Sala privada" "Qualquer um pode encontrar esta sala. Pode alterar esta opção nas definições da sala." @@ -19,5 +19,12 @@ Pode alterar esta opção nas definições da sala." "Visibilidade da sala" "Criar uma sala" "Descrição (opcional)" + "Diretório de salas" "Ocorreu um erro ao tentar iniciar uma conversa" + "Entrar na sala pelo endereço" + "Não é um endereço válido" + "Entrar…" + "Sala correspondente encontrado" + "Sala não encontrada" + "por exemplo, #sala:matrix.org" 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 329ca6de1c9..8d9e74b5307 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -14,9 +14,9 @@ Puteți modifica acest lucru oricând în setări." "Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte solicitarea" "Cereți să vă alăturați" "Pentru ca această cameră să fie vizibilă în directorul de camere publice, veți avea nevoie de o adresă de cameră." - "Adresa camerei" "Numele camerei" "Creați o cameră" "Subiect (opțional)" + "Director de camere" "A apărut o eroare la încercarea începerii conversației" diff --git a/features/createroom/impl/src/main/res/values-ru/translations.xml b/features/createroom/impl/src/main/res/values-ru/translations.xml index b21bcb68acd..6344491248c 100644 --- a/features/createroom/impl/src/main/res/values-ru/translations.xml +++ b/features/createroom/impl/src/main/res/values-ru/translations.xml @@ -19,5 +19,12 @@ "Видимость комнаты" "Создать комнату" "Тема (необязательно)" + "Каталог комнат" "Произошла ошибка при запуске чата" + "Присоединиться к комнате по адресу" + "Недействительный адрес" + "Ввести…" + "Соответствующая комната найдена" + "Комната не найдена" + "прим. #room-name:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-sk/translations.xml b/features/createroom/impl/src/main/res/values-sk/translations.xml index d2d742239f6..eaf6c2f19c3 100644 --- a/features/createroom/impl/src/main/res/values-sk/translations.xml +++ b/features/createroom/impl/src/main/res/values-sk/translations.xml @@ -19,5 +19,12 @@ Môžete to kedykoľvek zmeniť v nastaveniach miestnosti." "Viditeľnosť miestnosti" "Vytvoriť miestnosť" "Téma (voliteľné)" + "Adresár miestností" "Pri pokuse o spustenie konverzácie sa vyskytla chyba" + "Pripojte sa do miestnosti podľa adresy" + "Neplatná adresa" + "Zadajte…" + "Nájdená zodpovedajúca miestnosť" + "Miestnosť sa nenašla" + "napr. #nazov-miestnosti:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-sv/translations.xml b/features/createroom/impl/src/main/res/values-sv/translations.xml index 980d6ae6435..316c9cc32e4 100644 --- a/features/createroom/impl/src/main/res/values-sv/translations.xml +++ b/features/createroom/impl/src/main/res/values-sv/translations.xml @@ -13,8 +13,18 @@ Du kan ändra detta när som helst i rumsinställningarna." "Rumsåtkomst" "Vem som helst kan be om att gå med i rummet men en administratör eller en moderator måste acceptera begäran" "Be om att gå med" + "För att detta rum ska vara synligt i den allmänna rumskatalogen behöver du en rumsadress." + "Rumsadress" "Rumsnamn" + "Rumssynlighet" "Skapa ett rum" "Ämne (valfritt)" + "Rumskatalog" "Ett fel uppstod när du försökte starta en chatt" + "Gå med i rum med adress" + "Inte en giltig adress" + "Ange …" + "Matchande rum hittades" + "Rummet hittades inte" + "t.ex. #rumsnamn:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-tr/translations.xml b/features/createroom/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..c8e31365171 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,24 @@ + + + "Yeni oda" + "İnsanları davet et" + "Oda oluşturulurken bir hata oluştu" + "Bu odaya yalnızca davet edilen kişiler erişebilir. Tüm mesajlar uçtan uca şifrelenir." + "Özel oda" + "Bu odayı herkes bulabilir. +Bunu istediğiniz zaman oda ayarlarından değiştirebilirsiniz." + "Herkese açık oda" + "Bu odaya herkes katılabilir" + "Herkes" + "Oda Erişimi" + "Herkes odaya katılmayı isteyebilir ancak bir yönetici veya moderatörün isteği kabul etmesi gerekecektir" + "Katılmak için sor" + "Bu odanın genel oda dizininde görünür olması için bir oda adresine ihtiyacınız olacaktır." + "Oda adresi" + "Oda adı" + "Oda görünürlüğü" + "Bir oda oluştur" + "Konu (isteğe bağlı)" + "Oda dizini" + "Sohbet başlatmaya çalışırken bir hata oluştu" + 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 a6ad600672f..a43172fb8b5 100644 --- a/features/createroom/impl/src/main/res/values-uk/translations.xml +++ b/features/createroom/impl/src/main/res/values-uk/translations.xml @@ -7,7 +7,7 @@ "Приватна кімната (тільки за запрошенням)" "Будь-хто може знайти цю кімнату. Ви можете змінити це в будь-який час у налаштуваннях кімнати." - "Публічна кімната" + "Загальнодоступна кімната" "Будь-хто може приєднатися до цієї кімнати" "Кожний" "Доступ до кімнати" @@ -19,5 +19,12 @@ "Видимість кімнати" "Створити кімнату" "Тема (необов\'язково)" + "Каталог кімнат" "Під час спроби почати бесіду сталася помилка" + "Приєднатися до кімнати за адресою" + "Недійсна адреса" + "Введіть…" + "Знайдено відповідну кімнату" + "Кімната не знайдена" + "наприклад, #room-name:matrix.org" diff --git a/features/createroom/impl/src/main/res/values-ur/translations.xml b/features/createroom/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..6fe476462ed --- /dev/null +++ b/features/createroom/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,16 @@ + + + "نیا کمرہ" + "لوگوں کو مدعو کریں" + "کمرہ تخلیق کرتے ہوئے ایک نقص واقع ہوا" + "صرف مدعو لوگ ہی اس کمرے تک رسائی حاصل کر سکتے ہیں۔ تمام پیغامات آخر تا آخر مرموز کردہ ہیں۔" + "نجی کمرہ" + "کوئی بھی یہ کمرہ ڈھونڈ سکتا ہے۔ +آپ اسے کمرے کی ترتیبات میں کسی بھی وقت تبدیل کرسکتے ہیں۔" + "عوامی کمرہ" + "کمرے کا نام" + "ایک کمرہ بنائیں" + "موضوع (اختیاری)" + "کمرے کا راہنامچہ" + "گفتگو شروع کرنے کی کوشش کرتے وقت ایک خرابی واقع ہوگئی" + 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 649bdf86132..e3bc9de5288 100644 --- a/features/createroom/impl/src/main/res/values-uz/translations.xml +++ b/features/createroom/impl/src/main/res/values-uz/translations.xml @@ -6,7 +6,6 @@ "Bu xonadagi xabarlar shifrlangan. Keyinchalik shifrlashni o‘chirib bo‘lmaydi." "Shaxsiy xona (faqat taklif)" "Xabarlar shifrlanmagan va har kim ularni o\'qiy oladi. Keyinchalik shifrlashni yoqishingiz mumkin." - "Jamoat xonasi (har kim)" "Xona nomi" "Xonani yaratish" "Mavzu (ixtiyoriy)" 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 c94c35ad5b3..0d67c883c7b 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 @@ -7,7 +7,7 @@ "私密聊天室" "任何人都可以找到此聊天室。 您隨時都可以在聊天室設定中變更此設定。" - "公開聊天室" + "公開的聊天室" "任何人都可以加入此聊天室" "任何人" "聊天室存取權" @@ -19,5 +19,12 @@ "聊天室能見度" "建立聊天室" "主題(非必填)" + "聊天室目錄" "嘗試開始聊天時發生錯誤" + "按地址加入聊天室" + "不是有效的位址" + "輸入……" + "找到相符的聊天室" + "找不到聊天室" + "例如 #room-name:matrix.org" 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 0049da0bbbf..24d7df7f353 100644 --- a/features/createroom/impl/src/main/res/values-zh/translations.xml +++ b/features/createroom/impl/src/main/res/values-zh/translations.xml @@ -7,7 +7,7 @@ "私有聊天室" "任何人都能找到此聊天室。 你可以随时在聊天室设置中更改。" - "公开聊天室" + "公共聊天室" "任何人都可以加入此房间" "任何人" "房间访问权限" @@ -19,5 +19,6 @@ "房间可见性" "创建聊天室" "主题(可选)" + "聊天室目录" "在开始聊天时发生了错误" diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml index 6ed5510ce0d..d011a1d87b0 100644 --- a/features/createroom/impl/src/main/res/values/localazy.xml +++ b/features/createroom/impl/src/main/res/values/localazy.xml @@ -19,5 +19,12 @@ You can change this anytime in room settings." "Room visibility" "Create a room" "Topic (optional)" + "Room directory" "An error occurred when trying to start a chat" + "Join room by address" + "Not a valid address" + "Enter…" + "Matching room found" + "Room not found" + "e.g. #room-name:matrix.org" diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTest.kt index dabdc288db8..ef6b3b95170 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTest.kt @@ -10,13 +10,14 @@ package io.element.android.features.createroom.impl import androidx.compose.runtime.mutableStateOf import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.CreatedRoom +import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +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_THROWABLE -import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.test.runTest @@ -26,38 +27,70 @@ class DefaultStartDMActionTest { @Test fun `when dm is found, assert state is updated with given room id`() = runTest { val matrixClient = FakeMatrixClient().apply { - givenFindDmResult(A_ROOM_ID) + givenFindDmResult(Result.success(A_ROOM_ID)) } - val action = createStartDMAction(matrixClient) + val analyticsService = FakeAnalyticsService() + val action = createStartDMAction(matrixClient, analyticsService) val state = mutableStateOf>(AsyncAction.Uninitialized) - action.execute(A_USER_ID, state) + action.execute(aMatrixUser(), true, state) assertThat(state.value).isEqualTo(AsyncAction.Success(A_ROOM_ID)) + assertThat(analyticsService.capturedEvents).isEmpty() + } + + @Test + fun `when finding the dm fails, assert state is updated with given error`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenFindDmResult(Result.failure(AN_EXCEPTION)) + } + val analyticsService = FakeAnalyticsService() + val action = createStartDMAction(matrixClient, analyticsService) + val state = mutableStateOf>(AsyncAction.Uninitialized) + action.execute(aMatrixUser(), true, state) + assertThat(state.value).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) + assertThat(analyticsService.capturedEvents).isEmpty() } @Test fun `when dm is not found, assert dm is created, state is updated with given room id and analytics get called`() = runTest { val matrixClient = FakeMatrixClient().apply { - givenFindDmResult(null) + givenFindDmResult(Result.success(null)) givenCreateDmResult(Result.success(A_ROOM_ID)) } val analyticsService = FakeAnalyticsService() val action = createStartDMAction(matrixClient, analyticsService) val state = mutableStateOf>(AsyncAction.Uninitialized) - action.execute(A_USER_ID, state) + action.execute(aMatrixUser(), true, state) assertThat(state.value).isEqualTo(AsyncAction.Success(A_ROOM_ID)) assertThat(analyticsService.capturedEvents).containsExactly(CreatedRoom(isDM = true)) } + @Test + fun `when dm is not found, and createIfDmDoesNotExist is false, assert dm is not created and state is updated to confirmation state`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenFindDmResult(Result.success(null)) + givenCreateDmResult(Result.success(A_ROOM_ID)) + } + val analyticsService = FakeAnalyticsService() + val action = createStartDMAction(matrixClient, analyticsService) + val state = mutableStateOf>(AsyncAction.Uninitialized) + val matrixUser = aMatrixUser() + action.execute(matrixUser, false, state) + assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser)) + assertThat(analyticsService.capturedEvents).isEmpty() + } + @Test fun `when dm creation fails, assert state is updated with given error`() = runTest { val matrixClient = FakeMatrixClient().apply { - givenFindDmResult(null) - givenCreateDmResult(Result.failure(A_THROWABLE)) + givenFindDmResult(Result.success(null)) + givenCreateDmResult(Result.failure(AN_EXCEPTION)) } - val action = createStartDMAction(matrixClient) + val analyticsService = FakeAnalyticsService() + val action = createStartDMAction(matrixClient, analyticsService) val state = mutableStateOf>(AsyncAction.Uninitialized) - action.execute(A_USER_ID, state) - assertThat(state.value).isEqualTo(AsyncAction.Failure(A_THROWABLE)) + action.execute(aMatrixUser(), true, state) + assertThat(state.value).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) + assertThat(analyticsService.capturedEvents).isEmpty() } private fun createStartDMAction( diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/FakeCreateRoomNavigator.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/FakeCreateRoomNavigator.kt new file mode 100644 index 00000000000..28dcdd409d6 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/FakeCreateRoomNavigator.kt @@ -0,0 +1,39 @@ +/* + * 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.createroom.impl + +import io.element.android.features.createroom.CreateRoomNavigator +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias + +class FakeCreateRoomNavigator( + private val openRoomLambda: (roomIdOrAlias: RoomIdOrAlias, serverNames: List) -> Unit = { _, _ -> }, + private val createNewRoomLambda: () -> Unit = {}, + private val showJoinRoomByAddressLambda: () -> Unit = {}, + private val dismissJoinRoomByAddressLambda: () -> Unit = {}, + private val openRoomDirectoryLambda: () -> Unit = {}, +) : CreateRoomNavigator { + override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List) { + openRoomLambda(roomIdOrAlias, serverNames) + } + + override fun onCreateNewRoom() { + createNewRoomLambda() + } + + override fun onShowJoinRoomByAddress() { + showJoinRoomByAddressLambda() + } + + override fun onDismissJoinRoomByAddress() { + dismissJoinRoomByAddressLambda() + } + + override fun onOpenRoomDirectory() { + openRoomDirectoryLambda() + } +} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt new file mode 100644 index 00000000000..a0a4783093b --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureBaseRoomPresenterTest.kt @@ -0,0 +1,426 @@ +/* + * 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. + */ + +package io.element.android.features.createroom.impl.configureroom + +import android.net.Uri +import app.cash.turbine.TurbineTestContext +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.CreatedRoom +import io.element.android.features.createroom.impl.CreateRoomConfig +import io.element.android.features.createroom.impl.CreateRoomDataStore +import io.element.android.features.createroom.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.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_MESSAGE +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.media.AvatarAction +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity +import io.element.android.libraries.mediapickers.api.PickerProvider +import io.element.android.libraries.mediapickers.test.FakePickerProvider +import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.mediaupload.api.MediaUploadInfo +import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.io.File +import java.util.Optional + +private const val AN_URI_FROM_CAMERA = "content://uri_from_camera" +private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2" +private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery" + +@RunWith(RobolectricTestRunner::class) +class ConfigureBaseRoomPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Before + fun setup() { + mockkStatic(File::readBytes) + every { any().readBytes() } returns byteArrayOf() + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `present - initial state`() = runTest { + val presenter = createConfigureRoomPresenter() + presenter.test { + val initialState = initialState() + assertThat(initialState.config).isEqualTo(CreateRoomConfig()) + assertThat(initialState.config.roomName).isNull() + assertThat(initialState.config.topic).isNull() + assertThat(initialState.config.invites).isEmpty() + assertThat(initialState.config.avatarUri).isNull() + assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private) + assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(initialState.homeserverName).isEqualTo("matrix.org") + } + } + + @Test + fun `present - create room button is enabled only if the required fields are completed`() = runTest { + val presenter = createConfigureRoomPresenter() + presenter.test { + val initialState = initialState() + var config = initialState.config + assertThat(initialState.isValid).isFalse() + + // Room name not empty + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) + var newState: ConfigureRoomState = awaitItem() + config = config.copy(roomName = A_ROOM_NAME) + assertThat(newState.config).isEqualTo(config) + assertThat(newState.isValid).isTrue() + + // Clear room name + newState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) + newState = awaitItem() + config = config.copy(roomName = null) + assertThat(newState.config).isEqualTo(config) + assertThat(newState.isValid).isFalse() + } + } + + @Test + fun `present - state is updated when fields are changed`() = runTest { + val userListDataStore = UserListDataStore() + val pickerProvider = FakePickerProvider() + val permissionsPresenter = FakePermissionsPresenter() + val roomAliasHelper = FakeRoomAliasHelper() + val presenter = createConfigureRoomPresenter( + createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper), + pickerProvider = pickerProvider, + permissionsPresenter = permissionsPresenter, + ) + presenter.test { + val initialState = initialState() + var expectedConfig = CreateRoomConfig() + assertThat(initialState.config).isEqualTo(expectedConfig) + + // Select User + val selectedUser1 = aMatrixUser() + val selectedUser2 = aMatrixUser("@id_of_bob:server.org", "Bob") + userListDataStore.selectUser(selectedUser1) + skipItems(1) + userListDataStore.selectUser(selectedUser2) + var newState = awaitItem() + expectedConfig = expectedConfig.copy(invites = persistentListOf(selectedUser1, selectedUser2)) + assertThat(newState.config).isEqualTo(expectedConfig) + + // Room name + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) + newState = awaitItem() + expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME) + assertThat(newState.config).isEqualTo(expectedConfig) + + // Room topic + newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE)) + newState = awaitItem() + expectedConfig = expectedConfig.copy(topic = A_MESSAGE) + assertThat(newState.config).isEqualTo(expectedConfig) + + // Room avatar + // Pick avatar + pickerProvider.givenResult(null) + // From gallery + val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY) + pickerProvider.givenResult(uriFromGallery) + newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) + newState = awaitItem() + expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery) + assertThat(newState.config).isEqualTo(expectedConfig) + // From camera + val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA) + pickerProvider.givenResult(uriFromCamera) + assertThat(newState.cameraPermissionState.permissionGranted).isFalse() + newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) + newState = awaitItem() + assertThat(newState.cameraPermissionState.showDialog).isTrue() + permissionsPresenter.setPermissionGranted() + newState = awaitItem() + assertThat(newState.cameraPermissionState.permissionGranted).isTrue() + newState = awaitItem() + expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera) + assertThat(newState.config).isEqualTo(expectedConfig) + // Do it again, no permission is requested + val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2) + pickerProvider.givenResult(uriFromCamera2) + newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) + newState = awaitItem() + expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2) + assertThat(newState.config).isEqualTo(expectedConfig) + // Remove + newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove)) + newState = awaitItem() + expectedConfig = expectedConfig.copy(avatarUri = null) + assertThat(newState.config).isEqualTo(expectedConfig) + + // Room privacy + newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) + newState = awaitItem() + expectedConfig = expectedConfig.copy( + roomVisibility = RoomVisibilityState.Public( + roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")), + roomAccess = RoomAccess.Anyone, + ) + ) + assertThat(newState.config).isEqualTo(expectedConfig) + + // Remove user + newState.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(selectedUser1)) + newState = awaitItem() + expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList()) + assertThat(newState.config).isEqualTo(expectedConfig) + } + } + + @Test + fun `present - trigger create room action`() = runTest { + val matrixClient = createMatrixClient() + val presenter = createConfigureRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + val initialState = initialState() + val createRoomResult = Result.success(RoomId("!createRoomResult:domain")) + + matrixClient.givenCreateRoomResult(createRoomResult) + + initialState.eventSink(ConfigureRoomEvents.CreateRoom) + assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) + val stateAfterCreateRoom = awaitItem() + assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java) + assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull()) + } + } + + @Test + fun `present - record analytics when creating room`() = runTest { + val matrixClient = createMatrixClient() + val analyticsService = FakeAnalyticsService() + val presenter = createConfigureRoomPresenter( + matrixClient = matrixClient, + analyticsService = analyticsService + ) + presenter.test { + val initialState = initialState() + val createRoomResult = Result.success(RoomId("!createRoomResult:domain")) + + matrixClient.givenCreateRoomResult(createRoomResult) + + initialState.eventSink(ConfigureRoomEvents.CreateRoom) + skipItems(2) + + val analyticsEvent = analyticsService.capturedEvents.filterIsInstance().firstOrNull() + assertThat(analyticsEvent).isNotNull() + assertThat(analyticsEvent?.isDM).isFalse() + } + } + + @Test + fun `present - trigger create room with upload error and retry`() = runTest { + val matrixClient = createMatrixClient() + val analyticsService = FakeAnalyticsService() + val mediaPreProcessor = FakeMediaPreProcessor() + val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper()) + val presenter = createConfigureRoomPresenter( + createRoomDataStore = createRoomDataStore, + mediaPreProcessor = mediaPreProcessor, + matrixClient = matrixClient, + analyticsService = analyticsService + ) + presenter.test { + val initialState = initialState() + createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY)) + skipItems(1) + mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk()))) + matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION)) + + initialState.eventSink(ConfigureRoomEvents.CreateRoom) + assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) + val stateAfterCreateRoom = awaitItem() + assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java) + assertThat(analyticsService.capturedEvents.filterIsInstance()).isEmpty() + + matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL)) + stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom) + assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) + assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java) + } + } + + @Test + fun `present - trigger retry and cancel actions`() = runTest { + val fakeMatrixClient = createMatrixClient() + val presenter = createConfigureRoomPresenter( + matrixClient = fakeMatrixClient + ) + presenter.test { + val initialState = initialState() + val createRoomResult = Result.failure(AN_EXCEPTION) + + fakeMatrixClient.givenCreateRoomResult(createRoomResult) + + // Create + initialState.eventSink(ConfigureRoomEvents.CreateRoom) + assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) + val stateAfterCreateRoom = awaitItem() + assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java) + assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull()) + + // Retry + stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom) + assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) + val stateAfterRetry = awaitItem() + assertThat(stateAfterRetry.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java) + assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull()) + + // Cancel + stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom) + assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - address is invalid when format is invalid`() = runTest { + val aliasHelper = FakeRoomAliasHelper( + isRoomAliasValidLambda = { false } + ) + val presenter = createConfigureRoomPresenter( + roomAliasHelper = aliasHelper + ) + presenter.test { + val initialState = initialState() + initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) + skipItems(1) + initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address")) + skipItems(1) + advanceUntilIdle() + awaitItem().also { state -> + assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - address is not available when alias is not available`() = runTest { + val fakeMatrixClient = createMatrixClient(isAliasAvailable = false) + val presenter = createConfigureRoomPresenter( + matrixClient = fakeMatrixClient, + ) + presenter.test { + val initialState = initialState() + initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) + skipItems(1) + initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address")) + skipItems(1) + advanceUntilIdle() + awaitItem().also { state -> + assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - address is valid when alias is available and format is valid`() = runTest { + val fakeMatrixClient = createMatrixClient(isAliasAvailable = true) + val presenter = createConfigureRoomPresenter( + matrixClient = fakeMatrixClient, + ) + presenter.test { + val initialState = initialState() + initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) + skipItems(1) + initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address")) + skipItems(1) + advanceUntilIdle() + awaitItem().also { state -> + assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + } + } + } + + private suspend fun TurbineTestContext.initialState(): ConfigureRoomState { + skipItems(1) + return awaitItem() + } + + private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + resolveRoomAliasResult = { + val resolvedRoomAlias = if (isAliasAvailable) { + Optional.empty() + } else { + Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList())) + } + Result.success(resolvedRoomAlias) + } + ) + + private fun createConfigureRoomPresenter( + roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(), + createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper), + matrixClient: MatrixClient = createMatrixClient(), + pickerProvider: PickerProvider = FakePickerProvider(), + mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(), + analyticsService: AnalyticsService = FakeAnalyticsService(), + permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), + isKnockFeatureEnabled: Boolean = true, + ) = ConfigureRoomPresenter( + dataStore = createRoomDataStore, + matrixClient = matrixClient, + mediaPickerProvider = pickerProvider, + mediaPreProcessor = mediaPreProcessor, + analyticsService = analyticsService, + permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter), + roomAliasHelper = roomAliasHelper, + featureFlagService = FakeFeatureFlagService( + mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled) + ) + ) +} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt deleted file mode 100644 index 60698b8e981..00000000000 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTest.kt +++ /dev/null @@ -1,426 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.createroom.impl.configureroom - -import android.net.Uri -import app.cash.turbine.TurbineTestContext -import com.google.common.truth.Truth.assertThat -import im.vector.app.features.analytics.plan.CreatedRoom -import io.element.android.features.createroom.impl.CreateRoomConfig -import io.element.android.features.createroom.impl.CreateRoomDataStore -import io.element.android.features.createroom.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.MatrixClient -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias -import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper -import io.element.android.libraries.matrix.test.AN_AVATAR_URL -import io.element.android.libraries.matrix.test.A_MESSAGE -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_THROWABLE -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper -import io.element.android.libraries.matrix.ui.components.aMatrixUser -import io.element.android.libraries.matrix.ui.media.AvatarAction -import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity -import io.element.android.libraries.mediapickers.api.PickerProvider -import io.element.android.libraries.mediapickers.test.FakePickerProvider -import io.element.android.libraries.mediaupload.api.MediaPreProcessor -import io.element.android.libraries.mediaupload.api.MediaUploadInfo -import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor -import io.element.android.libraries.permissions.api.PermissionsPresenter -import io.element.android.libraries.permissions.test.FakePermissionsPresenter -import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory -import io.element.android.services.analytics.api.AnalyticsService -import io.element.android.services.analytics.test.FakeAnalyticsService -import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.test -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.io.File -import java.util.Optional - -private const val AN_URI_FROM_CAMERA = "content://uri_from_camera" -private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2" -private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery" - -@RunWith(RobolectricTestRunner::class) -class ConfigureRoomPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Before - fun setup() { - mockkStatic(File::readBytes) - every { any().readBytes() } returns byteArrayOf() - } - - @After - fun tearDown() { - unmockkAll() - } - - @Test - fun `present - initial state`() = runTest { - val presenter = createConfigureRoomPresenter() - presenter.test { - val initialState = initialState() - assertThat(initialState.config).isEqualTo(CreateRoomConfig()) - assertThat(initialState.config.roomName).isNull() - assertThat(initialState.config.topic).isNull() - assertThat(initialState.config.invites).isEmpty() - assertThat(initialState.config.avatarUri).isNull() - assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private) - assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(initialState.homeserverName).isEqualTo("matrix.org") - } - } - - @Test - fun `present - create room button is enabled only if the required fields are completed`() = runTest { - val presenter = createConfigureRoomPresenter() - presenter.test { - val initialState = initialState() - var config = initialState.config - assertThat(initialState.isValid).isFalse() - - // Room name not empty - initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) - var newState: ConfigureRoomState = awaitItem() - config = config.copy(roomName = A_ROOM_NAME) - assertThat(newState.config).isEqualTo(config) - assertThat(newState.isValid).isTrue() - - // Clear room name - newState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) - newState = awaitItem() - config = config.copy(roomName = null) - assertThat(newState.config).isEqualTo(config) - assertThat(newState.isValid).isFalse() - } - } - - @Test - fun `present - state is updated when fields are changed`() = runTest { - val userListDataStore = UserListDataStore() - val pickerProvider = FakePickerProvider() - val permissionsPresenter = FakePermissionsPresenter() - val roomAliasHelper = FakeRoomAliasHelper() - val presenter = createConfigureRoomPresenter( - createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper), - pickerProvider = pickerProvider, - permissionsPresenter = permissionsPresenter, - ) - presenter.test { - val initialState = initialState() - var expectedConfig = CreateRoomConfig() - assertThat(initialState.config).isEqualTo(expectedConfig) - - // Select User - val selectedUser1 = aMatrixUser() - val selectedUser2 = aMatrixUser("@id_of_bob:server.org", "Bob") - userListDataStore.selectUser(selectedUser1) - skipItems(1) - userListDataStore.selectUser(selectedUser2) - var newState = awaitItem() - expectedConfig = expectedConfig.copy(invites = persistentListOf(selectedUser1, selectedUser2)) - assertThat(newState.config).isEqualTo(expectedConfig) - - // Room name - initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) - newState = awaitItem() - expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME) - assertThat(newState.config).isEqualTo(expectedConfig) - - // Room topic - newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE)) - newState = awaitItem() - expectedConfig = expectedConfig.copy(topic = A_MESSAGE) - assertThat(newState.config).isEqualTo(expectedConfig) - - // Room avatar - // Pick avatar - pickerProvider.givenResult(null) - // From gallery - val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY) - pickerProvider.givenResult(uriFromGallery) - newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) - newState = awaitItem() - expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery) - assertThat(newState.config).isEqualTo(expectedConfig) - // From camera - val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA) - pickerProvider.givenResult(uriFromCamera) - assertThat(newState.cameraPermissionState.permissionGranted).isFalse() - newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) - newState = awaitItem() - assertThat(newState.cameraPermissionState.showDialog).isTrue() - permissionsPresenter.setPermissionGranted() - newState = awaitItem() - assertThat(newState.cameraPermissionState.permissionGranted).isTrue() - newState = awaitItem() - expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera) - assertThat(newState.config).isEqualTo(expectedConfig) - // Do it again, no permission is requested - val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2) - pickerProvider.givenResult(uriFromCamera2) - newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) - newState = awaitItem() - expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2) - assertThat(newState.config).isEqualTo(expectedConfig) - // Remove - newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove)) - newState = awaitItem() - expectedConfig = expectedConfig.copy(avatarUri = null) - assertThat(newState.config).isEqualTo(expectedConfig) - - // Room privacy - newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) - newState = awaitItem() - expectedConfig = expectedConfig.copy( - roomVisibility = RoomVisibilityState.Public( - roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")), - roomAccess = RoomAccess.Anyone, - ) - ) - assertThat(newState.config).isEqualTo(expectedConfig) - - // Remove user - newState.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(selectedUser1)) - newState = awaitItem() - expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList()) - assertThat(newState.config).isEqualTo(expectedConfig) - } - } - - @Test - fun `present - trigger create room action`() = runTest { - val matrixClient = createMatrixClient() - val presenter = createConfigureRoomPresenter( - matrixClient = matrixClient - ) - presenter.test { - val initialState = initialState() - val createRoomResult = Result.success(RoomId("!createRoomResult:domain")) - - matrixClient.givenCreateRoomResult(createRoomResult) - - initialState.eventSink(ConfigureRoomEvents.CreateRoom) - assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) - val stateAfterCreateRoom = awaitItem() - assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java) - assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull()) - } - } - - @Test - fun `present - record analytics when creating room`() = runTest { - val matrixClient = createMatrixClient() - val analyticsService = FakeAnalyticsService() - val presenter = createConfigureRoomPresenter( - matrixClient = matrixClient, - analyticsService = analyticsService - ) - presenter.test { - val initialState = initialState() - val createRoomResult = Result.success(RoomId("!createRoomResult:domain")) - - matrixClient.givenCreateRoomResult(createRoomResult) - - initialState.eventSink(ConfigureRoomEvents.CreateRoom) - skipItems(2) - - val analyticsEvent = analyticsService.capturedEvents.filterIsInstance().firstOrNull() - assertThat(analyticsEvent).isNotNull() - assertThat(analyticsEvent?.isDM).isFalse() - } - } - - @Test - fun `present - trigger create room with upload error and retry`() = runTest { - val matrixClient = createMatrixClient() - val analyticsService = FakeAnalyticsService() - val mediaPreProcessor = FakeMediaPreProcessor() - val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper()) - val presenter = createConfigureRoomPresenter( - createRoomDataStore = createRoomDataStore, - mediaPreProcessor = mediaPreProcessor, - matrixClient = matrixClient, - analyticsService = analyticsService - ) - presenter.test { - val initialState = initialState() - createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY)) - skipItems(1) - mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk()))) - matrixClient.givenUploadMediaResult(Result.failure(A_THROWABLE)) - - initialState.eventSink(ConfigureRoomEvents.CreateRoom) - assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) - val stateAfterCreateRoom = awaitItem() - assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java) - assertThat(analyticsService.capturedEvents.filterIsInstance()).isEmpty() - - matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL)) - stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom) - assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) - assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java) - } - } - - @Test - fun `present - trigger retry and cancel actions`() = runTest { - val fakeMatrixClient = createMatrixClient() - val presenter = createConfigureRoomPresenter( - matrixClient = fakeMatrixClient - ) - presenter.test { - val initialState = initialState() - val createRoomResult = Result.failure(A_THROWABLE) - - fakeMatrixClient.givenCreateRoomResult(createRoomResult) - - // Create - initialState.eventSink(ConfigureRoomEvents.CreateRoom) - assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) - val stateAfterCreateRoom = awaitItem() - assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java) - assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull()) - - // Retry - stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom) - assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) - val stateAfterRetry = awaitItem() - assertThat(stateAfterRetry.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java) - assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull()) - - // Cancel - stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom) - assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun `present - address is invalid when format is invalid`() = runTest { - val aliasHelper = FakeRoomAliasHelper( - isRoomAliasValidLambda = { false } - ) - val presenter = createConfigureRoomPresenter( - roomAliasHelper = aliasHelper - ) - presenter.test { - val initialState = initialState() - initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) - skipItems(1) - initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address")) - skipItems(1) - advanceUntilIdle() - awaitItem().also { state -> - assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols) - } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun `present - address is not available when alias is not available`() = runTest { - val fakeMatrixClient = createMatrixClient(isAliasAvailable = false) - val presenter = createConfigureRoomPresenter( - matrixClient = fakeMatrixClient, - ) - presenter.test { - val initialState = initialState() - initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) - skipItems(1) - initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address")) - skipItems(1) - advanceUntilIdle() - awaitItem().also { state -> - assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable) - } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun `present - address is valid when alias is available and format is valid`() = runTest { - val fakeMatrixClient = createMatrixClient(isAliasAvailable = true) - val presenter = createConfigureRoomPresenter( - matrixClient = fakeMatrixClient, - ) - presenter.test { - val initialState = initialState() - initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public)) - skipItems(1) - initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address")) - skipItems(1) - advanceUntilIdle() - awaitItem().also { state -> - assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) - } - } - } - - private suspend fun TurbineTestContext.initialState(): ConfigureRoomState { - skipItems(1) - return awaitItem() - } - - private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient( - userIdServerNameLambda = { "matrix.org" }, - resolveRoomAliasResult = { - val resolvedRoomAlias = if (isAliasAvailable) { - Optional.empty() - } else { - Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList())) - } - Result.success(resolvedRoomAlias) - } - ) - - private fun createConfigureRoomPresenter( - roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(), - createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper), - matrixClient: MatrixClient = createMatrixClient(), - pickerProvider: PickerProvider = FakePickerProvider(), - mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(), - analyticsService: AnalyticsService = FakeAnalyticsService(), - permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), - isKnockFeatureEnabled: Boolean = true, - ) = ConfigureRoomPresenter( - dataStore = createRoomDataStore, - matrixClient = matrixClient, - mediaPickerProvider = pickerProvider, - mediaPreProcessor = mediaPreProcessor, - analyticsService = analyticsService, - permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter), - roomAliasHelper = roomAliasHelper, - featureFlagService = FakeFeatureFlagService( - mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled) - ) - ) -} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressPresenterTest.kt new file mode 100644 index 00000000000..10cca20fa35 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressPresenterTest.kt @@ -0,0 +1,140 @@ +/* + * 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.createroom.impl.joinbyaddress + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.createroom.CreateRoomNavigator +import io.element.android.features.createroom.impl.FakeCreateRoomNavigator +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper +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.runTest +import org.junit.Test + +class JoinBaseRoomByAddressPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createJoinRoomByAddressPresenter() + presenter.test { + with(awaitItem()) { + assertThat(address).isEmpty() + assertThat(addressState).isEqualTo(RoomAddressState.Unknown) + } + } + } + + @Test + fun `present - invalid address`() = runTest { + val presenter = createJoinRoomByAddressPresenter( + roomAliasHelper = FakeRoomAliasHelper( + isRoomAliasValidLambda = { false } + ) + ) + presenter.test { + with(awaitItem()) { + eventSink(JoinRoomByAddressEvents.UpdateAddress("invalid_address")) + } + with(awaitItem()) { + assertThat(address).isEqualTo("invalid_address") + assertThat(addressState).isEqualTo(RoomAddressState.Unknown) + eventSink(JoinRoomByAddressEvents.Continue) + } + // The address should be marked as invalid only after the user tries to continue + with(awaitItem()) { + assertThat(address).isEqualTo("invalid_address") + assertThat(addressState).isEqualTo(RoomAddressState.Invalid) + } + } + } + + @Test + fun `present - room found`() = runTest { + val openRoomLambda = lambdaRecorder, Unit> { _, _ -> } + val dismissJoinRoomByAddressLambda = lambdaRecorder { } + val navigator = FakeCreateRoomNavigator( + openRoomLambda = openRoomLambda, + dismissJoinRoomByAddressLambda = dismissJoinRoomByAddressLambda + ) + val presenter = createJoinRoomByAddressPresenter(navigator = navigator) + presenter.test { + with(awaitItem()) { + eventSink(JoinRoomByAddressEvents.UpdateAddress("#room_found:matrix.org")) + } + with(awaitItem()) { + assertThat(address).isEqualTo("#room_found:matrix.org") + assertThat(addressState).isEqualTo(RoomAddressState.Unknown) + } + with(awaitItem()) { + assertThat(address).isEqualTo("#room_found:matrix.org") + assertThat(addressState).isInstanceOf(RoomAddressState.RoomFound::class.java) + eventSink(JoinRoomByAddressEvents.Continue) + } + assert(openRoomLambda).isCalledOnce() + assert(dismissJoinRoomByAddressLambda).isCalledOnce() + } + } + + @Test + fun `present - room not found`() = runTest { + val presenter = createJoinRoomByAddressPresenter( + matrixClient = FakeMatrixClient( + resolveRoomAliasResult = { Result.failure(RuntimeException()) } + ) + ) + presenter.test { + with(awaitItem()) { + eventSink(JoinRoomByAddressEvents.UpdateAddress("#room_not_found:matrix.org")) + } + with(awaitItem()) { + assertThat(address).isEqualTo("#room_not_found:matrix.org") + assertThat(addressState).isEqualTo(RoomAddressState.Unknown) + eventSink(JoinRoomByAddressEvents.Continue) + } + with(awaitItem()) { + assertThat(address).isEqualTo("#room_not_found:matrix.org") + assertThat(addressState).isEqualTo(RoomAddressState.Resolving) + } + with(awaitItem()) { + assertThat(address).isEqualTo("#room_not_found:matrix.org") + assertThat(addressState).isEqualTo(RoomAddressState.RoomNotFound) + } + } + } + + @Test + fun `present - dismiss`() = runTest { + val dismissJoinRoomByAddressLambda = lambdaRecorder { } + val navigator = FakeCreateRoomNavigator( + dismissJoinRoomByAddressLambda = dismissJoinRoomByAddressLambda + ) + val presenter = createJoinRoomByAddressPresenter(navigator = navigator) + presenter.test { + with(awaitItem()) { + eventSink(JoinRoomByAddressEvents.Dismiss) + } + assert(dismissJoinRoomByAddressLambda).isCalledOnce() + } + } + + private fun createJoinRoomByAddressPresenter( + navigator: CreateRoomNavigator = FakeCreateRoomNavigator(), + matrixClient: MatrixClient = FakeMatrixClient(), + roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(), + ): JoinRoomByAddressPresenter { + return JoinRoomByAddressPresenter( + navigator = navigator, + client = matrixClient, + roomAliasHelper = roomAliasHelper, + ) + } +} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt new file mode 100644 index 00000000000..b2e75c9f5a2 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt @@ -0,0 +1,63 @@ +/* + * 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.createroom.impl.joinbyaddress + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.createroom.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`() { + val eventsRecorder = EventsRecorder() + rule.setJoinRoomByAddressView( + aJoinRoomByAddressState( + eventSink = eventsRecorder, + ) + ) + val text = rule.activity.getString(R.string.screen_start_chat_join_room_by_address_action) + rule.onNodeWithText(text).performTextInput("#address:matrix.org") + eventsRecorder.assertSingle(JoinRoomByAddressEvents.UpdateAddress("#address:matrix.org")) + } + + @Test + fun `clicking on continue emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setJoinRoomByAddressView( + aJoinRoomByAddressState( + eventSink = eventsRecorder, + ) + ) + rule.clickOn(CommonStrings.action_continue) + eventsRecorder.assertSingle(JoinRoomByAddressEvents.Continue) + } +} + +private fun AndroidComposeTestRule.setJoinRoomByAddressView( + state: JoinRoomByAddressState, +) { + setSafeContent { + JoinRoomByAddressView(state = state) + } +} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt new file mode 100644 index 00000000000..d2d9959e2a6 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootPresenterTest.kt @@ -0,0 +1,199 @@ +/* + * 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. + */ + +package io.element.android.features.createroom.impl.root + +import androidx.compose.runtime.MutableState +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.createroom.api.ConfirmingStartDmWithMatrixUser +import io.element.android.features.createroom.api.StartDMAction +import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter +import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory +import io.element.android.features.createroom.impl.userlist.UserListDataStore +import io.element.android.features.createroom.test.FakeStartDMAction +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 +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.core.aBuildMeta +import io.element.android.libraries.usersearch.test.FakeUserRepository +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 kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class CreateBaseRoomRootPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - start DM action failure scenario`() = runTest { + val startDMFailureResult = AsyncAction.Failure(AN_EXCEPTION) + val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> + actionState.value = startDMFailureResult + } + val startDMAction = FakeStartDMAction(executeResult = executeResult) + val presenter = createCreateRoomRootPresenter(startDMAction) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName) + assertThat(initialState.userListState.selectedUsers).isEmpty() + assertThat(initialState.userListState.isSearchActive).isFalse() + assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse() + val matrixUser = MatrixUser(UserId("@name:domain")) + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + awaitItem().also { state -> + assertThat(state.startDmAction).isEqualTo(startDMFailureResult) + executeResult.assertions().isCalledOnce().with( + value(matrixUser), + value(false), + any(), + ) + state.eventSink(CreateRoomRootEvents.CancelStartDM) + } + awaitItem().also { state -> + assertThat(state.startDmAction.isUninitialized()).isTrue() + } + } + } + + @Test + fun `present - start DM action success scenario`() = runTest { + val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID) + val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> + actionState.value = startDMSuccessResult + } + val startDMAction = FakeStartDMAction(executeResult = executeResult) + val presenter = createCreateRoomRootPresenter(startDMAction) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName) + assertThat(initialState.userListState.selectedUsers).isEmpty() + assertThat(initialState.userListState.isSearchActive).isFalse() + assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse() + val matrixUser = MatrixUser(UserId("@name:domain")) + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + awaitItem().also { state -> + assertThat(state.startDmAction).isEqualTo(startDMSuccessResult) + executeResult.assertions().isCalledOnce().with( + value(matrixUser), + value(false), + any(), + ) + } + } + } + + @Test + fun `present - start DM action confirmation scenario - cancel`() = runTest { + val matrixUser = MatrixUser(UserId("@name:domain")) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> + actionState.value = startDMConfirmationResult + } + val startDMAction = FakeStartDMAction(executeResult = executeResult) + val presenter = createCreateRoomRootPresenter(startDMAction) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + val confirmingState = awaitItem() + assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult) + executeResult.assertions().isCalledOnce().with( + value(matrixUser), + value(false), + any(), + ) + // Cancelling should not create the DM + confirmingState.eventSink(CreateRoomRootEvents.CancelStartDM) + val finalState = awaitItem() + assertThat(finalState.startDmAction.isUninitialized()).isTrue() + executeResult.assertions().isCalledExactly(1) + } + } + + @Test + fun `present - start DM action confirmation scenario - confirm`() = runTest { + val matrixUser = MatrixUser(UserId("@name:domain")) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> + actionState.value = startDMConfirmationResult + } + val startDMAction = FakeStartDMAction(executeResult = executeResult) + val presenter = createCreateRoomRootPresenter(startDMAction) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + val confirmingState = awaitItem() + assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult) + executeResult.assertions().isCalledOnce().with( + value(matrixUser), + value(false), + any(), + ) + // Start DM again should invoke the action with createIfDmDoesNotExist = true + confirmingState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + executeResult.assertions().isCalledExactly(2).withSequence( + listOf(value(matrixUser), value(false), any()), + listOf(value(matrixUser), value(true), any()), + ) + } + } + + @Test + fun `present - room directory search`() = runTest { + val presenter = createCreateRoomRootPresenter(isRoomDirectorySearchEnabled = true) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().let { state -> + assertThat(state.isRoomDirectorySearchEnabled).isTrue() + } + } + } + + private fun createCreateRoomRootPresenter( + startDMAction: StartDMAction = FakeStartDMAction(), + isRoomDirectorySearchEnabled: Boolean = false, + ): CreateRoomRootPresenter { + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.RoomDirectorySearch.key to isRoomDirectorySearchEnabled, + ), + ) + return CreateRoomRootPresenter( + presenterFactory = FakeUserListPresenterFactory(FakeUserListPresenter()), + userRepository = FakeUserRepository(), + userListDataStore = UserListDataStore(), + startDMAction = startDMAction, + featureFlagService = featureFlagService, + buildMeta = aBuildMeta(), + ) + } +} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootViewTest.kt new file mode 100644 index 00000000000..9104b2dfb2e --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateBaseRoomRootViewTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.createroom.impl.root + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.createroom.impl.R +import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList +import io.element.android.features.createroom.impl.userlist.aUserListState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.ui.model.getBestName +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.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class CreateBaseRoomRootViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setCreateRoomRootView( + aCreateRoomRootState( + eventSink = eventsRecorder, + ), + onCloseClick = it + ) + rule.pressBack() + } + } + + @Test + fun `clicking on New room invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setCreateRoomRootView( + aCreateRoomRootState( + eventSink = eventsRecorder, + ), + onNewRoomClick = it + ) + rule.clickOn(R.string.screen_create_room_action_create_room) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on Invite people invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setCreateRoomRootView( + aCreateRoomRootState( + applicationName = "test", + eventSink = eventsRecorder, + ), + onInviteFriendsClick = it + ) + 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`() { + val recentDirectRoomList = aRecentDirectRoomList() + val firstRoom = recentDirectRoomList[0] + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithParam(firstRoom.roomId) { + rule.setCreateRoomRootView( + aCreateRoomRootState( + userListState = aUserListState( + recentDirectRooms = recentDirectRoomList + ), + eventSink = eventsRecorder, + ), + onOpenDM = it + ) + rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on Join room by address invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setCreateRoomRootView( + aCreateRoomRootState( + eventSink = eventsRecorder, + ), + onJoinRoomByAddressClick = it + ) + rule.clickOn(R.string.screen_start_chat_join_room_by_address_action) + } + } + + @Test + fun `clicking on room directory invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setCreateRoomRootView( + aCreateRoomRootState( + eventSink = eventsRecorder, + isRoomDirectorySearchEnabled = true + ), + onRoomDirectorySearchClick = it + ) + rule.clickOn(R.string.screen_room_directory_search_title) + } + } +} + +private fun AndroidComposeTestRule.setCreateRoomRootView( + state: CreateRoomRootState, + onCloseClick: () -> Unit = EnsureNeverCalled(), + onNewRoomClick: () -> Unit = EnsureNeverCalled(), + onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onInviteFriendsClick: () -> Unit = EnsureNeverCalled(), + onJoinRoomByAddressClick: () -> Unit = EnsureNeverCalled(), + onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + CreateRoomRootView( + state = state, + onCloseClick = onCloseClick, + onNewRoomClick = onNewRoomClick, + onOpenDM = onOpenDM, + onInviteFriendsClick = onInviteFriendsClick, + onJoinByAddressClick = onJoinRoomByAddressClick, + onRoomDirectorySearchClick = onRoomDirectorySearchClick, + ) + } +} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTest.kt deleted file mode 100644 index 139fae5982a..00000000000 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.createroom.impl.root - -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.createroom.api.StartDMAction -import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter -import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory -import io.element.android.features.createroom.impl.userlist.UserListDataStore -import io.element.android.features.createroom.test.FakeStartDMAction -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 io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_THROWABLE -import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.usersearch.test.FakeUserRepository -import io.element.android.tests.testutils.WarmUpRule -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class CreateRoomRootPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - start DM action complete scenario`() = runTest { - val startDMAction = FakeStartDMAction() - val presenter = createCreateRoomRootPresenter(startDMAction) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - - assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName) - assertThat(initialState.userListState.selectedUsers).isEmpty() - assertThat(initialState.userListState.isSearchActive).isFalse() - assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse() - - val matrixUser = MatrixUser(UserId("@name:domain")) - val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID) - val startDMFailureResult = AsyncAction.Failure(A_THROWABLE) - - // Failure - startDMAction.givenExecuteResult(startDMFailureResult) - initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) - assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java) - awaitItem().also { state -> - assertThat(state.startDmAction).isEqualTo(startDMFailureResult) - state.eventSink(CreateRoomRootEvents.CancelStartDM) - } - - // Success - startDMAction.givenExecuteResult(startDMSuccessResult) - awaitItem().also { state -> - assertThat(state.startDmAction).isEqualTo(AsyncAction.Uninitialized) - state.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) - } - assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java) - awaitItem().also { state -> - assertThat(state.startDmAction).isEqualTo(startDMSuccessResult) - } - } - } - - private fun createCreateRoomRootPresenter( - startDMAction: StartDMAction = FakeStartDMAction(), - ): CreateRoomRootPresenter { - return CreateRoomRootPresenter( - presenterFactory = FakeUserListPresenterFactory(FakeUserListPresenter()), - userRepository = FakeUserRepository(), - userListDataStore = UserListDataStore(), - startDMAction = startDMAction, - buildMeta = aBuildMeta(), - ) - } -} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootViewTest.kt deleted file mode 100644 index 88390f3b8c4..00000000000 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootViewTest.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.createroom.impl.root - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.createroom.impl.R -import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList -import io.element.android.features.createroom.impl.userlist.aUserListState -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.ui.model.getBestName -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.Rule -import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith -import org.robolectric.annotation.Config - -@RunWith(AndroidJUnit4::class) -class CreateRoomRootViewTest { - @get:Rule - val rule = createAndroidComposeRule() - - @Test - fun `clicking on back invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { - rule.setCreateRoomRootView( - aCreateRoomRootState( - eventSink = eventsRecorder, - ), - onCloseClick = it - ) - rule.pressBack() - } - } - - @Test - fun `clicking on New room invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { - rule.setCreateRoomRootView( - aCreateRoomRootState( - eventSink = eventsRecorder, - ), - onNewRoomClick = it - ) - rule.clickOn(R.string.screen_create_room_action_create_room) - } - } - - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on Invite people invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { - rule.setCreateRoomRootView( - aCreateRoomRootState( - applicationName = "test", - eventSink = eventsRecorder, - ), - onInviteFriendsClick = it - ) - 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`() { - val recentDirectRoomList = aRecentDirectRoomList() - val firstRoom = recentDirectRoomList[0] - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnceWithParam(firstRoom.roomId) { - rule.setCreateRoomRootView( - aCreateRoomRootState( - userListState = aUserListState( - recentDirectRooms = recentDirectRoomList - ), - eventSink = eventsRecorder, - ), - onOpenDM = it - ) - rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() - } - } -} - -private fun AndroidComposeTestRule.setCreateRoomRootView( - state: CreateRoomRootState, - onCloseClick: () -> Unit = EnsureNeverCalled(), - onNewRoomClick: () -> Unit = EnsureNeverCalled(), - onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(), - onInviteFriendsClick: () -> Unit = EnsureNeverCalled(), -) { - setContent { - CreateRoomRootView( - state = state, - onCloseClick = onCloseClick, - onNewRoomClick = onNewRoomClick, - onOpenDM = onOpenDM, - onInviteFriendsClick = onInviteFriendsClick, - ) - } -} diff --git a/features/createroom/test/build.gradle.kts b/features/createroom/test/build.gradle.kts index f2f2b8280a8..b7df0caab66 100644 --- a/features/createroom/test/build.gradle.kts +++ b/features/createroom/test/build.gradle.kts @@ -18,5 +18,6 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.test) implementation(projects.libraries.architecture) + implementation(projects.tests.testutils) api(projects.features.createroom.api) } diff --git a/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.kt b/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.kt index 27ad5f7d62a..90e2ecf1c1a 100644 --- a/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.kt +++ b/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.kt @@ -11,20 +11,19 @@ import androidx.compose.runtime.MutableState import io.element.android.features.createroom.api.StartDMAction import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.test.A_ROOM_ID -import kotlinx.coroutines.delay +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.tests.testutils.lambda.lambdaError -class FakeStartDMAction : StartDMAction { - private var executeResult: AsyncAction = AsyncAction.Success(A_ROOM_ID) - - fun givenExecuteResult(result: AsyncAction) { - executeResult = result +class FakeStartDMAction( + private val executeResult: (MatrixUser, Boolean, MutableState>) -> Unit = { _, _, _ -> + lambdaError() } - - override suspend fun execute(userId: UserId, actionState: MutableState>) { - actionState.value = AsyncAction.Loading - delay(1) - actionState.value = executeResult +) : StartDMAction { + override suspend fun execute( + matrixUser: MatrixUser, + createIfDmDoesNotExist: Boolean, + actionState: MutableState>, + ) { + executeResult(matrixUser, createIfDmDoesNotExist, actionState) } } 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 b7d9ab4ba1a..af1bcdad8df 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 @@ -5,8 +5,6 @@ * Please see LICENSE files in the repository root for full details. */ -@file:OptIn(ExperimentalComposeUiApi::class) - package io.element.android.features.logout.impl import androidx.compose.foundation.clickable @@ -32,12 +30,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -54,12 +53,10 @@ import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrgani import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.list.SwitchListItem -import io.element.android.libraries.designsystem.modifiers.autofill import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart -import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Scaffold @@ -87,12 +84,7 @@ fun AccountDeactivationView( navigationIcon = { BackButton(onClick = onBackClick) }, - title = { - Text( - text = stringResource(R.string.screen_deactivate_account_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(R.string.screen_deactivate_account_title), ) }, ) { padding -> @@ -276,14 +268,9 @@ private fun Content( .fillMaxWidth() .onTabOrEnterKeyFocusNext(focusManager) .testTag(TestTags.loginPassword) - .autofill( - autofillTypes = listOf(AutofillType.Password), - onFill = { - val sanitized = it.sanitize() - passwordFieldState = sanitized - eventSink(AccountDeactivationEvents.SetPassword(sanitized)) - } - ), + .semantics { + contentType = ContentType.Password + }, onValueChange = { val sanitized = it.sanitize() passwordFieldState = sanitized diff --git a/features/deactivation/impl/src/main/res/values-bg/translations.xml b/features/deactivation/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 00000000000..34ad5b47722 --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,5 @@ + + + "Моля, потвърдете, че искате да деактивирате акаунта си. Това действие не може да бъде отменено." + "Деактивиране на акаунта" + diff --git a/features/deactivation/impl/src/main/res/values-cy/translations.xml b/features/deactivation/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..e49073c36ed --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,14 @@ + + + "Cadarnhewch eich bod am gau\'r cyfrif. Does dom modd dadwneud y weithred hon." + "Dileu fy holl negeseuon" + "Rhybudd: Mae\'n bosibl y bydd defnyddwyr y dyfodol yn gweld sgyrsiau anghyflawn." + "Bydd cau eich cyfrif yn %1$s yn:" + "dim modd ei adfer" + "%1$s eich cyfrif (does dim modd i chi fewngofnodi eto, ac nid oes modd ailddefnyddio\'ch dull adnabod)." + "Analluogi\'n barhaol" + "Eich tynnu o bob ystafell sgwrsio." + "Dileu manylion eich cyfrif o\'n gweinydd hunaniaeth." + "Bydd eich negeseuon yn dal i fod yn weladwy i ddefnyddwyr cofrestredig ond fyddan nhw ddim ar gael i ddefnyddwyr newydd neu anghofrestredig os byddwch yn dewis eu dileu." + "Cau cyfrif" + diff --git a/features/deactivation/impl/src/main/res/values-da/translations.xml b/features/deactivation/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..e9d5d9fddae --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,14 @@ + + + "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." + "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)." + "Deaktiver permanent" + "Fjern dig fra alle samtalerum" + "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." + "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 0830d6ba3f9..cd61a6a9d00 100644 --- a/features/deactivation/impl/src/main/res/values-de/translations.xml +++ b/features/deactivation/impl/src/main/res/values-de/translations.xml @@ -10,5 +10,5 @@ "Sie werden aus allen Chatrooms entfernt." "Löschen Sie Ihre Kontoinformationen von unserem Identitätsserver." "Gelöschte Nachrichten werden für bereits registrierte Benutzer weiterhin sichtbar sein, wenn sie auch neuen oder nicht registrierten Benutzern nicht mehr zur Verfügung stehen." - "Benutzerkonto deaktivieren" + "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 53bd1bbbea4..ac645f30634 100644 --- a/features/deactivation/impl/src/main/res/values-el/translations.xml +++ b/features/deactivation/impl/src/main/res/values-el/translations.xml @@ -7,7 +7,7 @@ "μη αναστρέψιμο" "%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 new file mode 100644 index 00000000000..cd0757ba3ed --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,14 @@ + + + "Confirma que quieres desactivar tu cuenta. Esta acción no se puede deshacer." + "Borrar todos mis mensajes" + "Advertencia: Futuros usuarios pueden ver conversaciones incompletas." + "Desactivar tu cuenta es %1$s:" + "irreversible" + "%1$s tu cuenta (no podrás volver a iniciar sesión y tu ID no se podrá volver a utilizar)." + "Inhabilitará permanentemente" + "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-eu/translations.xml b/features/deactivation/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..d55c272e656 --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,9 @@ + + + "Ezabatu nire mezu guztiak" + "Kontuaren desaktibazioa %1$s, honakoa eragingo du:" + "ezin da desegin" + "Ezgaitu betiko" + "Kendu zure burua txat gela guztietatik." + "Desaktibatu kontua" + 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 875142bc019..675ac1e1e0b 100644 --- a/features/deactivation/impl/src/main/res/values-fr/translations.xml +++ b/features/deactivation/impl/src/main/res/values-fr/translations.xml @@ -10,5 +10,5 @@ "Vous retirer de tous les salons et toutes les discussions." "Supprimer les informations de votre compte du serveur d’identité." "Rendre vos messages invisibles aux futurs membres des salons si vous choisissez de les supprimer. Vos messages seront toujours visibles pour les utilisateurs qui les ont déjà récupérés." - "Désactiver votre compte" + "Désactiver le compte" diff --git a/features/deactivation/impl/src/main/res/values-in/translations.xml b/features/deactivation/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..e255ad34e99 --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,14 @@ + + + "Harap konfirmasi bahwa Anda ingin menonaktifkan akun Anda. Tindakan ini tidak dapat diurungkan." + "Hapus semua pesan saya" + "Peringatan: Pengguna masa depan mungkin melihat percakapan yang tidak lengkap." + "Penonaktifan akun Anda %1$s, ini akan:" + "tidak dapat diurungkan" + "%1$s akun Anda (Anda tidak dapat masuk kembali, dan ID Anda tidak dapat digunakan kembali)." + "Nonaktifkan secara permanen" + "Mengeluarkan Anda dari semua ruangan obrolan." + "Hapus informasi akun Anda dari server identitas kami." + "Pesan Anda akan tetap terlihat oleh pengguna terdaftar tetapi tidak akan tersedia bagi pengguna baru atau tidak terdaftar jika Anda memilih untuk menghapusnya." + "Nonaktifkan akun" + 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 7cd484d0e4e..3fbc9d536b7 100644 --- a/features/deactivation/impl/src/main/res/values-it/translations.xml +++ b/features/deactivation/impl/src/main/res/values-it/translations.xml @@ -10,5 +10,5 @@ "Ti rimuove da tutte le stanze di chat." "Elimina le informazioni del tuo account dal nostro server di identità." "I tuoi messaggi saranno ancora visibili agli utenti registrati, ma non saranno disponibili per gli utenti nuovi o non registrati se decidi di eliminarli." - "Disattivazione dell\'account" + "Disattiva account" diff --git a/features/deactivation/impl/src/main/res/values-nb/translations.xml b/features/deactivation/impl/src/main/res/values-nb/translations.xml index 082160d636c..cdff7f886df 100644 --- a/features/deactivation/impl/src/main/res/values-nb/translations.xml +++ b/features/deactivation/impl/src/main/res/values-nb/translations.xml @@ -5,8 +5,10 @@ "Advarsel: Fremtidige brukere vil kunne se ufullstendige samtaler." "Deaktivering av kontoen din er %1$s , det vil:" "irreversibel" + "%1$s kontoen din (du kan ikke logge på igjen, og ID-en din kan ikke brukes på nytt)." "Deaktiver permanent" "Fjern deg fra alle chatterom." "Slett kontoinformasjonen din fra vår identitetsserver." + "Meldingene dine vil fortsatt være synlige for registrerte brukere, men vil ikke være tilgjengelige for nye eller uregistrerte brukere hvis du velger å slette dem." "Deaktiver kontoen" diff --git a/features/deactivation/impl/src/main/res/values-nl/translations.xml b/features/deactivation/impl/src/main/res/values-nl/translations.xml index 5f7cf788477..e03ffa50467 100644 --- a/features/deactivation/impl/src/main/res/values-nl/translations.xml +++ b/features/deactivation/impl/src/main/res/values-nl/translations.xml @@ -7,7 +7,7 @@ "onomkeerbaar" "Je account %1$s (je kunt niet opnieuw inloggen en je ID kan niet opnieuw worden gebruikt)" "permanent uitschakelen" - "Je verwijderen uit alle chatrooms." + "Je verwijderen uit alle chatkamers." "Je accountgegevens verwijderen van onze identiteitsserver." "Je berichten zijn nog steeds zichtbaar voor geregistreerde gebruikers, maar niet beschikbaar voor nieuwe of niet-geregistreerde gebruikers als je ervoor kiest ze te verwijderen." "Account sluiten" 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 4125db4664f..51a412a7a3a 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 @@ -1,5 +1,14 @@ + "Confirme que você deseja desativar sua conta. Essa ação não pode ser desfeita." + "Excluir todas as minhas mensagens" + "Aviso: Os futuros usuários poderão ver conversas incompletas." "Desativar sua conta é %1$s, isso irá:" + "irreversível" + "%1$s sua conta (você não poderá fazer login novamente, e seu ID não poderá ser reutilizado)." + "Desativar permanentemente" "Te remover de todas as salas de conversa." + "Exclua 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 excluí-las." + "Desativar conta" diff --git a/features/deactivation/impl/src/main/res/values-tr/translations.xml b/features/deactivation/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..cc61b80c540 --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,14 @@ + + + "Lütfen hesabınızı devre dışı bırakmak istediğinizi onaylayın. Bu işlem geri alınamaz." + "Tüm mesajlarımı sil" + "Uyarı: Gelecekteki kullanıcılar eksik konuşmalar görebilir." + "Hesabınızı devre dışı bırakmak %1$s, şunları yapacaktır:" + "geri alınamaz" + "%1$s (tekrar giriş yapamazsınız ve kimliğiniz yeniden kullanılamaz)." + "Kalıcı olarak devre dışı bırak" + "Sizi tüm sohbet odalarından çıkarmak." + "Hesap bilgileriniz kimlik sunucumuzdan silinecek." + "Mesajlarınız kayıtlı kullanıcılar tarafından görülmeye devam eder, ancak silmeyi seçerseniz yeni veya kayıtlı olmayan kullanıcılar tarafından görüntülenemeyecek." + "Hesabı devre dışı bırak" + 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 62cf66cec1d..04b32df8b2c 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/enterprise/api/build.gradle.kts b/features/enterprise/api/build.gradle.kts index 91c54bc42c5..9f63ab2cf1f 100644 --- a/features/enterprise/api/build.gradle.kts +++ b/features/enterprise/api/build.gradle.kts @@ -13,6 +13,7 @@ android { } dependencies { + implementation(libs.compound) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) } 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 2ac334653ed..4e2a3cc1f2f 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 @@ -7,9 +7,28 @@ package io.element.android.features.enterprise.api +import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.libraries.matrix.api.core.SessionId interface EnterpriseService { val isEnterpriseBuild: Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean + fun defaultHomeserverList(): List + suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean + + fun semanticColorsLight(): SemanticColors + fun semanticColorsDark(): SemanticColors + + fun firebasePushGateway(): String? + fun unifiedPushDefaultPushGateway(): String? + + companion object { + const val ANY_ACCOUNT_PROVIDER = "*" + } +} + +fun EnterpriseService.canConnectToAnyHomeserver(): Boolean { + return defaultHomeserverList().let { + it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER) + } } 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 new file mode 100644 index 00000000000..c67b94476be --- /dev/null +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt @@ -0,0 +1,12 @@ +/* + * 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.enterprise.api + +interface SessionEnterpriseService { + suspend fun isElementCallAvailable(): Boolean +} diff --git a/features/enterprise/impl/build.gradle.kts b/features/enterprise/impl/build.gradle.kts index 6beea5b83a6..642b2fef40f 100644 --- a/features/enterprise/impl/build.gradle.kts +++ b/features/enterprise/impl/build.gradle.kts @@ -17,6 +17,7 @@ android { setupAnvil() dependencies { + implementation(libs.compound) implementation(projects.anvilannotations) api(projects.features.enterprise.api) implementation(projects.libraries.architecture) diff --git a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index c77c7377d88..7f4f4b387b3 100644 --- a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -8,6 +8,9 @@ package io.element.android.features.enterprise.impl import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.compound.tokens.generated.SemanticColors +import io.element.android.compound.tokens.generated.compoundColorsDark +import io.element.android.compound.tokens.generated.compoundColorsLight import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.SessionId @@ -18,4 +21,14 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService { override val isEnterpriseBuild = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false + + override fun defaultHomeserverList(): List = emptyList() + override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true + + override fun semanticColorsLight(): SemanticColors = compoundColorsLight + + override fun semanticColorsDark(): SemanticColors = compoundColorsDark + + override fun firebasePushGateway(): String? = null + override fun unifiedPushDefaultPushGateway(): String? = null } diff --git a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt new file mode 100644 index 00000000000..2728bdad6ae --- /dev/null +++ b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt @@ -0,0 +1,18 @@ +/* + * 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.enterprise.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.enterprise.api.SessionEnterpriseService +import io.element.android.libraries.di.SessionScope +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultSessionEnterpriseService @Inject constructor() : SessionEnterpriseService { + override suspend fun isElementCallAvailable(): Boolean = true +} diff --git a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt b/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt index e2a6a11a9ae..9f3ebbb8454 100644 --- a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt +++ b/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.enterprise.impl import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_SESSION_ID import kotlinx.coroutines.test.runTest import org.junit.Test @@ -19,6 +20,18 @@ class DefaultEnterpriseServiceTest { assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse() } + @Test + fun `defaultHomeserverList should return empty list`() { + val defaultEnterpriseService = DefaultEnterpriseService() + assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty() + } + + @Test + fun `isAllowedToConnectToHomeserver is true for all homeserver urls`() = runTest { + val defaultEnterpriseService = DefaultEnterpriseService() + assertThat(defaultEnterpriseService.isAllowedToConnectToHomeserver(A_HOMESERVER_URL)).isTrue() + } + @Test fun `isEnterpriseUser always return false`() = runTest { val defaultEnterpriseService = DefaultEnterpriseService() diff --git a/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt b/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt new file mode 100644 index 00000000000..b1e70ef045c --- /dev/null +++ b/features/enterprise/impl/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseServiceTest.kt @@ -0,0 +1,20 @@ +/* + * 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.enterprise.impl + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultSessionEnterpriseServiceTest { + @Test + fun `isElementCallAvailable is always true`() = runTest { + val service = DefaultSessionEnterpriseService() + assertThat(service.isElementCallAvailable()).isTrue() + } +} diff --git a/features/enterprise/test/build.gradle.kts b/features/enterprise/test/build.gradle.kts new file mode 100644 index 00000000000..91b76f4fa78 --- /dev/null +++ b/features/enterprise/test/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * 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. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.enterprise.test" +} + +dependencies { + api(projects.features.enterprise.api) + implementation(libs.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 new file mode 100644 index 00000000000..14195e12432 --- /dev/null +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -0,0 +1,57 @@ +/* + * 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.enterprise.test + +import io.element.android.compound.tokens.generated.SemanticColors +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +class FakeEnterpriseService( + override val isEnterpriseBuild: Boolean = false, + private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() }, + private val defaultHomeserverListResult: () -> List = { emptyList() }, + private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, + private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() }, + private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() }, + private val firebasePushGatewayResult: () -> String? = { lambdaError() }, + private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, +) : EnterpriseService { + override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask { + isEnterpriseUserResult(sessionId) + } + + override fun defaultHomeserverList(): List { + return defaultHomeserverListResult() + } + + override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean = simulateLongTask { + isAllowedToConnectToHomeserverResult(homeserverUrl) + } + + override fun semanticColorsLight(): SemanticColors { + return semanticColorsLightResult() + } + + override fun semanticColorsDark(): SemanticColors { + return semanticColorsDarkResult() + } + + override fun firebasePushGateway(): String? { + return firebasePushGatewayResult() + } + + override fun unifiedPushDefaultPushGateway(): String? { + return unifiedPushDefaultPushGatewayResult() + } + + companion object { + const val A_FAKE_HOMESERVER = "a_fake_homeserver" + } +} 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 new file mode 100644 index 00000000000..a1e811ed107 --- /dev/null +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt @@ -0,0 +1,20 @@ +/* + * 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.enterprise.test + +import io.element.android.features.enterprise.api.SessionEnterpriseService +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +class FakeSessionEnterpriseService( + private val isElementCallAvailableResult: () -> Boolean = { lambdaError() }, +) : SessionEnterpriseService { + override suspend fun isElementCallAvailable(): Boolean = simulateLongTask { + isElementCallAvailableResult() + } +} diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index d9cd9930437..e95a4697ae9 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -14,6 +14,12 @@ plugins { android { namespace = "io.element.android.features.ftue.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } setupAnvil() @@ -30,6 +36,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) implementation(projects.features.analytics.api) + implementation(projects.features.logout.api) implementation(projects.features.securebackup.api) implementation(projects.features.verifysession.api) implementation(projects.services.analytics.api) @@ -37,16 +44,19 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.noop) implementation(projects.services.toolbox.api) + implementation(projects.appconfig) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.noop) - testImplementation(projects.libraries.permissions.impl) testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.features.lockscreen.test) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index c5987508fe9..71a27dcaae1 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.navigation.backpresshandlerstrategies.BaseBackPressHandlerStrategy import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack @@ -38,8 +37,6 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SessionScope import io.element.android.services.analytics.api.AnalyticsService -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn @@ -59,7 +56,6 @@ class FtueFlowNode @AssistedInject constructor( backstack = BackStack( initialElement = NavTarget.Placeholder, savedStateMap = buildContext.savedStateMap, - backPressHandler = NoOpBackstackHandlerStrategy(), ), buildContext = buildContext, plugins = plugins, @@ -88,7 +84,7 @@ class FtueFlowNode @AssistedInject constructor( moveToNextStepIfNeeded() }) - analyticsService.didAskUserConsent() + analyticsService.didAskUserConsentFlow .distinctUntilChanged() .onEach { moveToNextStepIfNeeded() } .launchIn(lifecycleScope) @@ -104,7 +100,7 @@ class FtueFlowNode @AssistedInject constructor( NavTarget.Placeholder -> { createNode(buildContext) } - NavTarget.SessionVerification -> { + is NavTarget.SessionVerification -> { val callback = object : FtueSessionVerificationFlowNode.Callback { override fun onDone() { moveToNextStepIfNeeded() @@ -175,11 +171,3 @@ class FtueFlowNode @AssistedInject constructor( } } } - -private class NoOpBackstackHandlerStrategy : BaseBackPressHandlerStrategy() { - override val canHandleBackPressFlow: StateFlow = MutableStateFlow(true) - - override fun onBackPressed() { - // No-op - } -} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt new file mode 100644 index 00000000000..4387b3dc00f --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt @@ -0,0 +1,23 @@ +/* + * 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.ftue.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModePresenter +import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesTo(SessionScope::class) +@Module +interface FtueModule { + @Binds + fun bindChooseSelfVerificationMethodPresenter(presenter: ChooseSelfVerificationModePresenter): Presenter +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt index 05bd14efcd4..5db0955c5ac 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt @@ -16,6 +16,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter @@ -27,6 +28,7 @@ import kotlinx.coroutines.launch class NotificationsOptInPresenter @AssistedInject constructor( permissionsPresenterFactory: PermissionsPresenter.Factory, @Assisted private val callback: NotificationsOptInNode.Callback, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val permissionStateProvider: PermissionStateProvider, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt index 69bca0250bd..444c311a637 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.components.PageTitle 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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button @@ -149,6 +150,7 @@ private fun NotificationRow( ) { Avatar( avatarData = AvatarData(id = avatarColorsId, name = avatarLetter, size = AvatarSize.NotificationsOptIn), + avatarType = AvatarType.User, ) Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) { Box( diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt index 4ff7e393b4a..a37fdd21922 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt @@ -9,6 +9,7 @@ package io.element.android.features.ftue.impl.sessionverification import android.os.Parcelable import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.modality.BuildContext @@ -17,15 +18,21 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.newRoot +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.appconfig.LearnMoreConfig +import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode import io.element.android.features.securebackup.api.SecureBackupEntryPoint -import io.element.android.features.verifysession.api.VerifySessionEntryPoint +import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.designsystem.utils.OpenUrlInTabView import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.verification.VerificationRequest import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -33,11 +40,11 @@ import kotlinx.parcelize.Parcelize class FtueSessionVerificationFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val verifySessionEntryPoint: VerifySessionEntryPoint, + private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint, private val secureBackupEntryPoint: SecureBackupEntryPoint, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Root(showDeviceVerifiedScreen = false), + initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -45,7 +52,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( ) { sealed interface NavTarget : Parcelable { @Parcelize - data class Root(val showDeviceVerifiedScreen: Boolean) : NavTarget + data object Root : NavTarget + + @Parcelize + data object UseAnotherDevice : NavTarget @Parcelize data object EnterRecoveryKey : NavTarget @@ -62,7 +72,7 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( override fun onDone() { lifecycleScope.launch { // Move to the completed state view in the verification flow - backstack.newRoot(NavTarget.Root(showDeviceVerifiedScreen = true)) + backstack.newRoot(NavTarget.UseAnotherDevice) } } } @@ -70,19 +80,44 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.Root -> { - verifySessionEntryPoint.nodeBuilder(this, buildContext) - .params(VerifySessionEntryPoint.Params(navTarget.showDeviceVerifiedScreen)) - .callback(object : VerifySessionEntryPoint.Callback { - override fun onEnterRecoveryKey() { - backstack.push(NavTarget.EnterRecoveryKey) - } + val callback = object : ChooseSelfVerificationModeNode.Callback { + override fun onUseAnotherDevice() { + backstack.push(NavTarget.UseAnotherDevice) + } + + override fun onUseRecoveryKey() { + backstack.push(NavTarget.EnterRecoveryKey) + } + + override fun onResetKey() { + backstack.push(NavTarget.ResetIdentity) + } + + override fun onLearnMoreAboutEncryption() { + learnMoreUrl.value = LearnMoreConfig.DEVICE_VERIFICATION_URL + } + } + createNode(buildContext, plugins = listOf(callback)) + } + is NavTarget.UseAnotherDevice -> { + outgoingVerificationEntryPoint.nodeBuilder(this, buildContext) + .params(OutgoingVerificationEntryPoint.Params( + showDeviceVerifiedScreen = true, + verificationRequest = VerificationRequest.Outgoing.CurrentSession, + )) + .callback(object : OutgoingVerificationEntryPoint.Callback { override fun onDone() { plugins().forEach { it.onDone() } } - override fun onResetKey() { - backstack.push(NavTarget.ResetIdentity) + override fun onBack() { + backstack.pop() + } + + override fun onLearnMoreAboutEncryption() { + // Note that this callback is never called. The "Learn more" link is not displayed + // for the self session interactive verification. } }) .build() @@ -106,8 +141,12 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( } } + private val learnMoreUrl = mutableStateOf(null) + @Composable override fun View(modifier: Modifier) { BackstackView() + + OpenUrlInTabView(learnMoreUrl) } } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt new file mode 100644 index 00000000000..99e59506937 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeEvent.kt @@ -0,0 +1,12 @@ +/* + * 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.ftue.impl.sessionverification.choosemode + +sealed interface ChooseSelfVerificationModeEvent { + data object SignOut : ChooseSelfVerificationModeEvent +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt new file mode 100644 index 00000000000..687dc4aefeb --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeNode.kt @@ -0,0 +1,54 @@ +/* + * 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.ftue.impl.sessionverification.choosemode + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.logout.api.direct.DirectLogoutView +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class ChooseSelfVerificationModeNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: Presenter, + private val directLogoutView: DirectLogoutView, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onUseAnotherDevice() + fun onUseRecoveryKey() + fun onResetKey() + fun onLearnMoreAboutEncryption() + } + + private val callback = plugins().first() + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + + ChooseSelfVerificationModeView( + state = state, + onUseAnotherDevice = callback::onUseAnotherDevice, + onUseRecoveryKey = callback::onUseRecoveryKey, + onResetKey = callback::onResetKey, + onLearnMore = callback::onLearnMoreAboutEncryption, + modifier = modifier, + ) + + directLogoutView.Render(state = state.directLogoutState) + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt new file mode 100644 index 00000000000..0aec3b4e1d1 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt @@ -0,0 +1,47 @@ +/* + * 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.ftue.impl.sessionverification.choosemode + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import io.element.android.features.logout.api.direct.DirectLogoutEvents +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import javax.inject.Inject + +class ChooseSelfVerificationModePresenter @Inject constructor( + private val encryptionService: EncryptionService, + private val directLogoutPresenter: Presenter, +) : Presenter { + @Composable + override fun present(): ChooseSelfVerificationModeState { + val isLastDevice by encryptionService.isLastDevice.collectAsState() + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } } + + val directLogoutState = directLogoutPresenter.present() + + fun eventHandler(event: ChooseSelfVerificationModeEvent) { + when (event) { + ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) + } + } + + return ChooseSelfVerificationModeState( + isLastDevice = isLastDevice, + canEnterRecoveryKey = canEnterRecoveryKey, + directLogoutState = directLogoutState, + eventSink = ::eventHandler, + ) + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt new file mode 100644 index 00000000000..21c37a4ae29 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt @@ -0,0 +1,17 @@ +/* + * 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.ftue.impl.sessionverification.choosemode + +import io.element.android.features.logout.api.direct.DirectLogoutState + +data class ChooseSelfVerificationModeState( + val isLastDevice: Boolean, + val canEnterRecoveryKey: Boolean, + val directLogoutState: DirectLogoutState, + val eventSink: (ChooseSelfVerificationModeEvent) -> Unit, +) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt new file mode 100644 index 00000000000..574aa367c61 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt @@ -0,0 +1,31 @@ +/* + * 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.ftue.impl.sessionverification.choosemode + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.logout.api.direct.aDirectLogoutState + +class ChooseSelfVerificationModeStateProvider : + PreviewParameterProvider { + override val values = sequenceOf( + aChooseSelfVerificationModeState(isLastDevice = true, canEnterRecoveryKey = true), + aChooseSelfVerificationModeState(isLastDevice = true, canEnterRecoveryKey = false), + aChooseSelfVerificationModeState(isLastDevice = false, canEnterRecoveryKey = true), + aChooseSelfVerificationModeState(isLastDevice = false, canEnterRecoveryKey = false), + ) +} + +fun aChooseSelfVerificationModeState( + isLastDevice: Boolean = false, + canEnterRecoveryKey: Boolean = true, +) = ChooseSelfVerificationModeState( + isLastDevice = isLastDevice, + canEnterRecoveryKey = canEnterRecoveryKey, + directLogoutState = aDirectLogoutState(), + eventSink = {}, +) 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 new file mode 100644 index 00000000000..9839f3bf7c7 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt @@ -0,0 +1,127 @@ +/* + * 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.ftue.impl.sessionverification.choosemode + +import androidx.activity.compose.BackHandler +import androidx.activity.compose.LocalActivity +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.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.ftue.impl.R +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.PageTitle +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChooseSelfVerificationModeView( + state: ChooseSelfVerificationModeState, + onUseAnotherDevice: () -> Unit, + onUseRecoveryKey: () -> Unit, + onResetKey: () -> Unit, + onLearnMore: () -> Unit, + modifier: Modifier = Modifier +) { + val activity = LocalActivity.current + BackHandler { + activity?.finish() + } + + HeaderFooterPage( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + actions = { + TextButton( + text = stringResource(CommonStrings.action_signout), + onClick = { state.eventSink(ChooseSelfVerificationModeEvent.SignOut) } + ) + } + ) + }, + header = { + PageTitle( + iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()), + title = stringResource(id = R.string.screen_identity_confirmation_title), + subtitle = stringResource(id = R.string.screen_identity_confirmation_subtitle) + ) + }, + footer = { + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 16.dp) + ) { + if (state.isLastDevice.not()) { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_identity_use_another_device), + onClick = onUseAnotherDevice, + ) + } + if (state.canEnterRecoveryKey) { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_enter_recovery_key), + onClick = onUseRecoveryKey, + ) + } + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), + onClick = onResetKey, + ) + } + } + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Text( + modifier = Modifier + .clickable(onClick = onLearnMore) + .padding(vertical = 4.dp, horizontal = 16.dp), + text = stringResource(CommonStrings.action_learn_more), + style = ElementTheme.typography.fontBodyLgMedium + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun ChooseSelfVerificationModeViewPreview( + @PreviewParameter(ChooseSelfVerificationModeStateProvider::class) state: ChooseSelfVerificationModeState +) = ElementPreview { + ChooseSelfVerificationModeView( + state = state, + onUseAnotherDevice = {}, + onUseRecoveryKey = {}, + onResetKey = {}, + onLearnMore = {}, + ) +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt index 0409d73538e..744053b976a 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt @@ -66,7 +66,7 @@ class DefaultFtueService @Inject constructor( .onEach { updateState() } .launchIn(sessionCoroutineScope) - analyticsService.didAskUserConsent() + analyticsService.didAskUserConsentFlow .distinctUntilChanged() .onEach { updateState() } .launchIn(sessionCoroutineScope) @@ -118,7 +118,7 @@ class DefaultFtueService @Inject constructor( } private suspend fun needsAnalyticsOptIn(): Boolean { - return analyticsService.didAskUserConsent().first().not() + return analyticsService.didAskUserConsentFlow.first().not() } private suspend fun shouldAskNotificationPermissions(): Boolean { diff --git a/features/ftue/impl/src/main/res/values-be/translations.xml b/features/ftue/impl/src/main/res/values-be/translations.xml index 413bad91322..6c10902ca61 100644 --- a/features/ftue/impl/src/main/res/values-be/translations.xml +++ b/features/ftue/impl/src/main/res/values-be/translations.xml @@ -1,7 +1,18 @@ + "Не можаце пацвердзіць?" + "Стварыць новы ключ аднаўлення" + "Пацвердзіце гэтую прыладу, каб наладзіць бяспечны абмен паведамленнямі." + "Пацвердзіце, што гэта вы" + "Выкарыстоўвайце іншую прыладу" + "Выкарыстоўваць ключ аднаўлення" + "Цяпер вы можаце бяспечна чытаць і адпраўляць паведамленні, і ўсе, з кім вы маеце зносіны ў чаце, таксама могуць давяраць гэтай прыладзе." + "Прылада праверана" + "Выкарыстоўвайце іншую прыладу" + "Чаканне на іншай прыладзе…" "Вы можаце змяніць налады пазней." "Дазвольце апавяшчэнні і ніколі не прапускайце іх" + "Увядзіце ключ аднаўлення" "Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе." "Гісторыя паведамленняў для зашыфраваных пакояў пакуль недаступна." "Мы будзем рады пачуць вашае меркаванне, паведаміце нам аб гэтым праз старонку налад." diff --git a/features/ftue/impl/src/main/res/values-bg/translations.xml b/features/ftue/impl/src/main/res/values-bg/translations.xml index 22370ab1c5f..7918ac7a0c5 100644 --- a/features/ftue/impl/src/main/res/values-bg/translations.xml +++ b/features/ftue/impl/src/main/res/values-bg/translations.xml @@ -1,7 +1,15 @@ + "Не можете да потвърдите?" + "Потвърдете това устройство, за да настроите защитени съобщения." + "Потвърдете самоличността си" + "Използване на друго устройство" + "Използване на ключ за възстановяване" + "Устройството е потвърдено" + "Използване на друго устройство" "Можете да промените настройките си по-късно." "Разрешете известията и никога не пропускайте съобщение" + "Въвеждане на ключ за възстановяване" "Хронологията на съобщенията за шифровани стаи все още не е налична." "Добре дошли в %1$s!" diff --git a/features/ftue/impl/src/main/res/values-cs/translations.xml b/features/ftue/impl/src/main/res/values-cs/translations.xml index bc262cf2b46..9d852feed61 100644 --- a/features/ftue/impl/src/main/res/values-cs/translations.xml +++ b/features/ftue/impl/src/main/res/values-cs/translations.xml @@ -1,7 +1,18 @@ + "Nemůžete potvrdit?" + "Vytvoření nového klíče pro obnovení" + "Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv." + "Potvrďte, že jste to vy" + "Použít jiné zařízení" + "Použít klíč pro obnovení" + "Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat." + "Zařízení ověřeno" + "Použít jiné zařízení" + "Čekání na jiném zařízení…" "Nastavení můžete později změnit." "Povolte oznámení a nezmeškejte žádnou zprávu" + "Zadejte klíč pro obnovení" "Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku." "Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici." "Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením." diff --git a/features/ftue/impl/src/main/res/values-cy/translations.xml b/features/ftue/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..bf87c2ca70c --- /dev/null +++ b/features/ftue/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,22 @@ + + + "Methu cadarnhau?" + "Crëwch allwedd adfer newydd" + "Dilyswch y ddyfais hon er mwyn gosod negeseuon diogel." + "Cadarnhewch eich hunaniaeth" + "Defnyddiwch ddyfais arall" + "Defnyddiwch allwedd adfer" + "Nawr gallwch chi ddarllen neu anfon negeseuon yn ddiogel, a gall unrhyw un rydych chi\'n sgwrsio â nhw ymddiried yn y ddyfais hon hefyd." + "Dyfais wedi\'i dilysu" + "Defnyddiwch ddyfais arall" + "Yn aros ar ddyfais arall…" + "Gallwch newid eich gosodiadau yn nes ymlaen." + "Caniatáu hysbysiadau a pheidio byth â cholli neges" + "Rhowch eich allwedd adfer" + "Bydd galwadau, pleidleisiau, chwilio a mwy yn cael eu hychwanegu yn ddiweddarach eleni." + "Nid yw hanes negeseuon ar gyfer ystafelloedd sydd wedi\'u hamgryptio ar gael eto." + "Byddem wrth ein bodd yn clywed gennych, gadewch i ni wybod beth yw eich barn drwy\'r dudalen gosodiadau." + "Ymlaen!" + "Dyma beth sydd angen i chi ei wybod:" + "Croeso i %1$s!" + diff --git a/features/ftue/impl/src/main/res/values-da/translations.xml b/features/ftue/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..b1ae89a3b43 --- /dev/null +++ b/features/ftue/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,22 @@ + + + "Kan ikke bekræfte?" + "Opret en ny gendannelsesnøgle" + "Verificér denne enhed for at konfigurere sikre meddelelser." + "Bekræft din identitet" + "Brug en anden enhed" + "Brug gendannelsesnøgle" + "Nu kan du læse eller sende beskeder sikkert, og enhver du samtaler med kan også stole på denne enhed." + "Enhed verificeret" + "Brug en anden enhed" + "Venter på en anden enhed…" + "Du kan ændre dine indstillinger senere." + "Tillad notifikationer, og gå aldrig glip af en besked" + "Indtast gendannelsesnøgle" + "Opkald, afstemninger, søgninger og mere vil blive tilføjet senere på året." + "Beskedhistorik for krypterede rum er ikke tilgængelig endnu." + "Vi vil meget gerne høre fra dig. Fortæl os din mening via indstillingssiden." + "Lad os komme i gang!" + "Her er, hvad du har brug for at vide:" + "Velkommen til %1$s!" + 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 151500addad..10473d297e3 100644 --- a/features/ftue/impl/src/main/res/values-de/translations.xml +++ b/features/ftue/impl/src/main/res/values-de/translations.xml @@ -1,11 +1,22 @@ - "Du kannst deine Einstellungen später ändern." + "Bestätigung unmöglich?" + "Erstelle einen neuen Wiederherstellungsschlüssel" + "Verifiziere dieses Gerät, um sicheres Messaging einzurichten." + "Bestätigen Sie Ihre Identität" + "Ein anderes Gerät verwenden" + "Wiederherstellungsschlüssel verwenden" + "Sie können jetzt verschlüsselte Nachrichten lesen und versenden. Ihre Chatpartner vertrauen nun diesem Gerät auch." + "Gerät verifiziert" + "Ein anderes Gerät verwenden" + "Bitte warten bis das andere Gerät bereit ist." + "Sie können Ihre Einstellungen später ändern." "Erlaube Benachrichtigungen und verpasse keine Nachricht" + "Wiederherstellungsschlüssel eingeben" "Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt." "Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein." "Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit." "Los geht\'s!" - "Folgendes musst du wissen:" + "Folgendes sollten Sie wissen:" "Willkommen bei %1$s!" diff --git a/features/ftue/impl/src/main/res/values-el/translations.xml b/features/ftue/impl/src/main/res/values-el/translations.xml index 8c384bb8f49..585f5f0952d 100644 --- a/features/ftue/impl/src/main/res/values-el/translations.xml +++ b/features/ftue/impl/src/main/res/values-el/translations.xml @@ -1,9 +1,20 @@ + "Δεν μπορείς να επιβεβαιώσεις;" + "Δημιουργία νέου κλειδιού ανάκτησης" + "Επαλήθευσε αυτήν τη συσκευή για να ρυθμίσεις την ασφαλή επικοινωνία." + "Επιβεβαίωσε ότι είσαι εσύ" + "Χρήση άλλης συσκευής" + "Χρήση κλειδιού ανάκτησης" + "Τώρα μπορείς να διαβάζεις ή να στέλνεις μηνύματα με ασφάλεια και επίσης μπορεί να εμπιστευτεί αυτήν τη συσκευή οποιοσδήποτε με τον οποίο συνομιλείς." + "Επαληθευμένη συσκευή" + "Χρήση άλλης συσκευής" + "Αναμονή σε άλλη συσκευή…" "Μπορείς να αλλάξεις τις ρυθμίσεις σου αργότερα." "Επέτρεψε τις ειδοποιήσεις και μην χάσεις ούτε ένα μήνυμα" + "Εισαγωγή κλειδιού ανάκτησης" "Κλήσεις, δημοσκοπήσεις, αναζήτηση και άλλα, θα προστεθούν αργότερα φέτος." - "Το ιστορικό μηνυμάτων για κρυπτογραφημένα δωμάτια δεν είναι ακόμα διαθέσιμο." + "Το ιστορικό μηνυμάτων για κρυπτογραφημένες αίθουσες δεν είναι ακόμη διαθέσιμο." "Θα θέλαμε να ακούσουμε τη γνώμη σου, πες μας τη γνώμη σου μέσω της σελίδας ρυθμίσεων." "Πάμε!" "Να τί πρέπει να ξέρεις:" diff --git a/features/ftue/impl/src/main/res/values-es/translations.xml b/features/ftue/impl/src/main/res/values-es/translations.xml index 6a3224c5313..36015aa6dd9 100644 --- a/features/ftue/impl/src/main/res/values-es/translations.xml +++ b/features/ftue/impl/src/main/res/values-es/translations.xml @@ -1,7 +1,18 @@ + "¿No puedes confirmar?" + "Crear una nueva clave de recuperación" + "Verifica este dispositivo para configurar la mensajería segura." + "Confirma que eres tú" + "Usar otro dispositivo" + "Usar clave de recuperación" + "Ahora puedes leer o enviar mensajes de forma segura y cualquier persona con la que chatees también puede confiar en este dispositivo." + "Dispositivo verificado" + "Usar otro dispositivo" + "Esperando en otro dispositivo…" "Puedes cambiar la configuración más tarde." "Activa las notificaciones y nunca te pierdas un mensaje" + "Introduce la clave de recuperación" "Las llamadas, las encuestas, la búsqueda y más se agregarán más adelante este año." "El historial de mensajes de las salas cifradas aún no está disponible." "Nos encantaría saber de ti, haznos saber lo que piensas a través de la página de configuración." 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 83f6cb93fac..09762a4f631 100644 --- a/features/ftue/impl/src/main/res/values-et/translations.xml +++ b/features/ftue/impl/src/main/res/values-et/translations.xml @@ -1,7 +1,18 @@ + "Kas kinnitamine pole võimalik?" + "Loo uus taastevõti" + "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." + "Seade on verifitseeritud" + "Kasuta teist seadet" + "Ootame teise seadme järgi…" "Sa võid seadistusi hiljem alati muuta." "Luba teavitused ja kunagi ei jää sul sõnumid märkamata" + "Sisesta taastevõti" "Kõned, küsitlused, otsing ja palju muud lisanduvad hiljem selle aasta jooksul." "Krüptitud jututubade sõnumite ajalugu pole veel saadaval." "Me soovime teada mida sa arvad. Seadistuste lehel olevast valikust võid saata meile oma kommentaare." diff --git a/features/ftue/impl/src/main/res/values-eu/translations.xml b/features/ftue/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..2dcdff7330d --- /dev/null +++ b/features/ftue/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,21 @@ + + + "Ezin duzu baieztatu?" + "Sortu berreskuratze-gako berria" + "Egiaztatu gailua mezularitza segurua konfiguratzeko." + "Berretsi zure identitatea" + "Erabili beste gailu bat" + "Erabili berreskuratze-gakoa" + "Orain mezuak modu seguruan irakurri edo bidal ditzakezu, eta txateatzen duzun edonor ere fida daiteke gailu honetaz." + "Gailua egiaztatu da" + "Erabili beste gailu bat" + "Beste gailuaren zain…" + "Geroago alda ditzakezu ezarpenak." + "Baimendu jakinarazpenak eta ez galdu inoiz mezurik" + "Sartu berreskuratze-gakoa" + "Deiak, inkestak, bilaketa eta gehiago gehituko dira aurten." + "Enkriptatutako geletarako mezuen historia ez dago oraindik erabilgarri." + "Goazen!" + "Hona hemen jakin beharrekoa:" + "Ongi etorri %1$s(e)ra!" + diff --git a/features/ftue/impl/src/main/res/values-fa/translations.xml b/features/ftue/impl/src/main/res/values-fa/translations.xml index 43d968b7c57..72151a11760 100644 --- a/features/ftue/impl/src/main/res/values-fa/translations.xml +++ b/features/ftue/impl/src/main/res/values-fa/translations.xml @@ -1,7 +1,21 @@ + "نمی‌توانید تأیید کنید؟" + "ایجاد کلید بازیابی جدید" + "تأیید این افزاره برای برپایی پیام‌رسانی امن." + "تأیید هویتتان" + "استفاده از افزاره‌ای دیگر" + "استفاده از کلید بازیابی" + "اکنون می‌توانید پیام‌ها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ می‌زنید نیز می‌تواند به این افزاره اعتماد کند." + "افزاره تأیید شده" + "استفاده از افزاره‌ای دیگر" + "منتظر افزارهٔ دیگر…" "می‌توانید بعداً تنظیماتتان را تغییر دهید." "اجازه به آگاهی‌ها و از دست ندادن پیام‌ها" + "ورود کلید بازیابی" + "تماس ها، نظرسنجی، جستجو و موارد دیگر در اواخر امسال اضافه خواهند شد." + "سابقه پیام برای اتاق های رمزگذاری شده هنوز دردسترس نیست." + "ما دوست داریم از شما بشنویم، نظر خود را از طریق صفحه تنظیمات با ما در میان بگذارید." "بزن بریم!" "چیزهایی که باید بدانید:" "به %1$s خوش آمدید!" diff --git a/features/ftue/impl/src/main/res/values-fi/translations.xml b/features/ftue/impl/src/main/res/values-fi/translations.xml index f04854ffefe..3f3110bcdf9 100644 --- a/features/ftue/impl/src/main/res/values-fi/translations.xml +++ b/features/ftue/impl/src/main/res/values-fi/translations.xml @@ -1,7 +1,18 @@ + "Etkö voi vahvistaa?" + "Luo uusi palautusavain" + "Vahvista tämä laite suojattua viestintää varten." + "Vahvista identiteettisi" + "Käytä toista laitetta" + "Käytä palautusavainta" + "Nyt voit lukea ja lähettää viestejä turvallisesti, ja kaikki, joiden kanssa keskustelet, voivat myös luottaa tähän laitteeseen." + "Laite vahvistettu" + "Käytä toista laitetta" + "Odotetaan toista laitetta…" "Voit muuttaa asetuksia myöhemmin." "Salli ilmoitukset ja älä koskaan missaa viestejä" + "Syötä palautusavain" "Puhelut, kyselyt, haku ja paljon muuta lisätään myöhemmin tänä vuonna." "Salattujen huoneiden viestihistoria ei ole vielä käytettävissä." "Haluaisimme kuulla mielipiteesi, kerro mitä mieltä olet asetuksien kautta." diff --git a/features/ftue/impl/src/main/res/values-fr/translations.xml b/features/ftue/impl/src/main/res/values-fr/translations.xml index 9fc11413cbe..04396014474 100644 --- a/features/ftue/impl/src/main/res/values-fr/translations.xml +++ b/features/ftue/impl/src/main/res/values-fr/translations.xml @@ -1,7 +1,18 @@ + "Confirmation impossible ?" + "Créer une nouvelle clé de récupération" + "Vérifier cette session pour configurer votre messagerie sécurisée." + "Confirmez votre identité" + "Utiliser une autre session" + "Utiliser la clé de récupération" + "Vous pouvez désormais lire ou envoyer des messages en toute sécurité, et toute personne avec qui vous discutez peut également faire confiance à cette session." + "Session vérifiée" + "Utiliser une autre session" + "En attente d’une autre session…" "Vous pourrez modifier vos paramètres ultérieurement." "Autorisez les notifications et ne manquez aucun message" + "Utiliser la clé de récupération" "Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année." "L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour." "N’hésitez pas à nous faire part de vos commentaires via l’écran des paramètres." diff --git a/features/ftue/impl/src/main/res/values-hu/translations.xml b/features/ftue/impl/src/main/res/values-hu/translations.xml index 6a7acab0a4a..5dfdedd4b0e 100644 --- a/features/ftue/impl/src/main/res/values-hu/translations.xml +++ b/features/ftue/impl/src/main/res/values-hu/translations.xml @@ -1,7 +1,18 @@ + "Nem tudja megerősíteni?" + "Új helyreállítási kulcs létrehozása" + "A biztonságos üzenetkezelés beállításához ellenőrizze ezt az eszközt." + "Erősítse meg, hogy Ön az" + "Másik eszköz használata" + "Helyreállítási kulcs használata" + "Mostantól biztonságosan olvashat vagy küldhet üzeneteket, és bármelyik csevegőpartnere megbízhat ebben az eszközben." + "Eszköz ellenőrizve" + "Másik eszköz használata" + "Várakozás a másik eszközre…" "A beállításokat később is módosíthatja." "Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem" + "Adja meg a helyreállítási kulcsot" "A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra." "A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben." "Szeretnénk hallani a véleményét, ossza meg velünk a beállítások oldalon." diff --git a/features/ftue/impl/src/main/res/values-in/translations.xml b/features/ftue/impl/src/main/res/values-in/translations.xml index 1004f8a6431..5325b7f58d8 100644 --- a/features/ftue/impl/src/main/res/values-in/translations.xml +++ b/features/ftue/impl/src/main/res/values-in/translations.xml @@ -1,7 +1,18 @@ + "Tidak dapat mengonfirmasi?" + "Buat kunci pemulihan baru" + "Verifikasi perangkat ini untuk menyiapkan perpesanan aman." + "Konfirmasi bahwa ini Anda" + "Gunakan perangkat lain" + "Gunakan kunci pemulihan" + "Sekarang Anda dapat membaca atau mengirim pesan dengan aman, dan siapa pun yang mengobrol dengan Anda juga dapat mempercayai perangkat ini." + "Perangkat terverifikasi" + "Gunakan perangkat lain" + "Menunggu di perangkat lain…" "Anda dapat mengubah pengaturan Anda nanti." "Izinkan pemberitahuan dan jangan pernah melewatkan pesan" + "Masukkan kunci pemulihan" "Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini." "Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini." "Kami ingin mendengar dari Anda, beri tahu kami pendapat Anda melalui halaman pengaturan." diff --git a/features/ftue/impl/src/main/res/values-it/translations.xml b/features/ftue/impl/src/main/res/values-it/translations.xml index eaff5216dce..3992acd63eb 100644 --- a/features/ftue/impl/src/main/res/values-it/translations.xml +++ b/features/ftue/impl/src/main/res/values-it/translations.xml @@ -1,7 +1,18 @@ + "Non puoi confermare?" + "Crea una nuova chiave di recupero" + "Verifica questo dispositivo per segnare i tuoi messaggi come sicuri." + "Conferma la tua identità" + "Usa un altro dispositivo" + "Usa la chiave di recupero" + "Ora puoi leggere o inviare messaggi in tutta sicurezza e anche chi chatta con te può fidarsi di questo dispositivo." + "Dispositivo verificato" + "Usa un altro dispositivo" + "In attesa sull\'altro dispositivo…" "Potrai modificare le tue impostazioni in seguito." "Consenti le notifiche e non perdere mai un messaggio" + "Inserisci la chiave di recupero" "Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno." "La cronologia dei messaggi per le stanze crittografate non è ancora disponibile." "Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni." diff --git a/features/ftue/impl/src/main/res/values-ka/translations.xml b/features/ftue/impl/src/main/res/values-ka/translations.xml index 04aacccf836..f8ae896aba7 100644 --- a/features/ftue/impl/src/main/res/values-ka/translations.xml +++ b/features/ftue/impl/src/main/res/values-ka/translations.xml @@ -1,7 +1,14 @@ + "ახალი აღდგენის გასაღების შექმნა" + "დაადასტურეთ ეს მოწყობილობა უსაფრთხო მიმოწერისათვის." + "დაამტკიცეთ თქვენი პიროვნება" + "ახლა თქვენ შეძლებთ შეტყობინებების წაკითხვას ან გაგზავნას უსაფრთხოდ, სხვა მომხმარებლებსაც შეუძლიათ ამ მოწყობილობას ენდონ." + "მოწყობილობა დადასტურებულია" + "ველოდებით სხვა მოწყობილობას…" "თქვენ შეგიძლიათ შეცვალოთ თქვენი პარამეტრები მოგვიანებით." "ყველა შეტყობინებაზე შეტყობინებების მიღება" + "შეიყვანეთ აღდგენის გასაღები" "ზარები, გამოკითხვები, ძიება და სხვა დაემატება ამ წლის ბოლოს." "დაშიფრული ოთახებისთვის შეტყობინებების ისტორია ჯერ არ არის ხელმისაწვდომი." "ჩვენ სიამოვნებით მოვისმინოთ თქვენგან, შეგვატყობინეთ რას ფიქრობთ პარამეტრების გვერდზე." diff --git a/features/ftue/impl/src/main/res/values-nb/translations.xml b/features/ftue/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..61810952ba0 --- /dev/null +++ b/features/ftue/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,22 @@ + + + "Kan du ikke bekrefte?" + "Opprett en ny gjenopprettingsnøkkel" + "Verifiser denne enheten for å sette opp sikker meldingsutveksling." + "Bekreft identiteten din" + "Bruk en annen enhet" + "Bruk gjenopprettingsnøkkel" + "Nå kan du lese eller sende meldinger på en sikker måte, og alle du chatter med kan også stole på denne enheten." + "Enhet verifisert" + "Bruk en annen enhet" + "Venter på en annen enhet…" + "Du kan endre innstillingene dine senere." + "Tillat varslinger og gå aldri glipp av en melding" + "Skriv inn gjenopprettingsnøkkel" + "Samtaler, avstemninger, søk og mer vil bli lagt til senere i år." + "Meldingshistorikk for krypterte rom er ikke tilgjengelig ennå." + "Vi vil gjerne høre fra deg, så la oss få vite hva du synes via innstillingssiden." + "Kom igjen!" + "Her er hva du trenger å vite:" + "Velkommen til %1$s!" + diff --git a/features/ftue/impl/src/main/res/values-nl/translations.xml b/features/ftue/impl/src/main/res/values-nl/translations.xml index 1975f26467f..459b003e53c 100644 --- a/features/ftue/impl/src/main/res/values-nl/translations.xml +++ b/features/ftue/impl/src/main/res/values-nl/translations.xml @@ -1,7 +1,18 @@ + "Kan ik dit niet bevestigen?" + "Maak een nieuwe herstelsleutel" + "Verifieer dit apparaat om beveiligde berichten in te stellen." + "Bevestig dat jij het bent" + "Gebruik een ander apparaat" + "Gebruik de herstelsleutel" + "Nu kun je veilig berichten lezen of verzenden, en iedereen met wie je chat kan dit apparaat ook vertrouwen." + "Apparaat geverifieerd" + "Gebruik een ander apparaat" + "Wachten op ander apparaat…" "Je kunt je instellingen later wijzigen." "Sta meldingen toe en mis nooit meer een bericht" + "Voer herstelsleutel in" "Oproepen, peilingen, zoekopdrachten en meer zullen later dit jaar worden toegevoegd." "Berichtgeschiedenis voor versleutelde kamers is nog niet beschikbaar." "We horen graag van je, laat ons weten wat je ervan vindt via de instellingenpagina." 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 b1c1909ac28..682bae9cf91 100644 --- a/features/ftue/impl/src/main/res/values-pl/translations.xml +++ b/features/ftue/impl/src/main/res/values-pl/translations.xml @@ -1,7 +1,18 @@ + "Nie możesz potwierdzić?" + "Utwórz nowy klucz przywracania" + "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." + "Urządzenie zweryfikowane" + "Użyj innego urządzenia" + "Oczekiwanie na inne urządzenie…" "Możesz zmienić ustawienia później." "Zezwól na powiadomienia i nie przegap żadnej wiadomości" + "Wprowadź klucz przywracania" "Połączenia, ankiety, wyszukiwanie i inne zostaną dodane później w tym roku." "Historia wiadomości dla pokoi szyfrowanych nie jest jeszcze dostępna." "Chętnie poznamy Twoją opinię. Daj nam znać, co myślisz na stronie ustawień." diff --git a/features/ftue/impl/src/main/res/values-pt-rBR/translations.xml b/features/ftue/impl/src/main/res/values-pt-rBR/translations.xml index 1528027bd49..223ceedd31c 100644 --- a/features/ftue/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/ftue/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,7 +1,18 @@ + "Não consegue confirmar?" + "Criar uma nova chave de recuperação" + "Verifique este dispositivo para configurar mensagens seguras." + "Confirme sua identidade" + "Usar outro dispositivo" + "Use a chave de recuperação" + "Agora você pode ler ou enviar mensagens com segurança, e qualquer pessoa com quem você conversa também pode confiar neste dispositivo." + "Dispositivo verificado" + "Usar outro dispositivo" + "Aguardando outro dispositivo…" "Você pode alterar suas configurações mais tarde." "Permita notificações e nunca perca uma mensagem" + "Insira a chave de recuperação" "Chamadas, enquetes, pesquisa e muito mais serão adicionadas ainda este ano." "O histórico de mensagens para salas criptografadas ainda não está disponível." "Adoraríamos ouvir sua opinião. Deixe-nos saber o que você pensa através da página de configurações." 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 8ff79c3b64e..d96caa5065c 100644 --- a/features/ftue/impl/src/main/res/values-pt/translations.xml +++ b/features/ftue/impl/src/main/res/values-pt/translations.xml @@ -1,7 +1,18 @@ + "Não é possível confirmar?" + "Criar uma nova chave de recuperação" + "Verifica este dispositivo para configurar o envio seguro de mensagens." + "Confirma que és tu" + "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." + "Dispositivo verificado" + "Utilizar outro dispositivo" + "A aguardar por outros dispositivos…" "Podes alterar as tuas definições mais tarde." "Permite as notificações e nunca percas uma mensagem" + "Insere a chave de recuperação" "Chamadas, sondagens, pesquisa e mais funcionalidades vão ser adicionadas ao longo do ano." "O histórico de mensagens em salas cifradas ainda não está disponível." "Gostaríamos de ouvir a tua opinião, diz-nos o que pensas através da página de configurações." 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 b89537903d9..4d2c9febaeb 100644 --- a/features/ftue/impl/src/main/res/values-ro/translations.xml +++ b/features/ftue/impl/src/main/res/values-ro/translations.xml @@ -1,7 +1,18 @@ + "Nu puteți confirma?" + "Creați o nouă cheie de recuperare" + "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." + "Dispozitiv verificat" + "Utilizați un alt dispozitiv" + "Se așteaptă celălalt dispozitiv…" "Puteți modifica setările mai târziu." "Permiteți notificările și nu pierdeți niciodată un mesaj" + "Introduceți cheia de recuperare" "Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an." "Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare." "Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări." diff --git a/features/ftue/impl/src/main/res/values-ru/translations.xml b/features/ftue/impl/src/main/res/values-ru/translations.xml index 7a5ed89a217..d287e656407 100644 --- a/features/ftue/impl/src/main/res/values-ru/translations.xml +++ b/features/ftue/impl/src/main/res/values-ru/translations.xml @@ -1,7 +1,18 @@ + "Не можете подтвердить?" + "Создайте новый ключ восстановления" + "Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями." + "Подтвердите, что это вы" + "Использовать другое устройство" + "Используйте recovery key" + "Теперь вы можете безопасно читать и отправлять сообщения, и все, с кем вы общаетесь в чате, также могут доверять этому устройству." + "Устройство проверено" + "Использовать другое устройство" + "Ожидание на другом устройстве…" "Вы можете изменить настройки позже." "Разрешите отправку уведомлений и ни одно сообщение не будет пропущено" + "Введите ключ восстановления" "Звонки, опросы, поиск и многое другое будут добавлены позже в этом году." "История сообщений для зашифрованных комнат в этом обновлении будет недоступна." "Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек." diff --git a/features/ftue/impl/src/main/res/values-sk/translations.xml b/features/ftue/impl/src/main/res/values-sk/translations.xml index d963dbd7343..9f39ca6065e 100644 --- a/features/ftue/impl/src/main/res/values-sk/translations.xml +++ b/features/ftue/impl/src/main/res/values-sk/translations.xml @@ -1,7 +1,18 @@ + "Nemôžete potvrdiť?" + "Vytvoriť nový kľúč na obnovenie" + "Ak chcete nastaviť zabezpečené správy, overte toto zariadenie." + "Potvrďte, že ste to vy" + "Použite iné zariadenie" + "Použiť kľúč na obnovenie" + "Teraz môžete bezpečne čítať alebo odosielať správy a tomuto zariadeniu môže dôverovať aj ktokoľvek, s kým konverzujete." + "Zariadenie overené" + "Použite iné zariadenie" + "Čaká sa na druhom zariadení…" "Svoje nastavenia môžete neskôr zmeniť." "Povoľte oznámenia a nikdy nezmeškajte žiadnu správu" + "Zadajte kľúč na obnovenie" "Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku." "História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii." "Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení." diff --git a/features/ftue/impl/src/main/res/values-sv/translations.xml b/features/ftue/impl/src/main/res/values-sv/translations.xml index 31823cfaab3..ffaf682393f 100644 --- a/features/ftue/impl/src/main/res/values-sv/translations.xml +++ b/features/ftue/impl/src/main/res/values-sv/translations.xml @@ -1,7 +1,18 @@ + "Kan du inte bekräfta?" + "Skapa en ny återställningsnyckel" + "Verifiera den här enheten för att konfigurera säkra meddelanden." + "Bekräfta att det är du" + "Använd en annan enhet" + "Använd återställningsnyckel" + "Nu kan du läsa eller skicka meddelanden säkert, och alla du chattar med kan också lita på den här enheten." + "Enhet verifierad" + "Använd en annan enhet" + "Väntar på annan enhet …" "Du kan ändra dina inställningar senare." "Tillåt aviseringar och missa aldrig ett meddelande" + "Ange återställningsnyckel" "Samtal, omröstningar, sökning och mer kommer att läggas till senare i år." "Meddelandehistorik för krypterade rum är inte tillgänglig än." "Vi vill gärna höra från dig, låt oss veta vad du tycker via inställningssidan." diff --git a/features/ftue/impl/src/main/res/values-tr/translations.xml b/features/ftue/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..dbeb65832cc --- /dev/null +++ b/features/ftue/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,22 @@ + + + "Onaylayamıyor musunuz?" + "Yeni bir kurtarma anahtarı oluştur" + "Güvenli mesajlaşmayı ayarlamak için bu cihazı doğrulayın." + "Kimliğinizi doğrulayın" + "Başka bir cihaz kullan" + "Kurtarma anahtarı kullan" + "Artık mesajları güvenli bir şekilde okuyabilir veya gönderebilirsiniz ve sohbet ettiğiniz herkes de bu cihaza güvenebilir." + "Cihaz doğrulandı" + "Başka bir cihaz kullan" + "Diğer cihazda bekleniyor…" + "Ayarlarınızı daha sonra değiştirebilirsiniz." + "Bildirimlere izin verin ve hiçbir mesajı kaçırmayın" + "Kurtarma anahtarını girin" + "Çağrılar, anketler, arama ve daha fazlası bu yıl içinde eklenecek." + "Şifrelenmiş odalar için mesaj geçmişi henüz mevcut değil." + "Düşüncelerinizi bizimle paylaşmanızı çok isteriz. Ayarlar sayfasından düşüncelerinizi bize iletin." + "Hadi başlayalım!" + "İşte bilmeniz gerekenler:" + "%1$s hoş geldiniz!" + diff --git a/features/ftue/impl/src/main/res/values-uk/translations.xml b/features/ftue/impl/src/main/res/values-uk/translations.xml index 920f6d3d1c0..6dc00561499 100644 --- a/features/ftue/impl/src/main/res/values-uk/translations.xml +++ b/features/ftue/impl/src/main/res/values-uk/translations.xml @@ -1,7 +1,18 @@ + "Не можете підтвердити?" + "Створити новий ключ відновлення" + "Верифікуйте цей пристрій, щоб налаштувати безпечний обмін повідомленнями." + "Підтвердьте, що це ви" + "Використовуйте інший пристрій" + "Використовуйте ключ відновлення" + "Тепер ви можете безпечно читати або надсилати повідомлення, і кожен, з ким ви спілкуєтесь, також може довіряти цьому пристрою." + "Пристрій перевірено" + "Використовуйте інший пристрій" + "Чекає на інше пристрій…" "Ви можете змінити свої налаштування пізніше." "Дозволити сповіщення і ніколи не пропускати повідомлення" + "Введіть ключ відновлення" "Виклики, опитування, пошук тощо будуть додані пізніше цього року." "Історія повідомлень для зашифрованих кімнат ще недоступна." "Ми хотіли б почути вас, розкажіть нам ваші враження та ідеї щодо застосунку на сторінці налаштувань." diff --git a/features/ftue/impl/src/main/res/values-ur/translations.xml b/features/ftue/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..69882e87d0f --- /dev/null +++ b/features/ftue/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,22 @@ + + + "تصدیق نہیں کر سکتے؟" + "ایک نئی بازیابی کلید تخلیق کریں" + "محفوظ پیغام رسانی ترتیب دینے کیلئے اس آلے کی توثیق کریں۔" + "اپنی شناخت کی تصدیق کریں" + "دوسرا آلہ استعمال کریں" + "بازیابی کلید استعمال کریں" + "اب آپ محفوظ طریقے سے پیغامات پڑھ یا بھیج سکتے ہیں، اور جسکے ساتھ آپ گفتگو کرتے ہیں وہ بھی اس آلہ پر بھروسہ کر سکتا ہے۔" + "آلہ توثیق شدہ" + "دوسرا آلہ استعمال کریں" + "دوسرے آلہ پر منتظر…" + "آپ بعد میں اپنی ترتیبات تبدیل کر سکتے ہیں۔" + "اطلاعات کی اجازت دیں اور کبھی بھی کسی پیغام سے محروم نہ ہوں۔" + "بازیابی کلید درج کریں" + "مکالمات، رائے شماری، تلاش اور مزید بعد میں اس سال شامل کیا جائے گا۔" + "مرموز کردہ کمروں کیلئے سرگزشتِ پیغام ابھی دستیاب نہیں ہے" + "ہم آپ سے سننا پسند کریں گے، ترتیبات کے صفحہ کے ذریعے ہمیں بتائیں کہ آپ کیا سوچتے ہیں۔" + "چلیں!" + "یہ ہے جو آپ کو جاننے کی ضرورت ہے:" + "%1$s میں خوش آمدید!" + 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 34fb4fbe35c..5658f306160 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 @@ -1,7 +1,18 @@ + "無法確認?" + "建立新的復原金鑰" + "驗證這部裝置以設定安全通訊。" + "確認這是你本人" + "使用另一部裝置" + "使用復原金鑰" + "您可以安全地讀取和發送訊息了,與您聊天的人也可以信任這部裝置。" + "裝置已驗證" + "使用另一部裝置" + "正在等待其他裝置…" "您稍後仍可變更設定。" "允許通知,永遠不會錯誤任何訊息" + "輸入復原金鑰" "通話、投票、搜尋等更多功能將在今年登場。" "在這次的更新,您無法查看聊天室內被加密的歷史訊息。" "我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。" 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 c98be668674..88b8b4252e7 100644 --- a/features/ftue/impl/src/main/res/values-zh/translations.xml +++ b/features/ftue/impl/src/main/res/values-zh/translations.xml @@ -1,7 +1,18 @@ + "无法确认?" + "创建新的恢复密钥" + "验证此设备以开始安全地收发消息。" + "确认这是你" + "使用其他设备" + "使用恢复密钥" + "现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。" + "设备已验证" + "使用其他设备" + "正在等待其他设备……" "您可以稍后更改设置。" "允许通知,绝不错过任何消息" + "输入恢复密钥" "今年晚些时候将增加通话、投票、搜索等功能。" "加密聊天室的消息历史记录尚不可用。" "我们很乐意听取您的意见,请通过设置页面告诉我们您的想法。" diff --git a/features/ftue/impl/src/main/res/values/localazy.xml b/features/ftue/impl/src/main/res/values/localazy.xml index 3e8c86b7614..47214a93041 100644 --- a/features/ftue/impl/src/main/res/values/localazy.xml +++ b/features/ftue/impl/src/main/res/values/localazy.xml @@ -1,7 +1,18 @@ + "Can\'t confirm?" + "Create a new recovery key" + "Verify this device to set up secure messaging." + "Confirm your identity" + "Use another device" + "Use recovery key" + "Now you can read or send messages securely, and anyone you chat with can also trust this device." + "Device verified" + "Use another device" + "Waiting on other device…" "You can change your settings later." "Allow notifications and never miss a message" + "Enter recovery key" "Calls, polls, search and more will be added later this year." "Message history for encrypted rooms isn’t available yet." "We’d love to hear from you, let us know what you think via the settings page." diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt index 9ee2f519946..4a05433e5e3 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt @@ -19,7 +19,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.permissions.api.PermissionStateProvider -import io.element.android.libraries.permissions.impl.FakePermissionStateProvider +import io.element.android.libraries.permissions.test.FakePermissionStateProvider import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.services.analytics.api.AnalyticsService diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt index 55bc1b92438..fbdc9d1dd17 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt @@ -14,7 +14,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.api.PermissionsPresenter -import io.element.android.libraries.permissions.impl.FakePermissionStateProvider +import io.element.android.libraries.permissions.test.FakePermissionStateProvider import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt new file mode 100644 index 00000000000..3801001ed41 --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt @@ -0,0 +1,64 @@ +/* + * 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.ftue.impl.sessionverification.choosemode + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.logout.api.direct.DirectLogoutEvents +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.logout.api.direct.aDirectLogoutState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +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.test.runTest +import org.junit.Test + +class ChooseSessionVerificationModePresenterTest { + @Test + fun `initial state - is relayed from EncryptionService`() = runTest { + val encryptionService = FakeEncryptionService().apply { + // Is last device + emitIsLastDevice(true) + // Can enter recovery key + emitRecoveryState(RecoveryState.INCOMPLETE) + } + val presenter = createPresenter(encryptionService = encryptionService) + presenter.test { + awaitItem().run { + assertThat(isLastDevice).isTrue() + assertThat(canEnterRecoveryKey).isTrue() + assertThat(directLogoutState.logoutAction.isUninitialized()).isTrue() + } + } + } + + @Test + fun `sing out action triggers a direct logout`() = runTest { + val logoutEventRecorder = lambdaRecorder {} + val logoutPresenter = Presenter { + aDirectLogoutState(eventSink = logoutEventRecorder) + } + val presenter = createPresenter(directLogoutPresenter = logoutPresenter) + presenter.test { + val initial = awaitItem() + initial.eventSink(ChooseSelfVerificationModeEvent.SignOut) + + logoutEventRecorder.assertions().isCalledOnce().with(value(DirectLogoutEvents.Logout(ignoreSdkError = false))) + } + } + + private fun createPresenter( + encryptionService: FakeEncryptionService = FakeEncryptionService(), + directLogoutPresenter: Presenter = Presenter { aDirectLogoutState() } + ) = ChooseSelfVerificationModePresenter( + encryptionService = encryptionService, + directLogoutPresenter = directLogoutPresenter, + ) +} 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 new file mode 100644 index 00000000000..b89e3e42bd9 --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt @@ -0,0 +1,94 @@ +/* + * 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.ftue.impl.sessionverification.choosemode + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.ftue.impl.R +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`() { + ensureCalledOnce { callback -> + rule.setChooseSelfVerificationModeView( + aChooseSelfVerificationModeState(), + onLearnMoreClick = callback, + ) + rule.clickOn(CommonStrings.action_learn_more) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on use another device calls the callback`() { + ensureCalledOnce { callback -> + rule.setChooseSelfVerificationModeView( + aChooseSelfVerificationModeState(isLastDevice = false), + onUseAnotherDevice = callback, + ) + rule.clickOn(R.string.screen_identity_use_another_device) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on enter recovery key calls the callback`() { + ensureCalledOnce { callback -> + rule.setChooseSelfVerificationModeView( + aChooseSelfVerificationModeState(canEnterRecoveryKey = true), + onEnterRecoveryKey = callback, + ) + rule.clickOn(R.string.screen_session_verification_enter_recovery_key) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on cannot confirm calls the reset keys callback`() { + ensureCalledOnce { callback -> + rule.setChooseSelfVerificationModeView( + aChooseSelfVerificationModeState(), + onResetKey = callback, + ) + rule.clickOn(R.string.screen_identity_confirmation_cannot_confirm) + } + } + + private fun AndroidComposeTestRule.setChooseSelfVerificationModeView( + state: ChooseSelfVerificationModeState, + onLearnMoreClick: () -> Unit = EnsureNeverCalled(), + onUseAnotherDevice: () -> Unit = EnsureNeverCalled(), + onResetKey: () -> Unit = EnsureNeverCalled(), + onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), + ) { + setContent { + ChooseSelfVerificationModeView( + state = state, + onLearnMore = onLearnMoreClick, + onUseAnotherDevice = onUseAnotherDevice, + onResetKey = onResetKey, + onUseRecoveryKey = onEnterRecoveryKey, + ) + } + } +} diff --git a/features/home/api/build.gradle.kts b/features/home/api/build.gradle.kts new file mode 100644 index 00000000000..c1aee95e670 --- /dev/null +++ b/features/home/api/build.gradle.kts @@ -0,0 +1,18 @@ +/* + * 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. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.home.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) +} diff --git a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt new file mode 100644 index 00000000000..0f6ee581bba --- /dev/null +++ b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt @@ -0,0 +1,33 @@ +/* + * 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. + */ + +package io.element.android.features.home.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 + +interface HomeEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onRoomClick(roomId: RoomId) + fun onCreateRoomClick() + fun onSettingsClick() + fun onSetUpRecoveryClick() + fun onSessionConfirmRecoveryKeyClick() + fun onRoomSettingsClick(roomId: RoomId) + fun onReportBugClick() + fun onLogoutForNativeSlidingSyncMigrationNeeded() + } +} diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts new file mode 100644 index 00000000000..88184bcbcd9 --- /dev/null +++ b/features/home/impl/build.gradle.kts @@ -0,0 +1,82 @@ +import extension.setupAnvil + +/* + * Copyright 2022-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. + */ + +plugins { + id("io.element.android-compose-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.home.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +setupAnvil() + +dependencies { + implementation(projects.appconfig) + 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.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.testtags) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.dateformatter.api) + implementation(projects.libraries.eventformatter.api) + implementation(projects.libraries.indicator.api) + implementation(projects.libraries.deeplink) + implementation(projects.libraries.fullscreenintent.api) + implementation(projects.libraries.permissions.api) + implementation(projects.libraries.permissions.noop) + implementation(projects.libraries.preferences.api) + implementation(projects.libraries.push.api) + implementation(projects.features.invite.api) + implementation(projects.features.networkmonitor.api) + implementation(projects.features.logout.api) + implementation(projects.features.leaveroom.api) + implementation(projects.features.rageshake.api) + implementation(projects.services.analytics.api) + implementation(libs.androidx.datastore.preferences) + implementation(libs.haze) + implementation(libs.haze.materials) + implementation(projects.features.reportroom.api) + api(projects.features.home.api) + + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) + testImplementation(projects.features.invite.test) + testImplementation(projects.features.logout.test) + testImplementation(projects.features.networkmonitor.test) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.featureflag.test) + testImplementation(projects.libraries.dateformatter.test) + testImplementation(projects.libraries.eventformatter.test) + testImplementation(projects.libraries.indicator.test) + testImplementation(projects.libraries.permissions.noop) + testImplementation(projects.libraries.permissions.test) + testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.push.test) + testImplementation(projects.services.analytics.test) + testImplementation(projects.services.toolbox.test) + testImplementation(projects.tests.testutils) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPoint.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPoint.kt new file mode 100644 index 00000000000..3fe5c533d74 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPoint.kt @@ -0,0 +1,35 @@ +/* + * 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. + */ + +package io.element.android.features.home.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.home.api.HomeEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultHomeEntryPoint @Inject constructor() : HomeEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): HomeEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : HomeEntryPoint.NodeBuilder { + override fun callback(callback: HomeEntryPoint.Callback): HomeEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt new file mode 100644 index 00000000000..4632e40d5a9 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt @@ -0,0 +1,12 @@ +/* + * 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.home.impl + +sealed interface HomeEvents { + data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents +} 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 new file mode 100644 index 00000000000..4a7e5eb258c --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt @@ -0,0 +1,166 @@ +/* + * 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. + */ + +package io.element.android.features.home.impl + +import android.app.Activity +import android.os.Parcelable +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.lifecycle.subscribe +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.MobileScreen +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.home.api.HomeEntryPoint +import io.element.android.features.home.impl.components.RoomListMenuAction +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView +import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint +import io.element.android.features.logout.api.direct.DirectLogoutView +import io.element.android.features.reportroom.api.ReportRoomEntryPoint +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class HomeFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: HomePresenter, + private val inviteFriendsUseCase: InviteFriendsUseCase, + private val analyticsService: AnalyticsService, + private val acceptDeclineInviteView: AcceptDeclineInviteView, + private val directLogoutView: DirectLogoutView, + private val reportRoomEntryPoint: ReportRoomEntryPoint, + private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + init { + lifecycle.subscribe( + onResume = { + analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Home)) + } + ) + } + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data class ReportRoom(val roomId: RoomId) : NavTarget + + @Parcelize + data class DeclineInviteAndBlockUser(val inviteData: InviteData) : NavTarget + } + + private fun onRoomClick(roomId: RoomId) { + plugins().forEach { it.onRoomClick(roomId) } + } + + private fun onOpenSettings() { + plugins().forEach { it.onSettingsClick() } + } + + private fun onCreateRoomClick() { + plugins().forEach { it.onCreateRoomClick() } + } + + private fun onSetUpRecoveryClick() { + plugins().forEach { it.onSetUpRecoveryClick() } + } + + private fun onSessionConfirmRecoveryKeyClick() { + plugins().forEach { it.onSessionConfirmRecoveryKeyClick() } + } + + private fun onRoomSettingsClick(roomId: RoomId) { + plugins().forEach { it.onRoomSettingsClick(roomId) } + } + + private fun onReportRoomClick(roomId: RoomId) { + backstack.push(NavTarget.ReportRoom(roomId)) + } + + private fun onDeclineInviteAndBlockUserClick(roomSummary: RoomListRoomSummary) { + backstack.push(NavTarget.DeclineInviteAndBlockUser(roomSummary.toInviteData())) + } + + private fun onMenuActionClick(activity: Activity, roomListMenuAction: RoomListMenuAction) { + when (roomListMenuAction) { + RoomListMenuAction.InviteFriends -> { + inviteFriendsUseCase.execute(activity) + } + RoomListMenuAction.ReportBug -> { + plugins().forEach { it.onReportBugClick() } + } + } + } + + fun rootNode(buildContext: BuildContext): Node { + return node(buildContext) { modifier -> + val state = presenter.present() + val activity = requireNotNull(LocalActivity.current) + + HomeView( + homeState = state, + onRoomClick = this::onRoomClick, + onSettingsClick = this::onOpenSettings, + onCreateRoomClick = this::onCreateRoomClick, + onSetUpRecoveryClick = this::onSetUpRecoveryClick, + onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick, + onRoomSettingsClick = this::onRoomSettingsClick, + onMenuActionClick = { onMenuActionClick(activity, it) }, + onReportRoomClick = this::onReportRoomClick, + onDeclineInviteAndBlockUser = this::onDeclineInviteAndBlockUserClick, + modifier = modifier, + ) { + acceptDeclineInviteView.Render( + state = state.roomListState.acceptDeclineInviteState, + onAcceptInviteSuccess = this::onRoomClick, + onDeclineInviteSuccess = { }, + modifier = Modifier + ) + } + + directLogoutView.Render(state.directLogoutState) + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView() + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.ReportRoom -> reportRoomEntryPoint.createNode(this, buildContext, navTarget.roomId) + is NavTarget.DeclineInviteAndBlockUser -> declineInviteAndBlockUserEntryPoint.createNode(this, buildContext, navTarget.inviteData) + NavTarget.Root -> rootNode(buildContext) + } + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt new file mode 100644 index 00000000000..5254648f24e --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt @@ -0,0 +1,38 @@ +/* + * 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.home.impl + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import io.element.android.compound.tokens.generated.CompoundIcons + +enum class HomeNavigationBarItem( + @StringRes + val labelRes: Int, +) { + Chats( + labelRes = R.string.screen_roomlist_main_space_title + ), + Spaces( + // TODO Create a new entry in Localazy + labelRes = R.string.screen_roomlist_main_space_title + ); + + @Composable + fun icon() = when (this) { + Chats -> CompoundIcons.ChatSolid() + // TODO Spaces -> CompoundIcons.Workspace() + Spaces -> CompoundIcons.Code() + } + + companion object { + fun from(index: Int): HomeNavigationBarItem { + return entries.getOrElse(index) { Chats } + } + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt new file mode 100644 index 00000000000..894ed29e8d8 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -0,0 +1,87 @@ +/* + * 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.home.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.features.home.impl.roomlist.RoomListState +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +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.indicator.api.IndicatorService +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.sync.SyncService +import javax.inject.Inject + +class HomePresenter @Inject constructor( + private val client: MatrixClient, + private val syncService: SyncService, + private val snackbarDispatcher: SnackbarDispatcher, + private val indicatorService: IndicatorService, + private val roomListPresenter: Presenter, + private val logoutPresenter: Presenter, + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, + private val featureFlagService: FeatureFlagService, +) : Presenter { + @Composable + override fun present(): HomeState { + val matrixUser = client.userProfile.collectAsState() + val isOnline by syncService.isOnline.collectAsState() + val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } + val roomListState = roomListPresenter.present() + val isSpaceFeatureEnabled by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space) + }.collectAsState(initial = false) + var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) } + val currentHomeNavigationBarItem by remember { + derivedStateOf { + HomeNavigationBarItem.from(currentHomeNavigationBarItemOrdinal) + } + } + LaunchedEffect(Unit) { + // Force a refresh of the profile + client.getUserProfile() + } + // Avatar indicator + val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() + val directLogoutState = logoutPresenter.present() + + fun handleEvents(event: HomeEvents) { + when (event) { + is HomeEvents.SelectHomeNavigationBarItem -> { + currentHomeNavigationBarItemOrdinal = event.item.ordinal + } + } + } + + val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() + return HomeState( + matrixUser = matrixUser.value, + showAvatarIndicator = showAvatarIndicator, + hasNetworkConnection = isOnline, + currentHomeNavigationBarItem = currentHomeNavigationBarItem, + roomListState = roomListState, + snackbarMessage = snackbarMessage, + canReportBug = canReportBug, + directLogoutState = directLogoutState, + isSpaceFeatureEnabled = isSpaceFeatureEnabled, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt new file mode 100644 index 00000000000..5e6c16d2e42 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package io.element.android.features.home.impl + +import androidx.compose.runtime.Immutable +import io.element.android.features.home.impl.roomlist.RoomListState +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +import io.element.android.libraries.matrix.api.user.MatrixUser + +@Immutable +data class HomeState( + val matrixUser: MatrixUser, + val showAvatarIndicator: Boolean, + val hasNetworkConnection: Boolean, + val currentHomeNavigationBarItem: HomeNavigationBarItem, + val roomListState: RoomListState, + val snackbarMessage: SnackbarMessage?, + val canReportBug: Boolean, + val directLogoutState: DirectLogoutState, + val isSpaceFeatureEnabled: Boolean, + val eventSink: (HomeEvents) -> Unit, +) { + val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt new file mode 100644 index 00000000000..7a50296e170 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt @@ -0,0 +1,69 @@ +/* + * 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.home.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.home.impl.roomlist.RoomListState +import io.element.android.features.home.impl.roomlist.RoomListStateProvider +import io.element.android.features.home.impl.roomlist.aRoomListState +import io.element.android.features.home.impl.roomlist.aRoomsContentState +import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList +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.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.ui.strings.CommonStrings + +open class HomeStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aHomeState(), + aHomeState(hasNetworkConnection = false), + aHomeState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), + aHomeState( + isSpaceFeatureEnabled = true, + roomListState = aRoomListState( + // Add more rooms to see the blur effect under the NavigationBar + contentState = aRoomsContentState( + summaries = generateRoomListRoomSummaryList(), + ) + ), + ), + aHomeState( + isSpaceFeatureEnabled = true, + currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces, + ), + ) + RoomListStateProvider().values.map { + aHomeState(roomListState = it) + } +} + +internal fun aHomeState( + matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), + showAvatarIndicator: Boolean = false, + hasNetworkConnection: Boolean = true, + snackbarMessage: SnackbarMessage? = null, + currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats, + roomListState: RoomListState = aRoomListState(), + canReportBug: Boolean = true, + isSpaceFeatureEnabled: Boolean = false, + directLogoutState: DirectLogoutState = aDirectLogoutState(), + eventSink: (HomeEvents) -> Unit = {} +) = HomeState( + matrixUser = matrixUser, + showAvatarIndicator = showAvatarIndicator, + hasNetworkConnection = hasNetworkConnection, + snackbarMessage = snackbarMessage, + canReportBug = canReportBug, + directLogoutState = directLogoutState, + currentHomeNavigationBarItem = currentHomeNavigationBarItem, + roomListState = roomListState, + isSpaceFeatureEnabled = isSpaceFeatureEnabled, + eventSink = eventSink, +) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt new file mode 100644 index 00000000000..fb5b9c9ae37 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -0,0 +1,293 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalHazeMaterialsApi::class) + +package io.element.android.features.home.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.chrisbanes.haze.rememberHazeState +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.home.impl.components.RoomListContentView +import io.element.android.features.home.impl.components.RoomListMenuAction +import io.element.android.features.home.impl.components.RoomListTopBar +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.roomlist.RoomListContextMenu +import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu +import io.element.android.features.home.impl.roomlist.RoomListEvents +import io.element.android.features.home.impl.roomlist.RoomListState +import io.element.android.features.home.impl.search.RoomListSearchView +import io.element.android.features.leaveroom.api.LeaveRoomView +import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer +import io.element.android.libraries.androidutils.throttler.FirstThrottler +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.FloatingActionButton +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.NavigationBar +import io.element.android.libraries.designsystem.theme.components.NavigationBarItem +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.snackbar.SnackbarHost +import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState +import io.element.android.libraries.matrix.api.core.RoomId + +@Composable +fun HomeView( + homeState: HomeState, + onRoomClick: (RoomId) -> Unit, + onSettingsClick: () -> Unit, + onSetUpRecoveryClick: () -> Unit, + onConfirmRecoveryKeyClick: () -> Unit, + onCreateRoomClick: () -> Unit, + onRoomSettingsClick: (roomId: RoomId) -> Unit, + onMenuActionClick: (RoomListMenuAction) -> Unit, + onReportRoomClick: (roomId: RoomId) -> Unit, + onDeclineInviteAndBlockUser: (roomSummary: RoomListRoomSummary) -> Unit, + modifier: Modifier = Modifier, + acceptDeclineInviteView: @Composable () -> Unit, +) { + val state: RoomListState = homeState.roomListState + val coroutineScope = rememberCoroutineScope() + val firstThrottler = remember { FirstThrottler(300, coroutineScope) } + + ConnectivityIndicatorContainer( + modifier = modifier, + isOnline = homeState.hasNetworkConnection, + ) { topPadding -> + Box { + if (state.contextMenu is RoomListState.ContextMenu.Shown) { + RoomListContextMenu( + contextMenu = state.contextMenu, + canReportRoom = state.canReportRoom, + eventSink = state.eventSink, + onRoomSettingsClick = onRoomSettingsClick, + onReportRoomClick = onReportRoomClick, + ) + } + if (state.declineInviteMenu is RoomListState.DeclineInviteMenu.Shown) { + RoomListDeclineInviteMenu( + menu = state.declineInviteMenu, + canReportRoom = state.canReportRoom, + eventSink = state.eventSink, + onDeclineAndBlockClick = onDeclineInviteAndBlockUser, + ) + } + + LeaveRoomView(state = state.leaveRoomState) + + HomeScaffold( + state = homeState, + onSetUpRecoveryClick = onSetUpRecoveryClick, + onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, + onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) }, + onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() }, + onCreateRoomClick = { if (firstThrottler.canHandle()) onCreateRoomClick() }, + onMenuActionClick = onMenuActionClick, + modifier = Modifier.padding(top = topPadding), + ) + // This overlaid view will only be visible when state.displaySearchResults is true + RoomListSearchView( + state = state.searchState, + eventSink = state.eventSink, + hideInvitesAvatars = state.hideInvitesAvatars, + onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) }, + modifier = Modifier + .statusBarsPadding() + .padding(top = topPadding) + .fillMaxSize() + .background(ElementTheme.colors.bgCanvasDefault) + ) + acceptDeclineInviteView() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HomeScaffold( + state: HomeState, + onSetUpRecoveryClick: () -> Unit, + onConfirmRecoveryKeyClick: () -> Unit, + onRoomClick: (RoomId) -> Unit, + onOpenSettings: () -> Unit, + onCreateRoomClick: () -> Unit, + onMenuActionClick: (RoomListMenuAction) -> Unit, + modifier: Modifier = Modifier, +) { + fun onRoomClick(room: RoomListRoomSummary) { + onRoomClick(room.roomId) + } + + val appBarState = rememberTopAppBarState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState) + val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) + val roomListState: RoomListState = state.roomListState + + BackHandler( + enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats, + ) { + state.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats)) + } + + val hazeState = rememberHazeState() + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + RoomListTopBar( + title = stringResource(state.currentHomeNavigationBarItem.labelRes), + matrixUser = state.matrixUser, + showAvatarIndicator = state.showAvatarIndicator, + areSearchResultsDisplayed = roomListState.searchState.isSearchActive, + onToggleSearch = { roomListState.eventSink(RoomListEvents.ToggleSearchResults) }, + onMenuActionClick = onMenuActionClick, + onOpenSettings = onOpenSettings, + scrollBehavior = scrollBehavior, + displayMenuItems = state.displayActions, + displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats, + filtersState = roomListState.filtersState, + canReportBug = state.canReportBug, + ) + }, + bottomBar = { + if (state.isSpaceFeatureEnabled) { + NavigationBar( + containerColor = Color.Transparent, + modifier = Modifier + .hazeEffect( + state = hazeState, + style = HazeMaterials.regular(), + ) + ) { + HomeNavigationBarItem.entries.forEach { item -> + NavigationBarItem( + selected = state.currentHomeNavigationBarItem == item, + onClick = { + state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item)) + }, + icon = { + Icon( + imageVector = item.icon(), + contentDescription = null + ) + }, + label = { + Text(stringResource(item.labelRes)) + } + ) + } + } + } + }, + content = { padding -> + when (state.currentHomeNavigationBarItem) { + HomeNavigationBarItem.Chats -> { + RoomListContentView( + contentState = roomListState.contentState, + filtersState = roomListState.filtersState, + hideInvitesAvatars = roomListState.hideInvitesAvatars, + eventSink = roomListState.eventSink, + onSetUpRecoveryClick = onSetUpRecoveryClick, + onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, + onRoomClick = ::onRoomClick, + onCreateRoomClick = onCreateRoomClick, + // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80, + // and include provided bottom padding + contentBottomPadding = 80.dp + padding.calculateBottomPadding(), + modifier = Modifier + .padding( + top = padding.calculateTopPadding(), + bottom = 0.dp, + start = padding.calculateStartPadding(LocalLayoutDirection.current), + end = padding.calculateEndPadding(LocalLayoutDirection.current), + ) + .consumeWindowInsets(padding) + .hazeSource(state = hazeState) + ) + } + HomeNavigationBarItem.Spaces -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .consumeWindowInsets(padding) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = "Spaces are coming soon!", + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + ) + } + } + } + }, + floatingActionButton = { + if (state.displayActions) { + FloatingActionButton( + containerColor = ElementTheme.colors.iconPrimary, + onClick = onCreateRoomClick + ) { + Icon( + imageVector = CompoundIcons.Plus(), + contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message), + tint = ElementTheme.colors.iconOnSolidPrimary, + ) + } + } + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) +} + +internal fun RoomListRoomSummary.contentType() = displayType.ordinal + +@PreviewsDayNight +@Composable +internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state: HomeState) = ElementPreview { + HomeView( + homeState = state, + onRoomClick = {}, + onSettingsClick = {}, + onSetUpRecoveryClick = {}, + onConfirmRecoveryKeyClick = {}, + onCreateRoomClick = {}, + onRoomSettingsClick = {}, + onReportRoomClick = {}, + onMenuActionClick = {}, + onDeclineInviteAndBlockUser = {}, + acceptDeclineInviteView = {}, + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BannerPadding.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BannerPadding.kt similarity index 88% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BannerPadding.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BannerPadding.kt index a609d5dc7d2..389c2aecfb4 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BannerPadding.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BannerPadding.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.components +package io.element.android.features.home.impl.components import androidx.compose.foundation.layout.padding import androidx.compose.ui.Modifier diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt new file mode 100644 index 00000000000..017ca9592d6 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt @@ -0,0 +1,45 @@ +/* + * 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.home.impl.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.features.home.impl.R +import io.element.android.libraries.designsystem.components.Announcement +import io.element.android.libraries.designsystem.components.AnnouncementType +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.push.api.battery.BatteryOptimizationEvents +import io.element.android.libraries.push.api.battery.BatteryOptimizationState +import io.element.android.libraries.push.api.battery.aBatteryOptimizationState + +@Composable +internal fun BatteryOptimizationBanner( + state: BatteryOptimizationState, + modifier: Modifier = Modifier, +) { + Announcement( + modifier = modifier.roomListBannerPadding(), + title = stringResource(R.string.banner_battery_optimization_title_android), + description = stringResource(R.string.banner_battery_optimization_content_android), + type = AnnouncementType.Actionable( + actionText = stringResource(R.string.banner_battery_optimization_submit_android), + onActionClick = { state.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) }, + onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) }, + ), + ) +} + +@PreviewsDayNight +@Composable +internal fun BatteryOptimizationBannerPreview() = ElementPreview { + BatteryOptimizationBanner( + state = aBatteryOptimizationState(), + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/ConfirmRecoveryKeyBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/ConfirmRecoveryKeyBanner.kt similarity index 92% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/ConfirmRecoveryKeyBanner.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/ConfirmRecoveryKeyBanner.kt index 3ae7750fa35..c833e2914a5 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/ConfirmRecoveryKeyBanner.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/ConfirmRecoveryKeyBanner.kt @@ -5,12 +5,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.components +package io.element.android.features.home.impl.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import io.element.android.features.roomlist.impl.R +import io.element.android.features.home.impl.R import io.element.android.libraries.designsystem.components.Announcement import io.element.android.libraries.designsystem.components.AnnouncementType import io.element.android.libraries.designsystem.preview.ElementPreview diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt similarity index 80% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt index 7e84a26ab3e..e89392da62b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt @@ -5,16 +5,17 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.components +package io.element.android.features.home.impl.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import io.element.android.features.roomlist.impl.R +import io.element.android.features.home.impl.R import io.element.android.libraries.designsystem.components.Announcement import io.element.android.libraries.designsystem.components.AnnouncementType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.ui.strings.CommonStrings @@ -29,8 +30,8 @@ fun FullScreenIntentPermissionBanner( description = stringResource(R.string.full_screen_intent_banner_message), type = AnnouncementType.Actionable( actionText = stringResource(CommonStrings.action_continue), - onDismissClick = state.dismissFullScreenIntentBanner, - onActionClick = state.openFullScreenIntentSettings, + onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvents.Dismiss) }, + onActionClick = { state.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) }, ), modifier = modifier.roomListBannerPadding(), ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt similarity index 83% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index 525b1413a92..f86bd400f83 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.components +package io.element.android.features.home.impl.components import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement @@ -31,21 +31,23 @@ 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 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.roomlist.impl.R -import io.element.android.features.roomlist.impl.RoomListContentState -import io.element.android.features.roomlist.impl.RoomListContentStateProvider -import io.element.android.features.roomlist.impl.RoomListEvents -import io.element.android.features.roomlist.impl.SecurityBannerState -import io.element.android.features.roomlist.impl.contentType -import io.element.android.features.roomlist.impl.filters.RoomListFilter -import io.element.android.features.roomlist.impl.filters.RoomListFiltersEmptyStateResources -import io.element.android.features.roomlist.impl.filters.RoomListFiltersState -import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState -import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.R +import io.element.android.features.home.impl.contentType +import io.element.android.features.home.impl.filters.RoomListFilter +import io.element.android.features.home.impl.filters.RoomListFiltersEmptyStateResources +import io.element.android.features.home.impl.filters.RoomListFiltersState +import io.element.android.features.home.impl.filters.aRoomListFiltersState +import io.element.android.features.home.impl.filters.selection.FilterSelectionState +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.model.RoomSummaryDisplayType +import io.element.android.features.home.impl.roomlist.RoomListContentState +import io.element.android.features.home.impl.roomlist.RoomListContentStateProvider +import io.element.android.features.home.impl.roomlist.RoomListEvents +import io.element.android.features.home.impl.roomlist.SecurityBannerState 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 @@ -59,12 +61,13 @@ import kotlinx.collections.immutable.ImmutableList fun RoomListContentView( contentState: RoomListContentState, filtersState: RoomListFiltersState, + hideInvitesAvatars: Boolean, eventSink: (RoomListEvents) -> Unit, onSetUpRecoveryClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit, onCreateRoomClick: () -> Unit, - onMigrateToNativeSlidingSyncClick: () -> Unit, + contentBottomPadding: Dp, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { @@ -86,12 +89,13 @@ fun RoomListContentView( is RoomListContentState.Rooms -> { RoomsView( state = contentState, + hideInvitesAvatars = hideInvitesAvatars, filtersState = filtersState, eventSink = eventSink, onSetUpRecoveryClick = onSetUpRecoveryClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, - onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, onRoomClick = onRoomClick, + contentBottomPadding = contentBottomPadding, ) } } @@ -148,7 +152,7 @@ private fun EmptyView( onDismissClick = { eventSink(RoomListEvents.DismissBanner) }, ) } - else -> Unit + SecurityBannerState.None -> Unit } } } @@ -157,12 +161,13 @@ private fun EmptyView( @Composable private fun RoomsView( state: RoomListContentState.Rooms, + hideInvitesAvatars: Boolean, filtersState: RoomListFiltersState, eventSink: (RoomListEvents) -> Unit, onSetUpRecoveryClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit, - onMigrateToNativeSlidingSyncClick: () -> Unit, + contentBottomPadding: Dp, modifier: Modifier = Modifier, ) { if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) { @@ -173,11 +178,12 @@ private fun RoomsView( } else { RoomsViewList( state = state, + hideInvitesAvatars = hideInvitesAvatars, eventSink = eventSink, onSetUpRecoveryClick = onSetUpRecoveryClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onRoomClick = onRoomClick, - onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, + contentBottomPadding = contentBottomPadding, modifier = modifier.fillMaxSize(), ) } @@ -186,11 +192,12 @@ private fun RoomsView( @Composable private fun RoomsViewList( state: RoomListContentState.Rooms, + hideInvitesAvatars: Boolean, eventSink: (RoomListEvents) -> Unit, onSetUpRecoveryClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit, - onMigrateToNativeSlidingSyncClick: () -> Unit, + contentBottomPadding: Dp, modifier: Modifier = Modifier, ) { val lazyListState = rememberLazyListState() @@ -209,8 +216,7 @@ private fun RoomsViewList( LazyColumn( state = lazyListState, modifier = modifier, - // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 - contentPadding = PaddingValues(bottom = 80.dp) + contentPadding = PaddingValues(bottom = contentBottomPadding) ) { when (state.securityBannerState) { SecurityBannerState.SetUpRecovery -> { @@ -230,18 +236,14 @@ private fun RoomsViewList( ) } } - SecurityBannerState.NeedsNativeSlidingSyncMigration -> { - item { - NativeSlidingSyncMigrationBanner( - onContinueClick = onMigrateToNativeSlidingSyncClick, - onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }, - ) - } - } SecurityBannerState.None -> if (state.fullScreenIntentPermissionsState.shouldDisplayBanner) { item { FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState) } + } else if (state.batteryOptimizationState.shouldDisplayBanner) { + item { + BatteryOptimizationBanner(state = state.batteryOptimizationState) + } } } @@ -253,6 +255,9 @@ private fun RoomsViewList( ) { index, room -> RoomSummaryRow( room = room, + hideInviteAvatars = hideInvitesAvatars, + isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE && + state.seenRoomInvites.contains(room.roomId), onClick = onRoomClick, eventSink = eventSink, ) @@ -312,13 +317,19 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr RoomListContentView( contentState = state, filtersState = aRoomListFiltersState( - filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) } + filterSelectionStates = RoomListFilter.entries.map { + FilterSelectionState( + filter = it, + isSelected = true + ) + } ), + hideInvitesAvatars = false, eventSink = {}, onSetUpRecoveryClick = {}, onConfirmRecoveryKeyClick = {}, onRoomClick = {}, onCreateRoomClick = {}, - onMigrateToNativeSlidingSyncClick = {}, + contentBottomPadding = 0.dp, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListMenuAction.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListMenuAction.kt similarity index 81% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListMenuAction.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListMenuAction.kt index 14f46098acb..9f17fc835e7 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListMenuAction.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListMenuAction.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.components +package io.element.android.features.home.impl.components enum class RoomListMenuAction { InviteFriends, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt similarity index 91% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt index f196ba3c196..8071dead3dd 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.components +package io.element.android.features.home.impl.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -34,20 +34,23 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import io.element.android.appconfig.RoomListConfig import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.roomlist.impl.R -import io.element.android.features.roomlist.impl.filters.RoomListFiltersState -import io.element.android.features.roomlist.impl.filters.RoomListFiltersView -import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState +import io.element.android.features.home.impl.R +import io.element.android.features.home.impl.filters.RoomListFiltersState +import io.element.android.features.home.impl.filters.RoomListFiltersView +import io.element.android.features.home.impl.filters.aRoomListFiltersState import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom 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.avatarBloom import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -75,6 +78,7 @@ private val avatarBloomSize = 430.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomListTopBar( + title: String, matrixUser: MatrixUser, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, @@ -85,9 +89,11 @@ fun RoomListTopBar( displayMenuItems: Boolean, displayFilters: Boolean, filtersState: RoomListFiltersState, + canReportBug: Boolean, modifier: Modifier = Modifier, ) { DefaultRoomListTopBar( + title = title, matrixUser = matrixUser, showAvatarIndicator = showAvatarIndicator, areSearchResultsDisplayed = areSearchResultsDisplayed, @@ -98,6 +104,7 @@ fun RoomListTopBar( displayMenuItems = displayMenuItems, displayFilters = displayFilters, filtersState = filtersState, + canReportBug = canReportBug, modifier = modifier, ) } @@ -105,6 +112,7 @@ fun RoomListTopBar( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun DefaultRoomListTopBar( + title: String, matrixUser: MatrixUser, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, @@ -115,6 +123,7 @@ private fun DefaultRoomListTopBar( displayMenuItems: Boolean, displayFilters: Boolean, filtersState: RoomListFiltersState, + canReportBug: Boolean, modifier: Modifier = Modifier, ) { // We need this to manually clip the top app bar in preview mode @@ -162,7 +171,7 @@ private fun DefaultRoomListTopBar( // Workaround to display a very subtle bloom for avatars with very soft colors Color(0xFFF9F9F9) } else { - ElementTheme.materialColors.background + ElementTheme.colors.bgCanvasDefault }, blurSize = DpSize(avatarBloomSize, avatarBloomSize), offset = DpOffset(24.dp, 24.dp + statusBarPadding), @@ -174,7 +183,7 @@ private fun DefaultRoomListTopBar( } else { DpSize.Unspecified }, - bottomSoftEdgeColor = ElementTheme.materialColors.background, + bottomSoftEdgeColor = ElementTheme.colors.bgCanvasDefault, bottomSoftEdgeAlpha = if (displayFilters) { 1f } else { @@ -190,7 +199,12 @@ private fun DefaultRoomListTopBar( scrolledContainerColor = Color.Transparent, ), title = { - Text(text = stringResource(id = R.string.screen_roomlist_main_space_title)) + Text( + modifier = Modifier.semantics { + heading() + }, + text = title, + ) }, navigationIcon = { NavigationIcon( @@ -233,13 +247,13 @@ private fun DefaultRoomListTopBar( leadingIcon = { Icon( imageVector = CompoundIcons.ShareAndroid(), - tint = ElementTheme.materialColors.secondary, + tint = ElementTheme.colors.iconSecondary, contentDescription = null, ) } ) } - if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM) { + if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM && canReportBug) { DropdownMenuItem( onClick = { showMenu = false @@ -249,7 +263,7 @@ private fun DefaultRoomListTopBar( leadingIcon = { Icon( imageVector = CompoundIcons.ChatProblem(), - tint = ElementTheme.materialColors.secondary, + tint = ElementTheme.colors.iconSecondary, contentDescription = null, ) } @@ -294,6 +308,7 @@ private fun NavigationIcon( Box { Avatar( avatarData = avatarData, + avatarType = AvatarType.User, contentDescription = stringResource(CommonStrings.common_settings), ) if (showAvatarIndicator) { @@ -310,6 +325,7 @@ private fun NavigationIcon( @Composable internal fun DefaultRoomListTopBarPreview() = ElementPreview { DefaultRoomListTopBar( + title = stringResource(R.string.screen_roomlist_main_space_title), matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), showAvatarIndicator = false, areSearchResultsDisplayed = false, @@ -319,6 +335,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview { displayMenuItems = true, displayFilters = true, filtersState = aRoomListFiltersState(), + canReportBug = true, onMenuActionClick = {}, ) } @@ -328,6 +345,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview { @Composable internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview { DefaultRoomListTopBar( + title = stringResource(R.string.screen_roomlist_main_space_title), matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), showAvatarIndicator = true, areSearchResultsDisplayed = false, @@ -337,6 +355,7 @@ internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview { displayMenuItems = true, displayFilters = true, filtersState = aRoomListFiltersState(), + canReportBug = true, onMenuActionClick = {}, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryPlaceholderRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryPlaceholderRow.kt similarity index 98% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryPlaceholderRow.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryPlaceholderRow.kt index af980c1b092..f07fe880b35 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryPlaceholderRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryPlaceholderRow.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.components +package io.element.android.features.home.impl.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt similarity index 84% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index 05ff1042e58..e5a85a7442d 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.components +package io.element.android.features.home.impl.components -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -24,7 +23,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -39,14 +37,15 @@ 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.roomlist.impl.R -import io.element.android.features.roomlist.impl.RoomListEvents -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider -import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType +import io.element.android.features.home.impl.R +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.model.RoomListRoomSummaryProvider +import io.element.android.features.home.impl.model.RoomSummaryDisplayType +import io.element.android.features.home.impl.roomlist.RoomListEvents import io.element.android.libraries.core.extensions.orEmpty import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom -import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar +import io.element.android.libraries.designsystem.components.avatar.Avatar +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.theme.components.Button @@ -58,7 +57,6 @@ import io.element.android.libraries.designsystem.theme.roomListRoomMessage import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate import io.element.android.libraries.designsystem.theme.roomListRoomName import io.element.android.libraries.designsystem.theme.unreadIndicator -import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.ui.components.InviteSenderView import io.element.android.libraries.matrix.ui.model.InviteSender @@ -70,6 +68,8 @@ internal val minHeight = 84.dp @Composable internal fun RoomSummaryRow( room: RoomListRoomSummary, + hideInviteAvatars: Boolean, + isInviteSeen: Boolean, onClick: (RoomListRoomSummary) -> Unit, eventSink: (RoomListEvents) -> Unit, modifier: Modifier = Modifier, @@ -82,18 +82,20 @@ internal fun RoomSummaryRow( RoomSummaryDisplayType.INVITE -> { RoomSummaryScaffoldRow( room = room, + hideAvatarImage = hideInviteAvatars, onClick = onClick, onLongClick = { Timber.d("Long click on invite room") }, ) { - InviteNameAndIndicatorRow(name = room.name) - InviteSubtitle(isDm = room.isDm, inviteSender = room.inviteSender, canonicalAlias = room.canonicalAlias) + InviteNameAndIndicatorRow(name = room.name, isInviteSeen = isInviteSeen) + InviteSubtitle(isDm = room.isDm, inviteSender = room.inviteSender) if (!room.isDm && room.inviteSender != null) { Spacer(modifier = Modifier.height(4.dp)) InviteSenderView( modifier = Modifier.fillMaxWidth(), inviteSender = room.inviteSender, + hideAvatarImage = hideInviteAvatars ) } Spacer(modifier = Modifier.height(12.dp)) @@ -102,7 +104,7 @@ internal fun RoomSummaryRow( eventSink(RoomListEvents.AcceptInvite(room)) }, onDeclineClick = { - eventSink(RoomListEvents.DeclineInvite(room)) + eventSink(RoomListEvents.ShowDeclineInviteMenu(room)) } ) } @@ -120,7 +122,7 @@ internal fun RoomSummaryRow( timestamp = room.timestamp, isHighlighted = room.isHighlighted ) - LastMessageAndIndicatorRow(room = room) + MessagePreviewAndIndicatorRow(room = room) } } RoomSummaryDisplayType.KNOCKED -> { @@ -147,7 +149,7 @@ internal fun RoomSummaryRow( Spacer(modifier = Modifier.height(4.dp)) } Text( - text = stringResource(id = R.string.screen_join_room_knock_sent_title), + text = stringResource(id = R.string.screen_roomlist_knock_event_sent_description), maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodyMdRegular, @@ -159,18 +161,19 @@ internal fun RoomSummaryRow( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun RoomSummaryScaffoldRow( room: RoomListRoomSummary, onClick: (RoomListRoomSummary) -> Unit, onLongClick: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, + hideAvatarImage: Boolean = false, content: @Composable ColumnScope.() -> Unit ) { val clickModifier = Modifier.combinedClickable( onClick = { onClick(room) }, onLongClick = { onLongClick(room) }, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), indication = ripple(), interactionSource = remember { MutableInteractionSource() } ) @@ -183,9 +186,13 @@ private fun RoomSummaryScaffoldRow( .padding(horizontal = 16.dp, vertical = 11.dp) .height(IntrinsicSize.Min), ) { - CompositeAvatar( + Avatar( avatarData = room.avatarData, - heroes = room.heroes, + avatarType = AvatarType.Room( + heroes = room.heroes, + isTombstoned = room.isTombstoned, + ), + hideImage = hideAvatarImage, ) Spacer(modifier = Modifier.width(16.dp)) Column( @@ -212,7 +219,7 @@ private fun NameAndTimestampRow( style = ElementTheme.typography.fontBodyLgMedium, text = name ?: stringResource(id = CommonStrings.common_no_room_name), fontStyle = FontStyle.Italic.takeIf { name == null }, - color = MaterialTheme.roomListRoomName(), + color = ElementTheme.roomListRoomName(), maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -223,7 +230,7 @@ private fun NameAndTimestampRow( color = if (isHighlighted) { ElementTheme.colors.unreadIndicator } else { - MaterialTheme.roomListRoomMessageDate() + ElementTheme.roomListRoomMessageDate() }, ) } @@ -233,13 +240,12 @@ private fun NameAndTimestampRow( private fun InviteSubtitle( isDm: Boolean, inviteSender: InviteSender?, - canonicalAlias: RoomAlias?, modifier: Modifier = Modifier ) { val subtitle = if (isDm) { inviteSender?.userId?.value } else { - canonicalAlias?.value + null } if (subtitle != null) { Text( @@ -247,14 +253,14 @@ private fun InviteSubtitle( maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.roomListRoomMessage(), + color = ElementTheme.roomListRoomMessage(), modifier = modifier, ) } } @Composable -private fun LastMessageAndIndicatorRow( +private fun MessagePreviewAndIndicatorRow( room: RoomListRoomSummary, modifier: Modifier = Modifier, ) { @@ -262,13 +268,16 @@ private fun LastMessageAndIndicatorRow( modifier = modifier.fillMaxWidth(), horizontalArrangement = spacedBy(28.dp) ) { - // Last Message - val attributedLastMessage = room.lastMessage as? AnnotatedString - ?: AnnotatedString(room.lastMessage.orEmpty().toString()) + val messagePreview = if (room.isTombstoned) { + stringResource(R.string.screen_roomlist_tombstoned_room_description) + } else { + room.lastMessage.orEmpty() + } + val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.toString()) Text( modifier = Modifier.weight(1f), - text = attributedLastMessage, - color = MaterialTheme.roomListRoomMessage(), + text = annotatedMessagePreview, + color = ElementTheme.roomListRoomMessage(), style = ElementTheme.typography.fontBodyMdRegular, minLines = 2, maxLines = 2, @@ -303,6 +312,7 @@ private fun LastMessageAndIndicatorRow( @Composable private fun InviteNameAndIndicatorRow( name: String?, + isInviteSeen: Boolean, modifier: Modifier = Modifier, ) { Row( @@ -315,13 +325,15 @@ private fun InviteNameAndIndicatorRow( style = ElementTheme.typography.fontBodyLgMedium, text = name ?: stringResource(id = CommonStrings.common_no_room_name), fontStyle = FontStyle.Italic.takeIf { name == null }, - color = MaterialTheme.roomListRoomName(), + color = ElementTheme.roomListRoomName(), maxLines = 1, overflow = TextOverflow.Ellipsis ) - UnreadIndicatorAtom( - color = ElementTheme.colors.unreadIndicator - ) + if (!isInviteSeen) { + UnreadIndicatorAtom( + color = ElementTheme.colors.unreadIndicator + ) + } } } @@ -387,6 +399,9 @@ private fun MentionIndicatorAtom() { internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryProvider::class) data: RoomListRoomSummary) = ElementPreview { RoomSummaryRow( room = data, + hideInviteAvatars = false, + // Set isInviteSeen to true for the preview when the room has name "Bob" + isInviteSeen = data.name == "Bob", onClick = {}, eventSink = {}, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/SetUpRecoveryKeyBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SetUpRecoveryKeyBanner.kt similarity index 92% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/SetUpRecoveryKeyBanner.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SetUpRecoveryKeyBanner.kt index e1d251de8b8..37aed6d1302 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/SetUpRecoveryKeyBanner.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/SetUpRecoveryKeyBanner.kt @@ -5,12 +5,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.components +package io.element.android.features.home.impl.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import io.element.android.features.roomlist.impl.R +import io.element.android.features.home.impl.R import io.element.android.libraries.designsystem.components.Announcement import io.element.android.libraries.designsystem.components.AnnouncementType import io.element.android.libraries.designsystem.preview.ElementPreview diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt similarity index 92% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt index e81c8828ae7..dcda64ca9cc 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt @@ -5,13 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.datasource +package io.element.android.features.home.impl.datasource -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.libraries.androidutils.diff.DiffCacheUpdater import io.element.android.libraries.androidutils.diff.MutableListDiffCache import io.element.android.libraries.androidutils.system.DateTimeObserver import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.roomlist.RoomListService @@ -36,7 +37,8 @@ class RoomListDataSource @Inject constructor( private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory, private val coroutineDispatchers: CoroutineDispatchers, private val notificationSettingsService: NotificationSettingsService, - private val appScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val dateTimeObserver: DateTimeObserver, ) { init { @@ -77,7 +79,7 @@ class RoomListDataSource @Inject constructor( .onEach { roomListService.allRooms.rebuildSummaries() } - .launchIn(appScope) + .launchIn(sessionCoroutineScope) } private fun observeDateTimeChanges() { @@ -88,7 +90,7 @@ class RoomListDataSource @Inject constructor( is DateTimeObserver.Event.DateChanged -> rebuildAllRoomSummaries() } } - .launchIn(appScope) + .launchIn(sessionCoroutineScope) } private suspend fun replaceWith(roomSummaries: List) = withContext(coroutineDispatchers.computation) { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt similarity index 92% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt index d1581f545b1..3d776621887 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt @@ -5,10 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.datasource +package io.element.android.features.home.impl.datasource -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.model.RoomSummaryDisplayType import io.element.android.libraries.core.extensions.orEmpty import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode @@ -67,6 +67,7 @@ class RoomListRoomSummaryFactory @Inject constructor( heroes = roomInfo.heroes.map { user -> user.getAvatarData(size = AvatarSize.RoomListItem) }.toImmutableList(), + isTombstoned = roomInfo.successorRoom != null, ) } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt new file mode 100644 index 00000000000..ef645124622 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.home.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.home.impl.filters.RoomListFiltersPresenter +import io.element.android.features.home.impl.filters.RoomListFiltersState +import io.element.android.features.home.impl.roomlist.RoomListPresenter +import io.element.android.features.home.impl.roomlist.RoomListState +import io.element.android.features.home.impl.search.RoomListSearchPresenter +import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesTo(SessionScope::class) +@Module +interface RoomListModule { + @Binds + fun bindRoomListPresenter(presenter: RoomListPresenter): Presenter + + @Binds + fun bindSearchPresenter(presenter: RoomListSearchPresenter): Presenter + + @Binds + fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt similarity index 90% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt index 324dd1eb3d5..2c7ebc35736 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt @@ -5,9 +5,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters -import io.element.android.features.roomlist.impl.R +import io.element.android.features.home.impl.R /** * Enum class representing the different filters that can be applied to the room list. diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResources.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt similarity index 96% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResources.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt index a0c48787156..61436a0054e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResources.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt @@ -5,10 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters import androidx.annotation.StringRes -import io.element.android.features.roomlist.impl.R +import io.element.android.features.home.impl.R /** * Holds the resources for the empty state when filters are applied to the room list. diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvents.kt similarity index 86% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvents.kt index 42559995f6b..13fcc656e81 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEvents.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters sealed interface RoomListFiltersEvents { data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvents diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt similarity index 94% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt index 0213d3fc15e..de3f7eaa0bc 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt @@ -5,12 +5,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState -import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionStrategy +import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.roomlist.RoomListService import kotlinx.collections.immutable.toPersistentList diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt similarity index 83% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt index 3e72aa680bf..bb4146ce84b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersState.kt @@ -5,9 +5,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters -import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState +import io.element.android.features.home.impl.filters.selection.FilterSelectionState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt similarity index 87% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt index e104651af93..8cba01163c1 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt @@ -5,10 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState +import io.element.android.features.home.impl.filters.selection.FilterSelectionState import kotlinx.collections.immutable.toImmutableList class RoomListFiltersStateProvider : PreviewParameterProvider { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt similarity index 96% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt index 81bae26b4ac..91b00b1180d 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt @@ -5,12 +5,11 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.animateScrollBy @@ -41,6 +40,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.home.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 @@ -48,7 +48,6 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag -@OptIn(ExperimentalFoundationApi::class) @Composable fun RoomListFiltersView( state: RoomListFiltersState, @@ -149,7 +148,7 @@ private fun RoomListClearFiltersButton( modifier = Modifier.align(Alignment.Center), imageVector = CompoundIcons.Close(), tint = ElementTheme.colors.iconOnSolidPrimary, - contentDescription = stringResource(id = io.element.android.libraries.ui.strings.R.string.action_clear), + contentDescription = stringResource(id = R.string.screen_roomlist_clear_filters), ) } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/DefaultFilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt similarity index 92% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/DefaultFilterSelectionStrategy.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt index c89615fea72..26a77da5c78 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/DefaultFilterSelectionStrategy.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt @@ -5,10 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters.selection +package io.element.android.features.home.impl.filters.selection import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.roomlist.impl.filters.RoomListFilter +import io.element.android.features.home.impl.filters.RoomListFilter import io.element.android.libraries.di.SessionScope import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionState.kt new file mode 100644 index 00000000000..d604e51f840 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionState.kt @@ -0,0 +1,15 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.home.impl.filters.selection + +import io.element.android.features.home.impl.filters.RoomListFilter + +data class FilterSelectionState( + val filter: RoomListFilter, + val isSelected: Boolean, +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt similarity index 82% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionStrategy.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt index c8d3c09fec1..8396a4e2a22 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionStrategy.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt @@ -5,9 +5,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters.selection +package io.element.android.features.home.impl.filters.selection -import io.element.android.features.roomlist.impl.filters.RoomListFilter +import io.element.android.features.home.impl.filters.RoomListFilter import kotlinx.coroutines.flow.StateFlow interface FilterSelectionStrategy { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt similarity index 83% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt index 498ad762a30..3f166a66b44 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt @@ -5,9 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.model +package io.element.android.features.home.impl.model import androidx.compose.runtime.Immutable +import io.element.android.features.invite.api.InviteData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId @@ -35,16 +36,21 @@ data class RoomListRoomSummary( val isDm: Boolean, val isFavorite: Boolean, val inviteSender: InviteSender?, + val isTombstoned: Boolean, val heroes: ImmutableList, ) { val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE && (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) || - isMarkedUnread || - displayType == RoomSummaryDisplayType.INVITE + isMarkedUnread val hasNewContent = numberOfUnreadMessages > 0 || numberOfUnreadMentions > 0 || numberOfUnreadNotifications > 0 || - isMarkedUnread || - displayType == RoomSummaryDisplayType.INVITE + isMarkedUnread + + fun toInviteData() = InviteData( + roomId = roomId, + roomName = name ?: roomId.value, + isDm = isDm, + ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt similarity index 95% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt index d293951f89f..f06e5a1a278 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.model +package io.element.android.features.home.impl.model import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -110,6 +110,11 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider = emptyList(), + isTombstoned: Boolean = false, ) = RoomListRoomSummary( id = id, roomId = RoomId(id), @@ -164,4 +171,5 @@ internal fun aRoomListRoomSummary( displayType = displayType, canonicalAlias = canonicalAlias, heroes = heroes.toImmutableList(), + isTombstoned = isTombstoned, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomSummaryDisplayType.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomSummaryDisplayType.kt similarity index 86% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomSummaryDisplayType.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomSummaryDisplayType.kt index 4b472d88479..8fcef46faf3 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomSummaryDisplayType.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomSummaryDisplayType.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.model +package io.element.android.features.home.impl.model /** * Represents the type of display for a room list item. diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt new file mode 100644 index 00000000000..a421c239cb4 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package io.element.android.features.home.impl.roomlist + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState +import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.push.api.battery.BatteryOptimizationState +import io.element.android.libraries.push.api.battery.aBatteryOptimizationState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentSet + +open class RoomListContentStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomsContentState(), + aRoomsContentState(summaries = persistentListOf()), + aSkeletonContentState(), + anEmptyContentState(), + anEmptyContentState(securityBannerState = SecurityBannerState.SetUpRecovery), + ) +} + +internal fun aRoomsContentState( + securityBannerState: SecurityBannerState = SecurityBannerState.None, + summaries: ImmutableList = aRoomListRoomSummaryList(), + fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), + batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(), + seenRoomInvites: Set = emptySet(), +) = RoomListContentState.Rooms( + securityBannerState = securityBannerState, + fullScreenIntentPermissionsState = fullScreenIntentPermissionsState, + batteryOptimizationState = batteryOptimizationState, + summaries = summaries, + seenRoomInvites = seenRoomInvites.toPersistentSet(), +) + +internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16) + +internal fun anEmptyContentState( + securityBannerState: SecurityBannerState = SecurityBannerState.None, +) = RoomListContentState.Empty( + securityBannerState = securityBannerState, +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt similarity index 86% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt index 661beb64187..27883e88d59 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl +package io.element.android.features.home.impl.roomlist import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -19,6 +19,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.home.impl.R import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -34,14 +35,17 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun RoomListContextMenu( contextMenu: RoomListState.ContextMenu.Shown, + canReportRoom: Boolean, eventSink: (RoomListEvents.ContextMenuEvents) -> Unit, onRoomSettingsClick: (roomId: RoomId) -> Unit, + onReportRoomClick: (roomId: RoomId) -> Unit ) { ModalBottomSheet( onDismissRequest = { eventSink(RoomListEvents.HideContextMenu) }, ) { RoomListModalBottomSheetContent( contextMenu = contextMenu, + canReportRoom = canReportRoom, onRoomMarkReadClick = { eventSink(RoomListEvents.HideContextMenu) eventSink(RoomListEvents.MarkAsRead(contextMenu.roomId)) @@ -65,6 +69,10 @@ fun RoomListContextMenu( eventSink(RoomListEvents.HideContextMenu) eventSink(RoomListEvents.ClearCacheOfRoom(contextMenu.roomId)) }, + onReportRoomClick = { + eventSink(RoomListEvents.HideContextMenu) + onReportRoomClick(contextMenu.roomId) + }, ) } } @@ -72,12 +80,14 @@ fun RoomListContextMenu( @Composable private fun RoomListModalBottomSheetContent( contextMenu: RoomListState.ContextMenu.Shown, + canReportRoom: Boolean, onRoomSettingsClick: () -> Unit, onLeaveRoomClick: () -> Unit, onFavoriteChange: (isFavorite: Boolean) -> Unit, onRoomMarkReadClick: () -> Unit, onRoomMarkUnreadClick: () -> Unit, onClearCacheRoomClick: () -> Unit, + onReportRoomClick: () -> Unit, ) { Column( modifier = Modifier.fillMaxWidth() @@ -132,14 +142,10 @@ private fun RoomListModalBottomSheetContent( leadingContent = ListItemContent.Icon( iconSource = IconSource.Vector( CompoundIcons.Favourite(), - contentDescription = stringResource(id = CommonStrings.common_favourite), ) ), trailingContent = ListItemContent.Switch( checked = contextMenu.isFavorite, - onChange = { isFavorite -> - onFavoriteChange(isFavorite) - }, ), onClick = { onFavoriteChange(!contextMenu.isFavorite) @@ -157,32 +163,38 @@ private fun RoomListModalBottomSheetContent( leadingContent = ListItemContent.Icon( iconSource = IconSource.Vector( CompoundIcons.Settings(), - contentDescription = stringResource(id = CommonStrings.common_settings) ) ), style = ListItemStyle.Primary, ) + if (canReportRoom) { + ListItem( + headlineContent = { + Text(text = stringResource(CommonStrings.action_report_room)) + }, + modifier = Modifier.clickable { onReportRoomClick() }, + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector( + CompoundIcons.ChatProblem(), + contentDescription = stringResource(CommonStrings.action_report_room), + ) + ), + style = ListItemStyle.Destructive, + ) + } ListItem( headlineContent = { - val leaveText = stringResource( - id = if (contextMenu.isDm) { - CommonStrings.action_leave_conversation - } else { - CommonStrings.action_leave_room - } - ) - Text(text = leaveText) + Text(text = stringResource(CommonStrings.action_leave_room)) }, modifier = Modifier.clickable { onLeaveRoomClick() }, leadingContent = ListItemContent.Icon( iconSource = IconSource.Vector( CompoundIcons.Leave(), - contentDescription = stringResource(id = CommonStrings.action_leave_room) ) ), style = ListItemStyle.Destructive, ) - if (contextMenu.eventCacheFeatureFlagEnabled) { + if (contextMenu.displayClearRoomCacheAction) { ListItem( headlineContent = { Text(text = "Clear cache for this room") @@ -207,11 +219,13 @@ internal fun RoomListModalBottomSheetContentPreview( ) = ElementPreview { RoomListModalBottomSheetContent( contextMenu = contextMenu, + canReportRoom = true, onRoomMarkReadClick = {}, onRoomMarkUnreadClick = {}, onRoomSettingsClick = {}, onLeaveRoomClick = {}, onFavoriteChange = {}, onClearCacheRoomClick = {}, + onReportRoomClick = {}, ) } 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 new file mode 100644 index 00000000000..c6b91a02fce --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt @@ -0,0 +1,126 @@ +/* + * 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. + */ + +package io.element.android.features.home.impl.roomlist + +import androidx.compose.foundation.layout.Column +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.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.unit.dp +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.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.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomListDeclineInviteMenu( + menu: RoomListState.DeclineInviteMenu.Shown, + canReportRoom: Boolean, + onDeclineAndBlockClick: (RoomListRoomSummary) -> Unit, + eventSink: (RoomListEvents) -> Unit, +) { + ModalBottomSheet( + onDismissRequest = { eventSink(RoomListEvents.HideDeclineInviteMenu) }, + ) { + RoomListDeclineInviteMenuContent( + roomName = menu.roomSummary.name ?: menu.roomSummary.roomId.value, + onDeclineClick = { + eventSink(RoomListEvents.HideDeclineInviteMenu) + eventSink(RoomListEvents.DeclineInvite(menu.roomSummary, false)) + }, + onDeclineAndBlockClick = { + eventSink(RoomListEvents.HideDeclineInviteMenu) + if (canReportRoom) { + onDeclineAndBlockClick(menu.roomSummary) + } else { + eventSink(RoomListEvents.DeclineInvite(menu.roomSummary, true)) + } + }, + onCancelClick = { + eventSink(RoomListEvents.HideDeclineInviteMenu) + } + ) + } +} + +@Composable +private fun RoomListDeclineInviteMenuContent( + roomName: String, + onDeclineClick: () -> Unit, + onDeclineAndBlockClick: () -> Unit, + onCancelClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.screen_invites_decline_chat_title), + style = ElementTheme.typography.fontHeadingSmMedium, + color = ElementTheme.colors.textPrimary, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.screen_invites_decline_chat_message, roomName), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(22.dp)) + Button( + text = stringResource(CommonStrings.action_decline), + modifier = Modifier.fillMaxWidth(), + onClick = onDeclineClick, + ) + Spacer(Modifier.height(16.dp)) + OutlinedButton( + text = stringResource(CommonStrings.action_decline_and_block), + modifier = Modifier.fillMaxWidth(), + destructive = true, + onClick = onDeclineAndBlockClick + ) + Spacer(Modifier.height(16.dp)) + TextButton( + text = stringResource(CommonStrings.action_cancel), + modifier = Modifier.fillMaxWidth(), + onClick = onCancelClick + ) + } +} + +// 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 RoomListDeclineInviteMenuContentPreview() = ElementPreview { + RoomListDeclineInviteMenuContent( + roomName = "Room name", + onCancelClick = {}, + onDeclineClick = {}, + onDeclineAndBlockClick = {}, + ) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt new file mode 100644 index 00000000000..4ba37d810b8 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvents.kt @@ -0,0 +1,32 @@ +/* + * 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. + */ + +package io.element.android.features.home.impl.roomlist + +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface RoomListEvents { + data class UpdateVisibleRange(val range: IntRange) : RoomListEvents + data object DismissRequestVerificationPrompt : RoomListEvents + data object DismissBanner : RoomListEvents + data object ToggleSearchResults : RoomListEvents + data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents + + data class AcceptInvite(val roomSummary: RoomListRoomSummary) : RoomListEvents + data class DeclineInvite(val roomSummary: RoomListRoomSummary, val blockUser: Boolean) : RoomListEvents + data class ShowDeclineInviteMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents + data object HideDeclineInviteMenu : RoomListEvents + + sealed interface ContextMenuEvents : RoomListEvents + data object HideContextMenu : ContextMenuEvents + data class LeaveRoom(val roomId: RoomId) : ContextMenuEvents + data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents + data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents + data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents + data class ClearCacheOfRoom(val roomId: RoomId) : ContextMenuEvents +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt new file mode 100644 index 00000000000..64a363d73ff --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -0,0 +1,328 @@ +/* + * 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. + */ + +package io.element.android.features.home.impl.roomlist + +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.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import im.vector.app.features.analytics.plan.Interaction +import io.element.android.features.home.impl.datasource.RoomListDataSource +import io.element.android.features.home.impl.filters.RoomListFiltersState +import io.element.android.features.home.impl.search.RoomListSearchEvents +import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.leaveroom.api.LeaveRoomEvent.ShowConfirmation +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.mapState +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +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.roomlist.RoomList +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import io.element.android.libraries.push.api.battery.BatteryOptimizationState +import io.element.android.libraries.push.api.notifications.NotificationCleaner +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analyticsproviders.api.trackers.captureInteraction +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.launch +import javax.inject.Inject + +private const val EXTENDED_RANGE_SIZE = 40 +private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L + +class RoomListPresenter @Inject constructor( + private val client: MatrixClient, + private val leaveRoomPresenter: Presenter, + private val roomListDataSource: RoomListDataSource, + private val featureFlagService: FeatureFlagService, + private val filtersPresenter: Presenter, + private val searchPresenter: Presenter, + private val sessionPreferencesStore: SessionPreferencesStore, + private val analyticsService: AnalyticsService, + private val acceptDeclineInvitePresenter: Presenter, + private val fullScreenIntentPermissionsPresenter: Presenter, + private val batteryOptimizationPresenter: Presenter, + private val notificationCleaner: NotificationCleaner, + private val appPreferencesStore: AppPreferencesStore, + private val seenInvitesStore: SeenInvitesStore, +) : Presenter { + private val encryptionService: EncryptionService = client.encryptionService() + + @Composable + override fun present(): RoomListState { + val coroutineScope = rememberCoroutineScope() + val leaveRoomState = leaveRoomPresenter.present() + val filtersState = filtersPresenter.present() + val searchState = searchPresenter.present() + val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() + + LaunchedEffect(Unit) { + roomListDataSource.launchIn(this) + } + + var securityBannerDismissed by rememberSaveable { mutableStateOf(true) } + + // Avatar indicator + val hideInvitesAvatar by remember { + client + .mediaPreviewService() + .mediaPreviewConfigFlow + .mapState { config -> config.hideInviteAvatar } + }.collectAsState() + + val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } + val declineInviteMenu = remember { mutableStateOf(RoomListState.DeclineInviteMenu.Hidden) } + + fun handleEvents(event: RoomListEvents) { + when (event) { + is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch { + updateVisibleRange(event.range) + } + RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true + RoomListEvents.DismissBanner -> securityBannerDismissed = true + RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility) + is RoomListEvents.ShowContextMenu -> { + coroutineScope.showContextMenu(event, contextMenu) + } + is RoomListEvents.HideContextMenu -> { + contextMenu.value = RoomListState.ContextMenu.Hidden + } + is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(ShowConfirmation(event.roomId)) + is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite) + is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId) + is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId) + is RoomListEvents.AcceptInvite -> { + acceptDeclineInviteState.eventSink( + AcceptInvite(event.roomSummary.toInviteData()) + ) + } + is RoomListEvents.DeclineInvite -> { + acceptDeclineInviteState.eventSink( + DeclineInvite(event.roomSummary.toInviteData(), blockUser = event.blockUser, shouldConfirm = false) + ) + } + is RoomListEvents.ShowDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Shown(event.roomSummary) + RoomListEvents.HideDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Hidden + is RoomListEvents.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId) + } + } + + val contentState = roomListContentState(securityBannerDismissed) + + val canReportRoom by produceState(false) { value = client.canReportRoom() } + + return RoomListState( + contextMenu = contextMenu.value, + declineInviteMenu = declineInviteMenu.value, + leaveRoomState = leaveRoomState, + filtersState = filtersState, + searchState = searchState, + contentState = contentState, + acceptDeclineInviteState = acceptDeclineInviteState, + hideInvitesAvatars = hideInvitesAvatar, + canReportRoom = canReportRoom, + eventSink = ::handleEvents, + ) + } + + @Composable + private fun rememberSecurityBannerState( + securityBannerDismissed: Boolean, + ): State { + val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed) + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + return remember { + derivedStateOf { + calculateBannerState( + securityBannerDismissed = currentSecurityBannerDismissed, + recoveryState = recoveryState, + ) + } + } + } + + private fun calculateBannerState( + securityBannerDismissed: Boolean, + recoveryState: RecoveryState, + ): SecurityBannerState { + if (securityBannerDismissed) { + return SecurityBannerState.None + } + + when (recoveryState) { + RecoveryState.DISABLED -> return SecurityBannerState.SetUpRecovery + RecoveryState.INCOMPLETE -> return SecurityBannerState.RecoveryKeyConfirmation + RecoveryState.UNKNOWN, + RecoveryState.WAITING_FOR_SYNC, + RecoveryState.ENABLED -> Unit + } + + return SecurityBannerState.None + } + + @Composable + private fun roomListContentState( + securityBannerDismissed: Boolean, + ): RoomListContentState { + val roomSummaries by produceState(initialValue = AsyncData.Loading()) { + roomListDataSource.allRooms.collect { value = AsyncData.Success(it) } + } + val loadingState by roomListDataSource.loadingState.collectAsState() + val showEmpty by remember { + derivedStateOf { + (loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0 + } + } + val showSkeleton by remember { + derivedStateOf { + loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading + } + } + val seenRoomInvites by remember { seenInvitesStore.seenRoomIds() }.collectAsState(emptySet()) + val securityBannerState by rememberSecurityBannerState(securityBannerDismissed) + return when { + showEmpty -> RoomListContentState.Empty(securityBannerState = securityBannerState) + showSkeleton -> RoomListContentState.Skeleton(count = 16) + else -> { + RoomListContentState.Rooms( + securityBannerState = securityBannerState, + fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(), + batteryOptimizationState = batteryOptimizationPresenter.present(), + summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(), + seenRoomInvites = seenRoomInvites.toPersistentSet(), + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState) = launch { + val initialState = RoomListState.ContextMenu.Shown( + roomId = event.roomSummary.roomId, + roomName = event.roomSummary.name, + isDm = event.roomSummary.isDm, + isFavorite = event.roomSummary.isFavorite, + markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread), + hasNewContent = event.roomSummary.hasNewContent, + displayClearRoomCacheAction = appPreferencesStore.isDeveloperModeEnabledFlow().first(), + ) + contextMenuState.value = initialState + + client.getRoom(event.roomSummary.roomId)?.use { room -> + + val isShowingContextMenuFlow = snapshotFlow { contextMenuState.value is RoomListState.ContextMenu.Shown } + .distinctUntilChanged() + + val isFavoriteFlow = room.roomInfoFlow + .map { it.isFavorite } + .distinctUntilChanged() + + isFavoriteFlow + .onEach { isFavorite -> + contextMenuState.value = initialState.copy(isFavorite = isFavorite) + } + .flatMapLatest { isShowingContextMenuFlow } + .takeWhile { isShowingContextMenu -> isShowingContextMenu } + .collect() + } + } + + private fun CoroutineScope.setRoomIsFavorite(roomId: RoomId, isFavorite: Boolean) = launch { + client.getRoom(roomId)?.use { room -> + room.setIsFavorite(isFavorite) + .onSuccess { + analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuFavouriteToggle) + } + } + } + + private fun CoroutineScope.markAsRead(roomId: RoomId) = launch { + notificationCleaner.clearMessagesForRoom(client.sessionId, roomId) + client.getRoom(roomId)?.use { room -> + 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(roomId: RoomId) = launch { + client.getRoom(roomId)?.use { room -> + room.setUnreadFlag(isUnread = true) + .onSuccess { + analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle) + } + } + } + + private fun CoroutineScope.clearCacheOfRoom(roomId: RoomId) = launch { + client.getRoom(roomId)?.use { room -> + room.clearEventCacheStorage() + } + } + + private var currentUpdateVisibleRangeJob: Job? = null + private fun CoroutineScope.updateVisibleRange(range: IntRange) { + currentUpdateVisibleRangeJob?.cancel() + currentUpdateVisibleRangeJob = launch { + // Debounce the subscription to avoid subscribing to too many rooms + delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS) + + if (range.isEmpty()) return@launch + val currentRoomList = roomListDataSource.allRooms.first() + // Use extended range to 'prefetch' the next rooms info + val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2 + val extendedRange = range.first until range.last + midExtendedRangeSize + val roomIds = extendedRange.mapNotNull { index -> + currentRoomList.getOrNull(index)?.roomId + } + roomListDataSource.subscribeToVisibleRooms(roomIds) + } + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt new file mode 100644 index 00000000000..4cfa82fbfcd --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -0,0 +1,76 @@ +/* + * 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. + */ + +package io.element.android.features.home.impl.roomlist + +import androidx.compose.runtime.Immutable +import io.element.android.features.home.impl.filters.RoomListFiltersState +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.push.api.battery.BatteryOptimizationState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet + +@Immutable +data class RoomListState( + val contextMenu: ContextMenu, + val declineInviteMenu: DeclineInviteMenu, + val leaveRoomState: LeaveRoomState, + val filtersState: RoomListFiltersState, + val searchState: RoomListSearchState, + val contentState: RoomListContentState, + val acceptDeclineInviteState: AcceptDeclineInviteState, + val hideInvitesAvatars: Boolean, + val canReportRoom: Boolean, + val eventSink: (RoomListEvents) -> Unit, +) { + val displayFilters = contentState is RoomListContentState.Rooms + + sealed interface ContextMenu { + data object Hidden : ContextMenu + data class Shown( + val roomId: RoomId, + val roomName: String?, + val isDm: Boolean, + val isFavorite: Boolean, + val markAsUnreadFeatureFlagEnabled: Boolean, + val hasNewContent: Boolean, + val displayClearRoomCacheAction: Boolean, + ) : ContextMenu + } + + sealed interface DeclineInviteMenu { + data object Hidden : DeclineInviteMenu + data class Shown(val roomSummary: RoomListRoomSummary) : DeclineInviteMenu + } +} + +enum class SecurityBannerState { + None, + SetUpRecovery, + RecoveryKeyConfirmation, +} + +@Immutable +sealed interface RoomListContentState { + data class Skeleton(val count: Int) : RoomListContentState + data class Empty( + val securityBannerState: SecurityBannerState, + ) : RoomListContentState + + data class Rooms( + val securityBannerState: SecurityBannerState, + val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState, + val batteryOptimizationState: BatteryOptimizationState, + val summaries: ImmutableList, + val seenRoomInvites: ImmutableSet, + ) : RoomListContentState +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateContextMenuShownProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt similarity index 91% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateContextMenuShownProvider.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt index 47927956778..0cd3117de4c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateContextMenuShownProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl +package io.element.android.features.home.impl.roomlist import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.core.RoomId @@ -31,5 +31,5 @@ internal fun aContextMenuShown( markAsUnreadFeatureFlagEnabled = true, hasNewContent = hasNewContent, isFavorite = isFavorite, - eventCacheFeatureFlagEnabled = false, + displayClearRoomCacheAction = false, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt new file mode 100644 index 00000000000..0e4600c16ad --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt @@ -0,0 +1,131 @@ +/* + * 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. + */ + +package io.element.android.features.home.impl.roomlist + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.home.impl.filters.RoomListFiltersState +import io.element.android.features.home.impl.filters.aRoomListFiltersState +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.model.RoomSummaryDisplayType +import io.element.android.features.home.impl.model.aRoomListRoomSummary +import io.element.android.features.home.impl.model.anInviteSender +import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.home.impl.search.aRoomListSearchState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.leaveroom.api.aLeaveRoomState +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.matrix.api.core.RoomId +import io.element.android.libraries.push.api.battery.aBatteryOptimizationState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +open class RoomListStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomListState(), + aRoomListState(contextMenu = aContextMenuShown(roomName = null)), + aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")), + aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)), + aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)), + aRoomListState(contentState = anEmptyContentState()), + aRoomListState(contentState = aSkeletonContentState()), + aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), + aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)), + aRoomListState(contentState = aRoomsContentState(batteryOptimizationState = aBatteryOptimizationState(shouldDisplayBanner = true))), + ) +} + +internal fun aRoomListState( + contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden, + declineInviteMenu: RoomListState.DeclineInviteMenu = RoomListState.DeclineInviteMenu.Hidden, + leaveRoomState: LeaveRoomState = aLeaveRoomState(), + searchState: RoomListSearchState = aRoomListSearchState(), + filtersState: RoomListFiltersState = aRoomListFiltersState(), + contentState: RoomListContentState = aRoomsContentState(), + acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), + hideInvitesAvatars: Boolean = false, + canReportRoom: Boolean = true, + eventSink: (RoomListEvents) -> Unit = {} +) = RoomListState( + contextMenu = contextMenu, + declineInviteMenu = declineInviteMenu, + leaveRoomState = leaveRoomState, + filtersState = filtersState, + searchState = searchState, + contentState = contentState, + acceptDeclineInviteState = acceptDeclineInviteState, + hideInvitesAvatars = hideInvitesAvatars, + canReportRoom = canReportRoom, + eventSink = eventSink, +) + +internal fun anAcceptDeclineInviteState( + acceptAction: AsyncAction = AsyncAction.Uninitialized, + declineAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (AcceptDeclineInviteEvents) -> Unit = {} +) = AcceptDeclineInviteState( + acceptAction = acceptAction, + declineAction = declineAction, + eventSink = eventSink, +) + +internal fun aRoomListRoomSummaryList(): ImmutableList { + return persistentListOf( + aRoomListRoomSummary( + name = "Room Invited", + avatarData = AvatarData("!roomId", "Room with Alice and Bob", size = AvatarSize.RoomListItem), + id = "!roomId:domain", + inviteSender = anInviteSender(), + displayType = RoomSummaryDisplayType.INVITE, + ), + aRoomListRoomSummary( + name = "Room", + numberOfUnreadMessages = 1, + timestamp = "14:18", + lastMessage = "A very very very very long message which suites on two lines", + avatarData = AvatarData("!id", "R", size = AvatarSize.RoomListItem), + id = "!roomId:domain", + ), + aRoomListRoomSummary( + name = "Room#2", + numberOfUnreadMessages = 0, + timestamp = "14:16", + lastMessage = "A short message", + avatarData = AvatarData("!id", "Z", size = AvatarSize.RoomListItem), + id = "!roomId2:domain", + ), + aRoomListRoomSummary( + id = "!roomId3:domain", + displayType = RoomSummaryDisplayType.PLACEHOLDER, + ), + aRoomListRoomSummary( + id = "!roomId4:domain", + displayType = RoomSummaryDisplayType.PLACEHOLDER, + ), + ) +} + +internal fun generateRoomListRoomSummaryList( + numberOfRooms: Int = 10, +): ImmutableList { + return List(numberOfRooms) { index -> + aRoomListRoomSummary( + name = "Room#$index", + numberOfUnreadMessages = 0, + timestamp = "14:16", + lastMessage = "A message", + avatarData = AvatarData("!id$index", "${(65 + index % 26).toChar()}", size = AvatarSize.RoomListItem), + id = "!roomId$index:domain", + ) + }.toPersistentList() +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt similarity index 90% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt index 34152a7e65d..6840809480c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt @@ -5,10 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.search +package io.element.android.features.home.impl.search -import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt similarity index 87% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt index b8248d09f5c..26f713da819 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.search +package io.element.android.features.home.impl.search sealed interface RoomListSearchEvents { data object ToggleSearchVisibility : RoomListSearchEvents diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt similarity index 81% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt index 62b5fa524f5..b4d151fe028 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.search +package io.element.android.features.home.impl.search import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -15,14 +15,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import kotlinx.collections.immutable.persistentListOf import javax.inject.Inject class RoomListSearchPresenter @Inject constructor( private val dataSource: RoomListSearchDataSource, - private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): RoomListSearchState { @@ -57,14 +54,12 @@ class RoomListSearchPresenter @Inject constructor( } } - val isRoomDirectorySearchEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch).collectAsState(initial = false) val searchResults by dataSource.roomSummaries.collectAsState(initial = persistentListOf()) return RoomListSearchState( isSearchActive = isSearchActive, query = searchQuery, results = searchResults, - isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, eventSink = ::handleEvents ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt new file mode 100644 index 00000000000..9d50f52a339 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt @@ -0,0 +1,18 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.home.impl.search + +import io.element.android.features.home.impl.model.RoomListRoomSummary +import kotlinx.collections.immutable.ImmutableList + +data class RoomListSearchState( + val isSearchActive: Boolean, + val query: String, + val results: ImmutableList, + val eventSink: (RoomListSearchEvents) -> Unit +) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt new file mode 100644 index 00000000000..503e334418e --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt @@ -0,0 +1,38 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.home.impl.search + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +class RoomListSearchStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomListSearchState(), + aRoomListSearchState( + isSearchActive = true, + query = "Test", + results = aRoomListRoomSummaryList() + ), + ) +} + +fun aRoomListSearchState( + isSearchActive: Boolean = false, + query: String = "", + results: ImmutableList = persistentListOf(), + eventSink: (RoomListSearchEvents) -> Unit = { }, +) = RoomListSearchState( + isSearchActive = isSearchActive, + query = query, + results = results, + eventSink = eventSink, +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt similarity index 78% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt index d81f5442762..29c8078c09b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt @@ -5,19 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.search +package io.element.android.features.home.impl.search import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api @@ -26,7 +23,6 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester @@ -38,22 +34,18 @@ import androidx.compose.ui.res.stringResource 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.roomlist.impl.R -import io.element.android.features.roomlist.impl.RoomListEvents -import io.element.android.features.roomlist.impl.components.RoomSummaryRow -import io.element.android.features.roomlist.impl.contentType -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.components.RoomSummaryRow +import io.element.android.features.home.impl.contentType +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.roomlist.RoomListEvents import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.button.SuperButton import io.element.android.libraries.designsystem.modifiers.applyIf import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.FilledTextField 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.TopAppBar import io.element.android.libraries.designsystem.utils.copy import io.element.android.libraries.matrix.api.core.RoomId @@ -62,9 +54,9 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun RoomListSearchView( state: RoomListSearchState, + hideInvitesAvatars: Boolean, eventSink: (RoomListEvents) -> Unit, onRoomClick: (RoomId) -> Unit, - onRoomDirectorySearchClick: () -> Unit, modifier: Modifier = Modifier, ) { BackHandler(enabled = state.isSearchActive) { @@ -89,9 +81,9 @@ internal fun RoomListSearchView( if (state.isSearchActive) { RoomListSearchContent( state = state, + hideInvitesAvatars = hideInvitesAvatars, onRoomClick = onRoomClick, eventSink = eventSink, - onRoomDirectorySearchClick = onRoomDirectorySearchClick, ) } } @@ -102,9 +94,9 @@ internal fun RoomListSearchView( @Composable private fun RoomListSearchContent( state: RoomListSearchState, + hideInvitesAvatars: Boolean, eventSink: (RoomListEvents) -> Unit, onRoomClick: (RoomId) -> Unit, - onRoomDirectorySearchClick: () -> Unit, ) { val borderColor = MaterialTheme.colorScheme.tertiary val strokeWidth = 1.dp @@ -175,14 +167,6 @@ private fun RoomListSearchContent( .padding(padding) .consumeWindowInsets(padding) ) { - if (state.displayRoomDirectorySearch) { - RoomDirectorySearchButton( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 24.dp, horizontal = 16.dp), - onClick = onRoomDirectorySearchClick - ) - } LazyColumn( modifier = Modifier.weight(1f), ) { @@ -192,6 +176,9 @@ private fun RoomListSearchContent( ) { room -> RoomSummaryRow( room = room, + hideInviteAvatars = hideInvitesAvatars, + // TODO + isInviteSeen = false, onClick = ::onRoomClick, eventSink = eventSink, ) @@ -201,38 +188,13 @@ private fun RoomListSearchContent( } } -@Composable -private fun RoomDirectorySearchButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - SuperButton( - onClick = onClick, - modifier = modifier, - buttonSize = ButtonSize.Large, - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = CompoundIcons.ListBulleted(), - contentDescription = null, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.screen_roomlist_room_directory_button_title), - ) - } - } -} - @PreviewsDayNight @Composable internal fun RoomListSearchContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview { RoomListSearchContent( state = state, + hideInvitesAvatars = false, onRoomClick = {}, eventSink = {}, - onRoomDirectorySearchClick = {}, ) } diff --git a/features/home/impl/src/main/res/values-be/translations.xml b/features/home/impl/src/main/res/values-be/translations.xml new file mode 100644 index 00000000000..b34671ed861 --- /dev/null +++ b/features/home/impl/src/main/res/values-be/translations.xml @@ -0,0 +1,42 @@ + + + "Стварыце новы ключ аднаўлення, які можна выкарыстоўваць для аднаўлення зашыфраванай гісторыі паведамленняў у выпадку страты доступу да вашых прылад." + "Наладзьце аднаўленне" + "Наладзіць аднаўленне" + "Пацвердзіце свой ключ аднаўлення, каб захаваць доступ да сховішча ключоў і гісторыі паведамленняў." + "Ваша сховішча ключоў не сінхранізавана" + "Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны." + "Палепшыце якасць званкоў" + "Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?" + "Адхіліць запрашэнне" + "Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?" + "Адхіліць чат" + "Няма запрашэнняў" + "%1$s (%2$s) запрасіў(-ла) вас" + "Гэта аднаразовы працэс, дзякуем за чаканне." + "Налада ўліковага запісу." + "Стварыце новую размову або пакой" + "Пачніце з паведамлення каму-небудзь." + "Пакуль няма чатаў." + "Абранае" + "Дадаць чат у абранае можна ў наладах чата. +На дадзены момант вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты." + "У вас пакуль няма абраных чатаў" + "Запрашэнні" + "У вас няма непрынятых запрашэнняў." + "Нізкі прыярытэт" + "Вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты." + "У вас няма чатаў для гэтай катэгорыі" + "Людзі" + "У вас пакуль няма асабістых паведамленняў" + "Пакоі" + "Вас пакуль няма ў ніводным пакоі" + "Непрачытаныя" + "Віншуем! +У вас няма непрачытаных паведамленняў!" + "Усе чаты" + "Пазначыць як прачытанае" + "Пазначыць як непрачытанае" + "Здаецца, вы карыстаецеся новай прыладай. Праверце з дапамогай іншай прылады, каб атрымаць доступ да зашыфраваных паведамленняў." + "Пацвердзіце, што гэта вы" + diff --git a/features/home/impl/src/main/res/values-bg/translations.xml b/features/home/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 00000000000..8a555ebbb8d --- /dev/null +++ b/features/home/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,33 @@ + + + "Потвърдете ключа си за възстановяване, за да запазите достъп до хранилището за ключове и историята на съобщенията си." + "Въведете ключа си за възстановяване" + "Хранилището ви за ключове не е синхронизирано" + "Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?" + "Отказване на покана" + "Няма покани" + "%1$s (%2$s) ви покани" + "Създаване на нов разговор или стая" + "Започнете, като изпратите съобщение на някого." + "Все още няма чатове." + "Любими" + "Можете да добавите чат към фаворизираните си в настройките на чата. +Засега можете да премахнете избора на филтрите, за да видите другите си чатове." + "Все още нямате фаворизирани чатове" + "Покани" + "Нямате чакащи покани." + "Нисък приоритет" + "Можете да премахнете избора на филтрите, за да видите другите си чатове" + "Хора" + "Все още нямате директни съобщения" + "Стаи" + "Все още не сте в никоя стая" + "Непрочетени" + "Поздравления! +Нямате непрочетени съобщения!" + "Всички чатове" + "Отбелязване като прочетено" + "Отбелязване като непрочетено" + "Изглежда, че използвате ново устройство. Потвърдете с друго устройство за достъп до вашите шифровани съобщения." + "Потвърдете, че сте вие" + diff --git a/features/home/impl/src/main/res/values-cs/translations.xml b/features/home/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 00000000000..9fcea2887a1 --- /dev/null +++ b/features/home/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,49 @@ + + + "Zakažte optimalizaci baterie pro tuto aplikaci, abyste měli jistotu, že budou přijata všechna oznámení." + "Zakázat optimalizaci" + "Nepřicházejí vám oznámení?" + "Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením." + "Nastavení obnovy" + "Nastavení obnovy" + "Potvrďte klíč pro obnovení, abyste zachovali přístup k úložišti klíčů a historii zpráv." + "Zadejte klíč pro obnovení" + "Zapomněli jste klíč pro obnovení?" + "Vaše úložiště klíčů není synchronizováno" + "Abyste nikdy nezmeškali důležitý hovor, změňte nastavení tak, abyste povolili oznámení na celé obrazovce, když je telefon uzamčen." + "Vylepšete si zážitek z volání" + "Opravdu chcete odmítnout pozvánku do %1$s?" + "Odmítnout pozvání" + "Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?" + "Odmítnout chat" + "Žádné pozvánky" + "%1$s (%2$s) vás pozval(a)" + "Jedná se o jednorázový proces, prosíme o strpení." + "Nastavení vašeho účtu" + "Vytvořte novou konverzaci nebo místnost" + "Začněte tím, že někomu pošnete zprávu." + "Zatím žádné konverzace." + "Oblíbené" + "V nastavení chatu můžete přidat chat k oblíbeným. +Prozatím můžete zrušit výběr filtrů, abyste viděli své další chaty" + "Zatím nemáte oblíbené chaty" + "Pozvánky" + "Nemáte žádné nevyřízené pozvánky." + "Nízká priorita" + "Můžete zrušit výběr filtrů, abyste viděli své další chaty" + "Nemáte chaty pro tento výběr" + "Lidé" + "Zatím nemáte žádné přímé zprávy" + "Místnosti" + "Ještě nejste v žádné místnosti" + "Nepřečtené" + "Gratulujeme! +Nemáte žádné nepřečtené zprávy!" + "Žádost o vstup odeslána" + "Všechny chaty" + "Označit jako přečtené" + "Označit jako nepřečtené" + "Tato místnost byla aktualizována" + "Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám." + "Ověřte, že jste to vy" + diff --git a/features/home/impl/src/main/res/values-cy/translations.xml b/features/home/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..9b44b0fc3fa --- /dev/null +++ b/features/home/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,50 @@ + + + "Analluogwch optimeiddio batri ar gyfer yr ap hwn, er mwyn sicrhau bod pob hysbysiad yn cael ei dderbyn." + "Analluogi optimeiddio" + "Hysbysiadau ddim yn cyrraedd?" + "Adferwch eich hunaniaeth cryptograffig a hanes negeseuon gydag allwedd adfer os ydych wedi colli eich holl ddyfeisiau presennol." + "Gosod adfer" + "Gosodwch adferiad i ddiogelu eich cyfrif" + "Cadarnhewch eich allwedd adfer i gynnal mynediad i\'ch storfa allweddi a\'ch hanes negeseuon." + "Rhowch eich allwedd adfer" + "Wedi anghofio\'ch allwedd adfer?" + "Dyw eich allwedd storfa heb ei gydweddu" + "Er mwyn sicrhau fyddwch chi ddim yn colli galwad bwysig, newidiwch eich gosodiadau i ganiatáu hysbysiadau sgrin lawn pan fydd eich ffôn wedi\'i gloi." + "Gwella profiad eich galwadau" + "Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?" + "Gwrthod y gwahoddiad" + "Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?" + "Gwrthod sgwrs" + "Dim Gwahoddiadau" + "Mae %1$s (%2$s) wedi eich gwahodd" + "Mae hon yn broses un tro, diolch am aros." + "Creu eich cyfrif." + "Crëwch sgwrs neu ystafell newydd" + "Clirio\'r hidlau" + "Cychwynnwch arni trwy anfon neges at rywun." + "Dim sgyrsiau eto." + "Ffefrynnau" + "Gallwch ychwanegu sgwrs at eich ffefrynnau yn y gosodiadau sgwrsio. +Am y tro, gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill" + "Does gennych chi ddim hoff sgyrsiau eto" + "Gwahoddiadau" + "Does gennych chi ddim gwahoddiadau yn aros." + "Blaenoriaeth Isel" + "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill" + "Does gennych chi ddim sgyrsiau ar gyfer y dewis hwn" + "Pobl" + "Does gennych chi ddim unrhyw DMs eto" + "Ystafelloedd" + "Dydych chi ddim mewn unrhyw ystafell eto" + "Heb ei ddarllen" + "Llongyfarchiadau! +Does gennych chi ddim negeseuon heb eu darllen!" + "Anfonwyd y cais i ymuno" + "Sgyrsiau" + "Marcio fel wedi\'i ddarllen" + "Marcio fel heb ei ddarllen" + "Mae\'r ystafell hon wedi\'i huwchraddio" + "Mae\'n debyg eich bod chi\'n defnyddio dyfais newydd. Dilyswch gyda dyfais arall i gael mynediad at eich negeseuon wedi\'u hamgryptio." + "Gwiriwch mai chi sydd yna" + diff --git a/features/home/impl/src/main/res/values-da/translations.xml b/features/home/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..ae32f7ab667 --- /dev/null +++ b/features/home/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,50 @@ + + + "Deaktiver batterioptimering for denne app for at sikre, at alle notifikationer dukker op." + "Deaktivér optimering" + "Modtager du ikke notifikationer?" + "Gendan din kryptografiske identitet og meddelelseshistorik med en gendannelsesnøgle, hvis du har mistet alle dine eksisterende enheder." + "Opsæt gendannelse" + "Konfigurer gendannelse for at beskytte din konto" + "Bekræft din gendannelsesnøgle for at bevare adgangen til nøglelager og meddelelseshistorik." + "Indtast din gendannelsesnøgle" + "Har du glemt din gendannelsesnøgle?" + "Dit nøglelager er ikke synkroniseret" + "For at sikre, at du aldrig går glip af et vigtigt opkald, skal du ændre dine indstillinger til at tillade underretninger i fuld skærm, når din telefon er låst." + "Gør din opkaldsoplevelse bedre" + "Er du sikker på, at du vil afvise invitationen til at deltage i %1$s?" + "Afvis invitation" + "Er du sikker på, at du vil afvise denne private samtale med %1$s?" + "Afvis samtale" + "Ingen invitationer" + "%1$s(%2$s ) inviterede dig" + "Dette er en engangsproces, tak for din tålmodighed." + "Sætter din konto op." + "Opret en ny samtale eller et nyt rum" + "Ryd filtre" + "Kom i gang ved at sende en besked til nogen." + "Ingen samtaler endnu." + "Favoritter" + "Du kan tilføje en samtale til dine favoritter i samtaleindstillingerne. +For nu kan du fravælge filtre for at se dine andre samtaler" + "Du har endnu ingen foretrukne samtaler" + "Invitationer" + "Du har ingen afventende invitationer." + "Lav prioritet" + "Du kan fravælge filtre for at se dine andre samtaler" + "Du har ingen samtaler til dette valg" + "Mennesker" + "Du har ingen DM\'er endnu" + "Rum" + "Du er ikke i noget rum endnu" + "Ulæste" + "Tillykke! +Du har ingen ulæste beskeder!" + "Anmodning om at deltage sendt" + "Samtaler" + "Marker som læst" + "Marker som ulæst" + "Dette rum er blevet opgraderet" + "Det ser ud til, at du bruger en ny enhed. Bekræft med en anden enhed for at få adgang til dine krypterede meddelelser." + "Bekræft, at det er dig" + diff --git a/features/home/impl/src/main/res/values-de/translations.xml b/features/home/impl/src/main/res/values-de/translations.xml new file mode 100644 index 00000000000..7f2613809cd --- /dev/null +++ b/features/home/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,50 @@ + + + "Deaktiviere die Batterieoptimierung für diese App, um sicherzustellen, dass alle Benachrichtigungen empfangen werden." + "Optimierung deaktivieren" + "Kommen die Benachrichtigungen nicht an?" + "Falls Sie alle vorhandenen Geräte verloren haben, stellen Sie Ihre kryptografische Identität und Ihren Nachrichtenverlauf mit einem Wiederherstellungsschlüssel wieder her." + "Wiederherstellung einrichten" + "Wiederherstellung einrichten" + "Bestätigen Sie die Validität Ihres Wiederherstellungsschlüssels, um weiterhin auf Ihren Schlüsselspeicher und den Nachrichtenverlauf zugreifen zu können." + "Geben Sie Ihren Wiederherstellungsschlüssel ein" + "Haben Sie Ihren Wiederherstellungsschlüssel vergessen?" + "Ihr Schlüsselspeicher ist nicht synchronisiert" + "Damit Sie keine wichtigen Anrufe verpassen, ändern Sie bitte Ihre Einstellungen, so dass das gesperrte Telefon auch Benachrichtigungen im Vollbildmodus erhalten darf." + "Verbessere dein Anruferlebnis" + "Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?" + "Einladung ablehnen" + "Möchten Sie diesen privaten Chat mit %1$s wirklich ablehnen?" + "Einladung ablehnen" + "Keine Einladungen" + "%1$s (%2$s) hat dich eingeladen" + "Dies ist ein einmaliger Vorgang, danke fürs Warten." + "Dein Konto wird eingerichtet." + "Eine Unterthaltung oder Raum erstellen" + "Filter zurücksetzen" + "Beginnen Sie, indem Sie jemandem eine Nachricht senden." + "Noch keine Chats." + "Favoriten" + "In den Chatroomeinstellungen können Sie einen Chatroom als Favorit markieren. +Deaktivieren Sie den entsprechenden Filter, um Ihre anderen Chatrooms zu sehen" + "Sie haben noch keine Chatrooms als Favorit markiert" + "Einladungen" + "Sie haben keine ausstehenden Einladungen." + "Niedrige Priorität" + "Wähle Filter ab, um Deine Chats zu sehen." + "Diese Chats entsprechen diesen Kriterien nicht." + "Personen" + "Sie haben noch keine Direktnachrichten" + "Räume" + "Sie sind noch in keinem Raum" + "Ungelesen" + "Glückwunsch! +Sie haben keine ungelesenen Nachrichten!" + "Beitrittsanfrage geschickt" + "Chats" + "Als gelesen markieren" + "Als ungelesen markieren" + "Dieser Raum wurde aktualisiert." + "Sie verwenden anscheinend ein neues Gerät. Verifizieren Sie es mit einem anderen Gerät, um Zugriff auf ihre verschlüsselten Nachrichten zu erhalten." + "Bestätigen Sie ihre Identität" + diff --git a/features/home/impl/src/main/res/values-el/translations.xml b/features/home/impl/src/main/res/values-el/translations.xml new file mode 100644 index 00000000000..692aace0ab2 --- /dev/null +++ b/features/home/impl/src/main/res/values-el/translations.xml @@ -0,0 +1,49 @@ + + + "Απενεργοποίησε τη βελτιστοποίηση μπαταρίας για αυτήν την εφαρμογή, για να βεβαιωθείς ότι λαμβάνονται όλες οι ειδοποιήσεις." + "Απενεργοποίηση βελτιστοποίησης" + "Δεν φτάνουν οι ειδοποιήσεις;" + "Δημιούργησε ένα νέο κλειδί ανάκτησης που μπορεί να χρησιμοποιηθεί για την επαναφορά του ιστορικού των κρυπτογραφημένων μηνυμάτων σου σε περίπτωση που χάσεις την πρόσβαση στις συσκευές σου." + "Ρύθμιση ανάκτησης" + "Ρύθμιση ανάκτησης" + "Επιβεβαίωσε το κλειδί ανάκτησης για να διατηρήσεις την πρόσβαση στο χώρο αποθήκευσης κλειδιών και στο ιστορικό μηνυμάτων." + "Εισήγαγε το κλειδί ανάκτησης" + "Ξέχασες το κλειδί ανάκτησης;" + "Ο χώρος αποθήκευσης κλειδιών σου δεν είναι συγχρονισμένος" + "Για να διασφαλίσετε ότι δεν θα χάσετε ποτέ μια σημαντική κλήση, αλλάξτε τις ρυθμίσεις σας ώστε να επιτρέπονται οι ειδοποιήσεις πλήρους οθόνης όταν το τηλέφωνό σας είναι κλειδωμένο." + "Βελτίωσε την εμπειρία κλήσεων" + "Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;" + "Απόρριψη πρόσκλησης" + "Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;" + "Απόρριψη συνομιλίας" + "Χωρίς προσκλήσεις" + "%1$s (%2$s) σέ προσκάλεσε" + "Αυτή είναι μια εφάπαξ διαδικασία, ευχαριστώ που περίμενες." + "Ρύθμιση του λογαριασμού σου." + "Δημιουργία νέας συνομιλίας ή αίθουσας" + "Ξεκίνησε στέλνοντας μηνύματα σε κάποιον." + "Δεν υπάρχουν συνομιλίες ακόμα." + "Αγαπημένα" + "Μπορείς να προσθέσεις μια συνομιλία στα αγαπημένα σου στις ρυθμίσεις συνομιλίας. +Προς το παρόν, μπορείς να καταργήσεις την επιλογή φίλτρων για να δεις τις άλλες συνομιλίες σου" + "Δεν έχεις ακόμα αγαπημένες συνομιλίες" + "Προσκλήσεις" + "Δεν έχεις εκκρεμείς προσκλήσεις." + "Χαμηλής Προτεραιότητας" + "Μπορείς να καταργήσεις την επιλογή φίλτρων για να δεις τις άλλες συνομιλίες σου" + "Δεν έχεις συνομιλίες για αυτήν την επιλογή" + "Άτομα" + "Δεν έχεις ακόμα ΠΜ" + "Αίθουσες" + "Δεν είστε ακόμα σε κάποια αίθουσα" + "Μη αναγνωσμένα" + "Συγχαρητήρια! +Δεν έχεις μη αναγνωσμένα μηνύματα!" + "Το αίτημα συμμετοχής στάλθηκε" + "Συνομιλίες" + "Επισήμανση ως αναγνωσμένου" + "Επισήμανση ως μη αναγνωσμένου" + "Αυτή η αίθουσα έχει αναβαθμιστεί" + "Φαίνεται ότι χρησιμοποιείς μια νέα συσκευή. Επαλήθευσε με άλλη συσκευή για πρόσβαση στα κρυπτογραφημένα σου μηνύματα." + "Επαλήθευσε ότι είσαι εσύ" + diff --git a/features/roomlist/impl/src/main/res/values-en-rUS/translations.xml b/features/home/impl/src/main/res/values-en-rUS/translations.xml similarity index 100% rename from features/roomlist/impl/src/main/res/values-en-rUS/translations.xml rename to features/home/impl/src/main/res/values-en-rUS/translations.xml diff --git a/features/home/impl/src/main/res/values-es/translations.xml b/features/home/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..a58f9e743ae --- /dev/null +++ b/features/home/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,45 @@ + + + "Recupera tu identidad criptográfica y tu historial de mensajes con una clave de recuperación si has perdido todos tus dispositivos actuales." + "Configurar la recuperación" + "Configura la recuperación para proteger tu cuenta" + "Confirma tu clave de recuperación para mantener el acceso a tu almacén de claves y al historial de mensajes." + "Introduce tu clave de recuperación" + "¿Olvidaste tu clave de recuperación?" + "Tu almacén de claves no está sincronizado" + "Para asegurarte de que nunca te pierdas una llamada importante, modifica tus ajustes para permitir notificaciones a pantalla completa cuando el teléfono esté bloqueado." + "Mejora tu experiencia de llamada" + "¿Estás seguro de que quieres rechazar la invitación a unirte a %1$s?" + "Rechazar la invitación" + "¿Estás seguro de que quieres rechazar este chat privado con %1$s?" + "Rechazar el chat" + "Sin invitaciones" + "%1$s (%2$s) te invitó" + "Este proceso solo se hace una vez, gracias por esperar." + "Configura tu cuenta" + "Crear una nueva conversación o sala" + "Empieza enviando un mensaje a alguien." + "Aún no hay chats." + "Favoritos" + "Puedes añadir un chat a tus favoritos en la configuración del chat. +Por ahora, puedes deseleccionar los filtros para ver tus otros chats" + "Aún no tienes chats favoritos" + "Invitaciones" + "No tienes ninguna invitación pendiente." + "Prioridad baja" + "Puedes deseleccionar filtros para ver tus otros chats." + "No tienes chats para esta selección" + "Personas" + "Todavía no tienes ningún mensaje directo" + "Salas" + "Todavía no estás en ninguna sala" + "No leídos" + "¡Felicidades! +¡No tienes ningún mensaje sin leer!" + "Solicitud de unión enviada" + "Chats" + "Marcar como leído" + "Marcar como no leído" + "Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados." + "Verifica que eres tú" + diff --git a/features/home/impl/src/main/res/values-et/translations.xml b/features/home/impl/src/main/res/values-et/translations.xml new file mode 100644 index 00000000000..f8dd7fc086a --- /dev/null +++ b/features/home/impl/src/main/res/values-et/translations.xml @@ -0,0 +1,50 @@ + + + "Kui tahad olla kindel, et näed õigel ajal kõiki teavitusi, siis palun lülita akukasutuse optimeerimine välja." + "Lülita akukasutuse optimeerimine välja" + "Sa ei näe kõiki teavitusi?" + "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?" + "Sinu võtmehoidla pole sünkroonis" + "Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused." + "Sinu tõhusad telefonikõned" + "Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?" + "Lükka kutse tagasi" + "Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?" + "Keeldu vestlusest" + "Kutseid pole" + "%1$s (%2$s) saatis sulle kutse" + "Tänud, et ootad - seda toimingut on vaja teha vaid üks kord." + "Seadistame sinu kasutajakontot." + "Loo uus vestlus või jututuba" + "Tühjenda filtrid" + "Alustamiseks saada kellelegi sõnum." + "Veel pole vestlusi." + "Lemmikud" + "Vestluse seadistusest saad ta määrata lemmikuks. +Aga seni… oma teiste vestluste nägemiseks pead eemaldama filtrid" + "Sul veel pole lemmikvestlusi" + "Kutsed" + "Sul pole ootel kutseid." + "Vähetähtis" + "Oma teiste vestluste nägemiseks sa pead filtrid eemaldama" + "Selle valiku jaoks sul veel pole vestlusi" + "Inimesed" + "Sul pole veel otsevestlusi" + "Jututoad" + "Sa veel ei osale mitte üheski jututoas" + "Lugemata" + "Õnnitleme! +Sul pole ühtegi lugemata sõnumit!" + "Liitumispalve on saadetud" + "Vestlused" + "Märgi loetuks" + "Märgi mitteloetuks" + "See jututuba on uuendatud" + "Tundub, et kasutad uut seadet. Oma krüptitud sõnumite lugemiseks verifitseeri ta mõne muu oma seadmega." + "Verifitseeri, et see oled sina" + diff --git a/features/home/impl/src/main/res/values-eu/translations.xml b/features/home/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..8289dc97d47 --- /dev/null +++ b/features/home/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,41 @@ + + + "Konfiguratu berreskurapena" + "Sartu zure berreskuratze-gakoa" + "Berreskuratze-gakoa ahaztu al duzu?" + "Dei garrantzitsurik galduko ez duzula ziurtatzeko, aldatu ezarpenak telefonoa blokeatuta dagoenean pantaila osoko jakinarazpenak baimentzeko." + "Hobetu deien esperientzia" + "Ziur %1$s(e)ra batzeko gonbidapena baztertu nahi duzula?" + "Baztertu gonbidapena" + "Ziur %1$s(r)en txat pribatua baztertu nahi duzula?" + "Baztertu txata" + "Ez dago gonbidapenik" + "%1$s(e)k (%2$s) gonbidatu zaitu" + "Behin egin beharreko prozesua da; eskerrik asko itxaroteagatik." + "Zure kontua konfiguratzen." + "Sortu elkarrizketa edo gela berria" + "Hasi norbaiti mezuak bidaltzen." + "Oraindik ez dago txatik." + "Gogokoak" + "Txatak gogokoetara gehi dezakezu txaten ezarpenetan. +Oraingoz, iragazkiak desautatu ditzakezu zure gainerako txatak ikusteko" + "Oraindik ez duzu gogoko txatik" + "Gonbidapenak" + "Ez duzu gonbidapenik zain." + "Lehentasun baxua" + "Iragazkiak desautatu ditzakezu gainerako txatak ikusteko" + "Ez duzu hautaketa betetzen duen txatik" + "Jendea" + "Oraindik ez duzu Mezu Pribaturik" + "Gelak" + "Oraindik ez zaude inolako gelatan" + "Irakurri gabeak" + "Bejondeizula! +Ez duzu irakurri gabeko mezurik!" + "Sartzeko eskaera bidali da" + "Txatak" + "Markatu irakurritzat" + "Markatu irakurri gabetzat" + "Gailu berri bat erabiltzen ari zarela dirudi. Egiaztatu beste gailu batekin enkriptatutako mezuak atzitzeko." + "Egiaztatu zu zarela" + diff --git a/features/home/impl/src/main/res/values-fa/translations.xml b/features/home/impl/src/main/res/values-fa/translations.xml new file mode 100644 index 00000000000..ef436cbe90d --- /dev/null +++ b/features/home/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,39 @@ + + + "برپایی بازیابی" + "برپایی بازیابی" + "ورود کلید بازیابیتان" + "ذخیره‌ساز کلیدتان از هم‌گام بودن در آمده" + "بهبود تجریهٔ تماستان" + "مطمئنید که می‌خواهید دعوت پیوستن به %1$s را رد کنید؟" + "رد دعوت" + "مطمئنید که می‌خواهید این گپ خصوصی با %1$s را رد کنید؟" + "رد گپ" + "بدون دعوت" + "%1$s (%2$s) دعوتتان کرد" + "فرایندی یک باره است. ممنون از شکیباییتان." + "برپایی حسابتان." + "ایجاد اتاق یا گفت‌وگویی جدید" + "آغاز با پیام دادن به کسی." + "هنوز گپی وجود ندارد." + "علاقه‌مندی‌ها" + "هنوز هیچ گپ مورد علاقه‌ای ندارید" + "دعوت‌ها" + "هیچ دعوت منتظری ندارید." + "اولویت کم" + "می توانید پالایه‌ها را برای دیدن دیگر گپ‌هایتان بردارید" + "هیچ گپی برای این گزینش ندارید" + "افراد" + "هنوز هیچ پیام مستقیمی ندارید" + "اتاق‌ها" + "هنوز در هیچ اتاقی نیستید" + "نخوانده‌ها" + "تبریک! +هیچ پیام نخوانده‌ای ندارید!" + "درخواست پیوستن فرستاده شد" + "گپ‌ها" + "علامت‌گذاری به عنوان خوانده شده" + "نشان به ناخوانده" + "به نظر می رسد از دستگاه جدیدی استفاده می کنید. برای دسترسی به پیام های رمزگذاری شده خود، با دستگاه دیگری این دستگاه را تأیید کنید." + "تأیید کنید که خودتانید" + diff --git a/features/home/impl/src/main/res/values-fi/translations.xml b/features/home/impl/src/main/res/values-fi/translations.xml new file mode 100644 index 00000000000..10da41c88b1 --- /dev/null +++ b/features/home/impl/src/main/res/values-fi/translations.xml @@ -0,0 +1,46 @@ + + + "Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, mikäli menetät pääsyn kaikkiin laitteisiisi." + "Ota palautus käyttöön" + "Ota palautus käyttöön tilisi suojaamiseksi" + "Vahvista palautusavaimesi, jotta pääset edelleen käyttämään avainten säilytystä ja viestihistoriaa." + "Syötä palautusavaimesi" + "Unohditko palautusavaimesi?" + "Avainten säilytys ei ole synkronoitu" + "Salli koko näytön ilmoitukset, kun laite on lukittu, jos et halua koskaan missata tärkeää puhelua." + "Paranna puhelukokemustasi" + "Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?" + "Hylkää kutsu" + "Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?" + "Hylkää keskustelu" + "Ei kutsuja" + "%1$s (%2$s) kutsui sinut" + "Tämä on kertaluonteinen prosessi, kiitos odottamisesta." + "Tiliä määritetään." + "Luo uusi keskustelu tai huone" + "Aloita lähettämällä viesti jollekin." + "Sinulla ei ole vielä keskusteluja." + "Suosikit" + "Voit lisätä keskustelun suosikkeihisi keskustelun asetuksissa. +Toistaiseksi voit poistaa suodattimien valinnan, jotta näet muut keskustelut." + "Sinulla ei ole vielä suosikkikeskusteluja" + "Kutsut" + "Sinulla ei ole yhtään odottavaa kutsua." + "Matala prioriteetti" + "Voit poistaa suodattimien valinnan nähdäksesi muut keskustelusi." + "Sinulla ei ole sopivia keskusteluja tähän valintaan" + "Ihmiset" + "Sinulla ei ole vielä yhtään yksityisviestiä" + "Huoneet" + "Et ole vielä missään huoneessa" + "Lukemattomat" + "Onnittelut! +Sinulla ei ole lukemattomia viestejä!" + "Liittymispyyntö lähetetty" + "Keskustelut" + "Merkitse luetuksi" + "Merkitse lukemattomaksi" + "Tämä huone on päivitetty" + "Vaikuttaisi siltä, että käytät uutta laitetta. Vahvista toisella laitteella nähdäksesi salatut viestit." + "Vahvista, että se olet sinä" + diff --git a/features/home/impl/src/main/res/values-fr/translations.xml b/features/home/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 00000000000..3f86a648105 --- /dev/null +++ b/features/home/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,50 @@ + + + "Désactivez l’optimisation de la batterie pour cette application afin de vous assurer que toutes les notifications sont reçues." + "Désactiver l’optimisation" + "Ils vous manque des notifications?" + "Générez une nouvelle clé de récupération qui peut être utilisée pour restaurer l’historique de vos messages chiffrés au cas où vous perdriez l’accès à vos appareils." + "Configurer la sauvegarde" + "Configurer la récupération" + "Confirmez votre clé de récupération pour conserver l’accès à votre stockage de clés et à l’historique des messages." + "Saisissez votre clé de récupération" + "Clé de récupération oubliée ?" + "Le stockage de vos clés n’est pas synchronisé" + "Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé." + "Améliorez votre expérience d’appel" + "Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?" + "Refuser l’invitation" + "Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?" + "Refuser l’invitation" + "Aucune invitation" + "%1$s (%2$s) vous a invité(e)" + "Il s’agit d’une opération ponctuelle, merci d’attendre quelques instants." + "Configuration de votre compte." + "Créer une nouvelle discussion ou un nouveau salon" + "Supprimer les filtres" + "Commencez par envoyer un message à quelqu’un." + "Aucune discussion pour le moment." + "Favoris" + "Vous pouvez ajouter une discussion aux favoris depuis les paramètres de la discussion. +En attendant, vous pouvez désélectionner des filtres pour voir vos autres salons." + "Vous n’avez pas encore de discussions favorites" + "Invitations" + "Vous n’avez aucune invitation en attente." + "Priorité basse" + "Veuillez désélectionner des filtres pour voir vos discussions" + "Vous n’avez pas de discussions pour cette sélection" + "Personnes" + "Vous n’avez pas encore de discussions" + "Salons" + "Vous n’êtes membre d’aucun salon" + "Non-lus" + "Félicitations ! +Vous n’avez plus de messages non-lus !" + "Demande de rejoindre le salon envoyée" + "Conversations" + "Marquer comme lu" + "Marquer comme non lu" + "Ce salon a été mis à niveau." + "Il semblerait que vous utilisiez un nouvel appareil. Vérifiez la session avec un autre de vos appareils pour accéder à vos messages chiffrés." + "Vérifier que c’est bien vous" + diff --git a/features/home/impl/src/main/res/values-hu/translations.xml b/features/home/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 00000000000..e0763e4c6fd --- /dev/null +++ b/features/home/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,50 @@ + + + "Kapcsolja ki az alkalmazás akkumulátor-optimalizálását, hogy biztosan megkapja az összes értesítést." + "Optimalizálás letiltása" + "Nem érkeznek meg az értesítések?" + "Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést." + "Helyreállítás beállítása" + "Helyreállítás beállítása a fiókja védelméhez" + "Erősítse meg a helyreállítási kulcsát, hogy továbbra is hozzáférjen a kulcstárolójához és az üzenetelőzményekhez." + "Adja meg a helyreállítási kulcsot" + "Elfelejtette a helyreállítási kulcsot?" + "A kulcstároló nincs szinkronizálva" + "Hogy sose maradjon le egyetlen fontos hívásról sem, a beállításokban engedélyezze a teljes képernyős értesítéseket, amikor a telefon zárolva van." + "Fokozza a hívásélményét" + "Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?" + "Meghívás elutasítása" + "Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?" + "Csevegés elutasítása" + "Nincsenek meghívások" + "%1$s (%2$s) meghívta" + "Ez egy egyszeri folyamat, köszönjük a türelmét." + "A fiók beállítása." + "Új beszélgetés vagy szoba létrehozása" + "Szűrők törlése" + "Kezdje azzal, hogy üzenetet küld valakinek." + "Még nincsenek csevegések." + "Kedvencek" + "A csevegési beállításokban csevegéseket adhat hozzá a kedvencekhez. +Egyelőre törölheti a szűrőket a többi csevegés megtekintéséhez." + "Még nincsenek kedvenc csevegései" + "Meghívások" + "Nincsenek függőben lévő meghívásai." + "Alacsony prioritás" + "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez" + "Ehhez a kiválasztáshoz nem tartoznak csevegések" + "Emberek" + "Még nincsenek privát üzenetei" + "Szobák" + "Még nincs egy szobában sem" + "Olvasatlan" + "Gratulálunk! +Nincs olvasatlan üzenete!" + "Csatlakozási kérés elküldve" + "Összes csevegés" + "Megjelölés olvasottként" + "Megjelölés olvasatlanként" + "A szoba verzióját frissítették" + "Úgy tűnik, hogy új eszközt használ. Ellenőrizze egy másik eszközzel, hogy a továbbiakban elérje a titkosított üzeneteket." + "Ellenőrizze, hogy Ön az" + diff --git a/features/home/impl/src/main/res/values-in/translations.xml b/features/home/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..b3e16b4c25a --- /dev/null +++ b/features/home/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,50 @@ + + + "Nonaktifkan pengoptimalan baterai untuk aplikasi ini, untuk memastikan semua notifikasi diterima." + "Nonaktifkan optimasi" + "Notifikasi tidak masuk?" + "Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda." + "Siapkan pemulihan" + "Siapkan pemulihan" + "Konfirmasikan kunci pemulihan Anda untuk mempertahankan akses ke penyimpanan kunci dan riwayat pesan Anda." + "Masukkan kunci pemulihan Anda" + "Lupa kunci pemulihan Anda?" + "Penyimpanan kunci Anda tidak sinkron" + "Untuk memastikan Anda tidak melewatkan panggilan penting, silakan ubah pengaturan Anda untuk memperbolehkan notifikasi layar penuh ketika ponsel Anda terkunci." + "Tingkatkan pengalaman panggilan Anda" + "Apakah Anda yakin ingin menolak undangan untuk bergabung ke %1$s?" + "Tolak undangan" + "Apakah Anda yakin ingin menolak obrolan pribadi dengan %1$s?" + "Tolak obrolan" + "Tidak ada undangan" + "%1$s (%2$s) mengundang Anda" + "Ini adalah proses satu kali, terima kasih telah menunggu." + "Menyiapkan akun Anda." + "Buat percakapan atau ruangan baru" + "Hapus filter" + "Mulailah dengan mengirim pesan kepada seseorang." + "Belum ada obrolan." + "Favorit" + "Anda dapat menambahkan percakapan ke favorit Anda dalam pengaturan percakapan. +Untuk sementara, Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain" + "Anda belum memiliki percakapan favorit" + "Undangan" + "Anda tidak memiliki undangan yang tertunda." + "Prioritas Rendah" + "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain" + "Anda tidak memiliki percakapan untuk pemilihan ini" + "Orang" + "Anda belum memiliki percakapan langsung" + "Ruangan" + "Anda belum berada dalam ruangan" + "Belum dibaca" + "Selamat! +Anda tidak memiliki pesan yang belum dibaca!" + "Permintaan untuk bergabung dikirim" + "Semua Obrolan" + "Tandai sebagai dibaca" + "Tandai sebagai belum dibaca" + "Ruangan ini telah ditingkatkan" + "Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya." + "Verifikasi bahwa ini Anda" + diff --git a/features/home/impl/src/main/res/values-it/translations.xml b/features/home/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..88ff0eb3be4 --- /dev/null +++ b/features/home/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,50 @@ + + + "Disabilita l\'ottimizzazione della batteria per questa app, per assicurarti che tutte le notifiche vengano ricevute." + "Disabilita l\'ottimizzazione" + "Le notifiche non arrivano?" + "Genera una nuova chiave di recupero che può essere usata per ripristinare la cronologia dei messaggi crittografati nel caso in cui tu perda l\'accesso ai tuoi dispositivi." + "Configura il recupero" + "Configura il ripristino" + "Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi." + "Inserisci la tua chiave di recupero" + "Hai dimenticato la chiave di recupero?" + "L\'archiviazione delle chiavi non è sincronizzata" + "Per non perdere mai una chiamata importante, modifica le impostazioni per consentire le notifiche a schermo intero quando il telefono è bloccato." + "Migliora la tua esperienza di chiamata" + "Vuoi davvero rifiutare l\'invito ad entrare in %1$s?" + "Rifiuta l\'invito" + "Vuoi davvero rifiutare questa conversazione privata con %1$s?" + "Rifiuta l\'invito alla conversazione" + "Nessun invito" + "%1$s (%2$s) ti ha invitato" + "Si tratta di una procedura che si effettua una sola volta, grazie per l\'attesa." + "Configurazione del tuo account." + "Crea una nuova conversazione o stanza" + "Elimina filtri" + "Inizia inviando un messaggio a qualcuno." + "Ancora nessuna conversazione." + "Preferiti" + "Puoi aggiungere una conversazione ai tuoi preferiti nelle impostazioni della stessa. +Per il momento, puoi deselezionare i filtri per vedere le altre conversazioni." + "Non hai ancora conversazioni preferite" + "Inviti" + "Non hai nessun invito in sospeso." + "Bassa priorità" + "Puoi deselezionare i filtri per vedere le altre conversazioni." + "Non hai conversazioni per questa selezione" + "Persone" + "Non hai ancora nessuna conversazione diretta" + "Stanze" + "Non sei ancora in nessuna stanza" + "Non letti" + "Congratulazioni! +Non hai messaggi non letti!" + "Richiesta di accesso inviata" + "Tutte le conversazioni" + "Segna come letto" + "Segna come non letto" + "Questa stanza è stata aggiornata" + "Sembra che tu stia usando un nuovo dispositivo. Verificati con un altro dispositivo per accedere ai tuoi messaggi cifrati." + "Verifica che sei tu" + diff --git a/features/home/impl/src/main/res/values-ka/translations.xml b/features/home/impl/src/main/res/values-ka/translations.xml new file mode 100644 index 00000000000..bcedaeac607 --- /dev/null +++ b/features/home/impl/src/main/res/values-ka/translations.xml @@ -0,0 +1,37 @@ + + + "აღდგენის დაყენება" + "დაადასტურეთ თქვენი აღდგენის გასაღები რათა გქონდეთ წვდომა გასაღებების დამგროვებელთან და შეტყობინებების ისტორიასთან." + "თქვენი გასაღების დამგროვებელი არაა სინქრონიზებული" + "დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ მოწვევაზე %1$s-ში?" + "მოწვევაზე უარის თქმა" + "დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ ჩატზე %1$s-თან?" + "ჩატზე უარის თქვა" + "მოწვევები არ არის" + "%1$s (%2$s) მოგიწვიათ" + "ეს არის ერთჯერადი პროცესი, მადლობა ლოდინისთვის." + "თქვენი ანგარიშის კონფიგურაცია" + "ახალი საუბრისა ან ოთახის შექმნა" + "დაიწყეთ ვინმესთვის შეტყობინების გაგზავნით." + "არც ერთი ჩატი ჯერ არაა." + "რჩეულები" + "თქვენ შეგიძლიათ ოთახის რჩეულებში დამატება ოთახების პარამეტრებში. +ახლა კი შეგიძლიათ ფილტრების მოხსნა სხვა ოთახების გამოსაჩენად" + "თქვენ ჯერ არ გაქვთ რჩეული ჩატები" + "მოწვევები" + "დაბალი პრიორიტეტი" + "თქვენ შეგიძლიათ წაშალოთ ფილტრები სხვა ჩეთების გამოსაჩენად" + "თქვენ არ გაქვთ ოთახები ამ არჩევნისთვის" + "ხალხი" + "თქვენ ჯერ არ გაქვთ პირადი შეტყობინებები" + "ოთახები" + "თქვენ ჯერ არც ერთ ოთახში არ ხართ" + "წაუკითხავი" + "გილოცავთ! +თქვენ არ გაქვთ წაუკითხავი შეტყობინებები!" + "ჩატები" + "წაკითხულად მონიშვნა" + "წაუკითხავად მონიშვნა" + "როგორც ჩანს, ახალ მოწყობილობას იყენებთ. დაადასტურეთ სხვა მოწყობილობით თქვენს დაშიფრულ შეტყობინებებზე წვდომისთვის." + "დაადასტურეთ, რომ ეს თქვენ ხართ" + diff --git a/features/home/impl/src/main/res/values-lt/translations.xml b/features/home/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..37e9003c206 --- /dev/null +++ b/features/home/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,15 @@ + + + "Ar tikrai norite atmesti kvietimą prisijungti prie %1$s?" + "Atmesti kvietimą" + "Ar tikrai norite atmesti šį privatų pokalbį su %1$s ?" + "Atmesti pokalbį" + "Jokių kvietimų" + "%1$s(%2$s) pakvietė Jus" + "Sukurti naują pokalbį arba kambarį" + "Kvietimai" + "Žmonės" + "Pokalbiai" + "Panašu, kad naudojate naują įrenginį. Patvirtinkite naudodami kitą įrenginį, kad galėtumėte pasiekti savo šifruotas žinutes." + "Patvirtinkite, kad tai Jūs" + diff --git a/features/home/impl/src/main/res/values-nb/translations.xml b/features/home/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..2ce6733b8aa --- /dev/null +++ b/features/home/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,49 @@ + + + "Deaktiver batterioptimalisering for denne appen for å sikre at alle varsler mottas." + "Deaktiver optimalisering" + "Kommer ikke varslene frem?" + "Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter." + "Konfigurer gjenoppretting" + "Konfigurer gjenoppretting for å beskytte kontoen din" + "Verifiser gjenopprettingsnøkkelen for å opprettholde tilgangen til nøkkellageret og meldingshistorikken." + "Skriv inn gjenopprettingsnøkkelen din" + "Har du glemt din gjenopprettingsnøkkel?" + "Nøkkellagringen din er ikke synkronisert" + "For å sikre at du aldri går glipp av en viktig samtale, må du endre innstillingene dine for å tillate fullskjermvarsler når telefonen er låst." + "Forbedre samtaleopplevelsen din" + "Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?" + "Avvis invitasjon" + "Er du sikker på at du vil avslå denne private chatten med %1$s?" + "Avslå chat" + "Ingen invitasjoner" + "%1$s(%2$s) inviterte deg" + "Dette er en engangsprosess, takk for at du venter." + "Setter opp kontoen din." + "Opprett en ny samtale eller et nytt rom" + "Kom i gang med å sende meldinger til noen." + "Ingen chatter ennå." + "Favoritter" + "Du kan legge til en chat blant favorittene dine i chat-innstillingene. +Inntil videre kan du velge bort filtre for å se de andre chattene dine" + "Du har ikke favorittchatter ennå" + "Invitasjoner" + "Du har ingen ventende invitasjoner." + "Lav prioritet" + "Du kan velge bort filtre for å se de andre chattene dine" + "Du har ikke chatter for dette utvalget" + "Personer" + "Du har ingen DM-er ennå" + "Rom" + "Du er ikke i noe rom ennå" + "Uleste" + "Gratulerer! +Du har ingen uleste meldinger!" + "Forespørsel om å bli med sendt" + "Chatter" + "Marker som lest" + "Merk som ulest" + "Dette rommet har blitt oppgradert" + "Det ser ut til at du bruker en ny enhet. Bekreft med en annen enhet for å få tilgang til de krypterte meldingene dine." + "Bekreft at det er deg" + diff --git a/features/home/impl/src/main/res/values-nl/translations.xml b/features/home/impl/src/main/res/values-nl/translations.xml new file mode 100644 index 00000000000..fc207da0167 --- /dev/null +++ b/features/home/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,43 @@ + + + "Herstel je cryptografische identiteit en berichtengeschiedenis met een herstelsleutel voor als je al je bestaande apparaten kwijt bent." + "Herstelmogelijkheid instellen" + "Herstel instellen om je account te beschermen" + "Bevestig je herstelsleutel om toegang te houden tot je sleutelopslag en berichtengeschiedenis." + "Je sleutelopslag is niet gesynchroniseerd" + "Pas je instellingen aan om meldingen op het volledige scherm toe te staan wanneer de telefoon is vergrendeld. Zo mis je nooit een belangrijk gesprek." + "Verbeter je gesprekservaring" + "Weet je zeker dat je de uitnodiging om toe te treden tot %1$s wilt weigeren?" + "Uitnodiging weigeren" + "Weet je zeker dat je deze privéchat met %1$s wilt weigeren?" + "Chat weigeren" + "Geen uitnodigingen" + "%1$s (%2$s) heeft je uitgenodigd" + "Dit is een eenmalig proces, bedankt voor het wachten." + "Je account instellen." + "Begin een nieuw gesprek of maak een nieuwe kamer" + "Ga aan de slag door iemand een bericht te sturen." + "Nog geen chats." + "Favorieten" + "Je kunt een chat toevoegen aan je favorieten in de chatinstellingen. +Voor nu kun je filters deselecteren om je andere chats te zien" + "Je hebt nog geen favoriete chats" + "Uitnodigingen" + "Je hebt geen openstaande uitnodigingen." + "Lage prioriteit" + "Je kunt filters deselecteren om je andere chats te zien" + "Je hebt geen chats voor deze selectie" + "Personen" + "Je hebt nog geen directe chats" + "Kamers" + "Je zit nog niet in een kamer" + "Ongelezen" + "Gefeliciteerd! +Je hebt geen ongelezen berichten!" + "Verzoek om toe te treden verzonden" + "Chats" + "Markeren als gelezen" + "Markeren als ongelezen" + "Het lijkt erop dat je een nieuw apparaat gebruikt. Verifieer met een ander apparaat om toegang te krijgen tot je versleutelde berichten." + "Verifieer dat jij het bent" + diff --git a/features/home/impl/src/main/res/values-pl/translations.xml b/features/home/impl/src/main/res/values-pl/translations.xml new file mode 100644 index 00000000000..8bb236e71a5 --- /dev/null +++ b/features/home/impl/src/main/res/values-pl/translations.xml @@ -0,0 +1,45 @@ + + + "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?" + "Magazyn kluczy nie jest zsynchronizowany" + "Upewnij się, że nie pominiesz żadnego połączenia. Zmień swoje ustawienia i zezwól na powiadomienia na blokadzie ekranu." + "Popraw jakość swoich rozmów" + "Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?" + "Odrzuć zaproszenie" + "Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?" + "Odrzuć czat" + "Brak zaproszeń" + "%1$s (%2$s) zaprosił Cię" + "Jest to jednorazowy proces, dziękujemy za czekanie." + "Konfigurowanie Twojego konta." + "Utwórz nową rozmowę lub pokój" + "Wyślij komuś wiadomość, aby rozpocząć." + "Brak czatów." + "Ulubione" + "Możesz dodać czat do ulubionych w ustawieniach czatu. +Na razie możesz wyczyścić filtry, aby zobaczyć pozostałe czaty" + "Nie masz jeszcze ulubionych czatów" + "Zaproszenia" + "Nie masz żadnych oczekujących zaproszeń." + "Niski priorytet" + "Wyczyść filtry, aby zobaczyć pozostałe czaty" + "Brak czatów dla podanych kryteriów" + "Osoby" + "Nie masz jeszcze żadnych PW" + "Pokoje" + "Nie jesteś jeszcze w żadnym pokoju" + "Nieprzeczytane" + "Gratulacje! +Nie masz żadnych nieprzeczytanych wiadomości!" + "Wysłano prośbę o dołączenie" + "Wszystkie czaty" + "Oznacz jako przeczytane" + "Oznacz jako nieprzeczytane" + "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-rBR/translations.xml b/features/home/impl/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..871a2bcc5cc --- /dev/null +++ b/features/home/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,50 @@ + + + "Desative a otimização de bateria para este app, para que tenha certeza que todas as notificações sejam recebidas." + "Desativar otimização" + "As notificações não chegam?" + "Recupere sua identidade criptográfica e o histórico de mensagens com uma chave de recuperação se você tiver perdido todos os dispositivos existentes." + "Configurar a recuperação" + "Configure a recuperação para proteger sua conta" + "Confirme sua chave de recuperação para manter o acesso ao seu armazenamento de chaves e histórico de mensagens." + "Digite sua chave de recuperação" + "Esqueceu sua chave de recuperação?" + "Seu armazenamento de chaves está fora de sincronia" + "Para garantir que você nunca perca uma chamada importante, por favor altere as suas configurações para permitir notificações em tela cheia enquanto o seu celular estiver bloqueado." + "Melhore a sua experiência de chamadas" + "Tem certeza de que deseja recusar o convite para ingressar em %1$s?" + "Recusar convite" + "Tem certeza de que deseja recusar esse chat privado com %1$s?" + "Recusar chat" + "Sem convites" + "%1$s(%2$s) convidou você" + "Este é um processo único, obrigado por esperar." + "Configurando sua conta." + "Criar uma nova conversa ou sala" + "Limpar filtros" + "Comece enviando uma mensagem para alguém." + "Ainda não há conversas." + "Favoritos" + "Você pode adicionar um bate-papo aos seus favoritos nas configurações de bate-papo. +Por enquanto, você pode desmarcar os filtros para ver seus outros bate-papos" + "Você não tem nenhuma conversa favorita ainda" + "Convites" + "Você não tem nenhum convite pendente." + "Baixa prioridade" + "Você pode desmarcar filtros para ver suas outras conversas" + "Você não tem conversas para esta seleção" + "Pessoas" + "Você não tem nenhum conversa privada ainda" + "Salas" + "Você não está em nenhuma sala ainda" + "Não lidos" + "Parabéns! +Você não tem nenhuma mensagem não lida!" + "Pedido de adesão enviado" + "Conversas" + "Marcar como lido" + "Marcar como não lido" + "Esta sala foi atualizada" + "Parece que você está usando um novo dispositivo. Verifique com outro dispositivo para acessar suas mensagens criptografadas." + "Verifique se é você" + diff --git a/features/home/impl/src/main/res/values-pt/translations.xml b/features/home/impl/src/main/res/values-pt/translations.xml new file mode 100644 index 00000000000..daba977faac --- /dev/null +++ b/features/home/impl/src/main/res/values-pt/translations.xml @@ -0,0 +1,50 @@ + + + "Desativa as otimizações de bateria para esta aplicação, de modo a garantir que todas as notificações chegam." + "Desativar otimizações" + "As notificações não chegam?" + "Recupera a tua identidade criptográfica e o histórico de mensagens com uma chave de recuperação se tiveres perdido todos os teus dispositivos existentes." + "Configurar recuperação" + "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" + "Esqueceste-te da tua chave de recuperação?" + "O teu armazenamento de chaves não está sincronizado" + "Para garantir que nunca perdes uma chamada importante, altera as configurações para permitir notificações em ecrã inteiro quando o telemóvel está bloqueado." + "Melhora a tua experiência de chamada" + "Tens a certeza que queres rejeitar o convite para entra em %1$s?" + "Rejeitar convite" + "Tem a certeza que queres rejeitar esta conversa privada com %1$s?" + "Rejeitar conversa" + "Sem convites" + "%1$s (%2$s) convidou-te" + "Este processo só acontece uma única vez, obrigado por esperares." + "A configurar a tua conta…" + "Criar uma nova conversa ou sala" + "Limpar filtros" + "Começa por enviar uma mensagem a alguém." + "Ainda não tens conversas." + "Favoritas" + "Podes adicionar uma conversa às tuas favoritas nas suas configurações. +Por enquanto, podes anular a seleção dos filtros para veres as tuas outras conversas" + "Ainda não tens nenhuma conversa favorita" + "Convites" + "Não tens nenhum convite pendente." + "Prioridade baixa" + "Podes anular a seleção dos filtros para veres as tuas outras conversas" + "Não tens nenhuma conversa selecionada" + "Pessoas" + "Ainda não tens nenhuma MD (mensagem direta)" + "Salas" + "Ainda não estás em nenhuma sala" + "Por ler" + "Parabéns! +Não tens nenhuma mensagem por ler!" + "Pedido de adesão enviado" + "Conversas" + "Marcar como lida" + "Marcar como não lida" + "Esta sala foi atualizada" + "Parece que estás a utilizar um novo dispositivo. Verifica-o com um outro para poderes aceder às tuas mensagens cifradas." + "Verifica que és tu" + diff --git a/features/home/impl/src/main/res/values-ro/translations.xml b/features/home/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..2d7bdd8a952 --- /dev/null +++ b/features/home/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,43 @@ + + + "Recuperați-vă identitatea criptografică și istoricul mesajelor 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 în prezent. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup." + "Backup-ul nu este sincronizat" + "Pentru a vă asigura că nu pierdeți niciodată un apel important, vă rugăm să modificați setările pentru a permite notificări fullscreen atunci când telefonul este blocat." + "Îmbunătățiți-vă experiența in timpul unui apel" + "Sigur doriți să refuzați alăturarea la %1$s?" + "Refuzați invitația" + "Sigur doriți să refuzați conversațiile cu %1$s?" + "Refuzați conversația" + "Nicio invitație" + "%1$s (%2$s) v-a invitat." + "Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare." + "Contul dumneavoastră se configurează" + "Creați o conversație sau o cameră nouă" + "Începeți prin a trimite mesaje cuiva." + "Nu există încă discuții." + "Favorite" + "Puteți adăuga un chat la preferințele dvs. în setările de chat. +Deocamdată, puteți deselecta filtrele pentru a vedea celelalte chat-uri" + "Încă nu aveți conversații preferate" + "Invitații" + "Nu aveți invitații în așteptare." + "Prioritate scăzută" + "Puteți deselecta filtrele pentru a vedea celelalte chat-uri" + "Nu aveți chat-uri pentru această selecție" + "Persoane" + "Încă nu aveți DM-uri" + "Camere" + "Nu sunteți încă în nicio cameră" + "Necitite" + "Felicitari! +Nu aveți mesaje necitite!" + "Cererea de alăturare a fost trimisă" + "Toate conversatiile" + "Marcați ca citită" + "Marcați ca necitită" + "Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea cu un alt dispozitiv pentru a accesa mesajele dumneavoastră criptate." + "Verificați că sunteți dumneavoastră" + diff --git a/features/home/impl/src/main/res/values-ru/translations.xml b/features/home/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 00000000000..a70efcfdfdd --- /dev/null +++ b/features/home/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,46 @@ + + + "Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам." + "Настроить восстановление" + "Для защиты вашего аккаунта рекомендуется настроить восстановление" + "Подтвердите ключ восстановления, чтобы сохранить доступ к хранилищу ключей и истории сообщений." + "Введите ключ восстановления" + "Забыли ключ восстановления?" + "Хранилище ключей не синхронизировано" + "Чтобы больше не пропускать важные звонки, разрешите приложению показывать полноэкранные уведомления на заблокированном экране телефона." + "Улучшите качество звонков" + "Вы уверены, что хотите отклонить приглашение в %1$s?" + "Отклонить приглашение" + "Вы уверены, что хотите отказаться от личного общения с %1$s?" + "Отклонить чат" + "Нет приглашений" + "%1$s (%2$s) пригласил вас" + "Это одноразовый процесс, спасибо, что подождали." + "Настройка учетной записи." + "Создайте новую беседу или комнату" + "Начните переписку с отправки сообщения." + "Пока нет доступных чатов." + "Избранное" + "Добавить чат в избранное можно в настройках чата. +На данный момент вы можете убрать фильтры, чтобы увидеть другие ваши чаты." + "У вас пока нет избранных чатов" + "Приглашения" + "У вас нет отложенных приглашений." + "Низкий приоритет" + "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты." + "У вас нет чатов для этой подборки" + "Пользователи" + "У вас пока нет личных сообщений" + "Комнаты" + "Вас пока нет ни в одной комнате" + "Непрочитанные" + "Поздравляем! +У вас нет непрочитанных сообщений!" + "Запрос на присоединение отправлен" + "Все чаты" + "Пометить как прочитанное" + "Отметить как непрочитанное" + "Эта комната была обновлена" + "Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям пройдите подтверждение с другим устройством." + "Подтвердите, что это вы" + diff --git a/features/home/impl/src/main/res/values-sk/translations.xml b/features/home/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 00000000000..ade2c192c7b --- /dev/null +++ b/features/home/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,50 @@ + + + "Vypnite optimalizáciu batérie pre túto aplikáciu, aby ste sa uistili, že sú prijaté všetky upozornenia." + "Zakázať optimalizáciu" + "Oznámenia neprichádzajú?" + "Vytvorte nový kľúč na obnovenie, ktorý môžete použiť na obnovenie vašej histórie šifrovaných správ v prípade straty prístupu k vašim zariadeniam." + "Nastaviť obnovenie" + "Nastaviť obnovenie" + "Potvrďte svoj kľúč na obnovenie, aby ste zachovali prístup k úložisku kľúčov a histórii správ." + "Zadajte kľúč na obnovenie" + "Zabudli ste svoj kľúč na obnovenie?" + "Vaše úložisko kľúčov nie je synchronizované" + "Aby ste už nikdy nezmeškali dôležitý hovor, zmeňte svoje nastavenia a povoľte upozornenia na celú obrazovku, keď je váš telefón uzamknutý." + "Vylepšite svoj zážitok z hovoru" + "Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?" + "Odmietnuť pozvanie" + "Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?" + "Odmietnuť konverzáciu" + "Žiadne pozvánky" + "%1$s (%2$s) vás pozval/a" + "Ide o jednorazový proces, ďakujeme za trpezlivosť." + "Nastavenie vášho účtu." + "Vytvorte novú konverzáciu alebo miestnosť" + "Vyčistiť filtre" + "Začnite tým, že niekomu pošlete správu." + "Zatiaľ žiadne konverzácie." + "Obľúbené" + "Môžete pridať konverzáciu medzi obľúbené v nastaveniach konverzácie. +Zatiaľ môžete zrušiť výber filtrov, aby ste videli ostatné konverzácie" + "Zatiaľ nemáte obľúbené konverzácie" + "Pozvánky" + "Nemáte žiadne čakajúce pozvánky." + "Nízka priorita" + "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie" + "Nemáte konverzácie pre tento výber" + "Ľudia" + "Zatiaľ nemáte žiadne priame správy" + "Miestnosti" + "Zatiaľ ešte nie ste v žiadnej miestnosti" + "Neprečítané" + "Gratulujeme! +Nemáte žiadne neprečítané správy!" + "Žiadosť o pripojenie bola odoslaná" + "Všetky konverzácie" + "Označiť ako prečítané" + "Označiť ako neprečítané" + "Táto miestnosť bola aktualizovaná" + "Vyzerá to tak, že používate nové zariadenie. Overte svoj prístup k zašifrovaným správam pomocou vášho druhého zariadenia." + "Overte, že ste to vy" + diff --git a/features/home/impl/src/main/res/values-sv/translations.xml b/features/home/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 00000000000..ab00fd3dd3a --- /dev/null +++ b/features/home/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,46 @@ + + + "Skapa en ny återställningsnyckel som kan användas för att återställa din krypterade meddelandehistorik om du förlorar åtkomst till dina enheter." + "Ställ in återställning" + "Ställ in återställning" + "Bekräfta din återställningsnyckel för att behålla åtkomsten till din nyckellagring och meddelandehistorik." + "Ange din återställningsnyckel" + "Glömt din återställningsnyckel?" + "Din nyckellagring är inte synkroniserad" + "För att säkerställa att du aldrig missar ett viktigt samtal, ändra dina inställningar för att tillåta helskärmsmeddelanden när telefonen är låst." + "Förbättra din samtalsupplevelse" + "Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?" + "Avböj inbjudan" + "Är du säker på att du vill avböja denna privata chatt med %1$s?" + "Avböj chatt" + "Inga inbjudningar" + "%1$s (%2$s) bjöd in dig" + "Detta är en engångsprocess, tack för att du väntar." + "Konfigurerar ditt konto" + "Skapa en ny konversation eller ett nytt rum" + "Kom igång genom att skicka meddelanden till någon." + "Inga chattar än." + "Favoriter" + "Du kan lägga till en chatt till dina favoriter i chattinställningarna. +För tillfället kan du avmarkera filter för att se dina andra chattar" + "Du har inga favoritchattar än" + "Inbjudningar" + "Du har inga väntande inbjudningar." + "Låg prioritet" + "Du kan avmarkera filter för att se dina andra chattar" + "Du har inga chattar för det här valet" + "Personer" + "Du har inga DM:er än" + "Rum" + "Du är inte i något rum än" + "Olästa" + "Grattis! +Du har inga olästa meddelanden!" + "Begäran om att gå med skickad" + "Alla chattar" + "Markera som läst" + "Markera som oläst" + "Det här rummet har uppgraderats" + "Det verkar som om du använder en ny enhet. Verifiera med en annan enhet för att komma åt dina krypterade meddelanden." + "Verifiera att det är du" + diff --git a/features/home/impl/src/main/res/values-tr/translations.xml b/features/home/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..6f42809ef32 --- /dev/null +++ b/features/home/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,45 @@ + + + "Mevcut tüm cihazlarınızı kaybettiyseniz şifreleme kimliğinizi ve mesaj geçmişinizi bir kurtarma anahtarıyla kurtarın." + "Kurtarmayı ayarlayın" + "Hesabınızı korumak için kurtarmayı ayarlayın" + "Anahtar depolama alanınıza ve mesaj geçmişinize erişimi sürdürmek için kurtarma anahtarınızı onaylayın." + "Kurtarma anahtarınızı girin" + "Kurtarma anahtarınızı mı unuttunuz?" + "Anahtar depolama alanınız senkronize değil" + "Önemli bir aramayı asla kaçırmamak için, telefonunuz kilitliyken tam ekran bildirimlere izin vermek üzere ayarlarınızı değiştirin." + "Arama deneyiminizi geliştirin" + "%1$s katılma davetini reddetmek istediğinizden emin misiniz?" + "Daveti reddet" + "%1$s ile bu özel sohbeti reddetmek istediğinizden emin misiniz?" + "Sohbeti reddet" + "Davet Yok" + "%1$s (%2$s) sizi davet etti" + "Bu tek seferlik bir işlemdir, beklediğiniz için teşekkürler." + "Hesabınızı ayarlanıyor." + "Yeni bir sohbet veya oda oluşturun" + "Birine mesaj göndererek başla." + "Henüz sohbet yok." + "Favoriler" + "Sohbet ayarlarından bir sohbeti favorilerinize ekleyebilirsiniz. +Şimdilik, diğer sohbetlerinizi görmek için filtrelerin seçimini kaldırabilirsiniz" + "Henüz favori sohbetleriniz yok" + "Davetiyeler" + "Bekleyen davetiniz yok." + "Düşük Öncelikli" + "Diğer sohbetlerinizi görmek için filtrelerin seçimini kaldırabilirsiniz" + "Bu seçim için sohbetiniz yok" + "Kişiler" + "Henüz hiç DM\'niz yok" + "Odalar" + "Henüz herhangi bir odada değilsiniz" + "Okunmamış" + "Tebrikler! +Okunmamış mesajınız yok!" + "Katılma isteği gönderildi" + "Sohbetler" + "Okundu olarak işaretle" + "Okunmamış olarak işaretle" + "Görünüşe göre yeni bir cihaz kullanıyorsunuz. Şifrelenmiş mesajlarınıza erişmek için başka bir cihazla doğrulayın." + "Siz olduğunuzu doğrulayın" + diff --git a/features/home/impl/src/main/res/values-uk/translations.xml b/features/home/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 00000000000..57d750d0293 --- /dev/null +++ b/features/home/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,49 @@ + + + "Вимкніть оптимізацію акумулятора для цього застосунку, щоб надходили всі сповіщення." + "Вимкнути оптимізацію" + "Не надходять сповіщення?" + "Відновіть свою криптографічну ідентичність та історію повідомлень за допомогою ключа відновлення, якщо ви втратили всі наявні пристрої." + "Налаштувати відновлення" + "Налаштуйте відновлення для захисту свого облікового запису" + "Підтвердіть свій ключ відновлення, щоб мати доступ до сховища ключів та історії повідомлень." + "Введіть ключ відновлення" + "Забули ключ відновлення?" + "Ваше сховище ключів не синхронізовано" + "Щоб ніколи не пропустити важливий виклик, змініть налаштування, щоб увімкнути повноекранні сповіщення, коли телефон заблоковано." + "Покращуйте досвід дзвінків" + "Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?" + "Відхилити запрошення" + "Ви дійсно хочете відмовитися від приватної бесіди з %1$s?" + "Відхилити бесіду" + "Немає запрошень" + "%1$s (%2$s) запрошує вас" + "Це одноразовий процес, дякую за очікування." + "Налаштування облікового запису." + "Створити нову розмову або кімнату" + "Почніть з обміну повідомленнями з кимось." + "Ще немає бесід." + "Обране" + "Ви можете додати бесіду до обраних у налаштуваннях бесіди. +Наразі ви можете зняти фільтри, щоб побачити інші ваші бесіди" + "Ви ще не маєте обраних бесід" + "Запрошення" + "У вас немає запрошень, що очікують на розгляд." + "Низький пріоритет" + "Ви можете зняти фільтри, щоб побачити інші ваші бесіди" + "Ви не маєте бесід для цієї категорії" + "Люди" + "Ви ще не маєте жодної особистої бесіди" + "Кімнати" + "Ви ще не учасник жодної кімнати" + "Непрочитані" + "Вітаємо! +У вас немає непрочитаних повідомлень!" + "Запит на приєднання надіслано" + "Бесіди" + "Позначити прочитаним" + "Позначити непрочитаним" + "Цю кімнату оновлено" + "Схоже, ви використовуєте новий пристрій. Щоб отримати доступ до зашифрованих повідомлень, підтвердьте особу за допомогою іншого пристрою." + "Підтвердьте, що це ви" + diff --git a/features/home/impl/src/main/res/values-ur/translations.xml b/features/home/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..7a0fd3acb6b --- /dev/null +++ b/features/home/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,40 @@ + + + "بازیابی مرتب کریں" + "اپنے کلید کے ذخیرہ اور پیغام کی سرگزشت تک رسائی کو برقرار رکھنے کیلئے اپنی بازیابی کلید کی تصدیق کریں۔" + "آپ کا کلید کا ذخیرہ غیر ہم وقت ساز ہے۔" + "اس بات کو یقینی بنانے کے لیے کہ آپ کبھی بھی اہم مکالمہ سے محروم نہ ہوں، براہ کرم اپنی ترتیبات تبدیل کریں تاکہ آپ کا ہاتف مقفل ہونے پر مکمل پردۂ نمائش اطلاعات کی اجازت دی جا سکے۔" + "اپنے مکالمتی تجربے کو احسن کریں" + "کیا آپکو یقین ہے کہ آپ %1$s میں شامل ہونے کی درخواست مسترد کرنا چاہتے ہیں؟" + "دعوت مسترد کریں" + "کیا آپکو یقین ہے کہ آپ %1$s کیساتھ نجی گفتگو مسترد کرنا چاہتے ہیں؟" + "گفتگو مسترد کریں" + "کوئی دعوت نامے نہیں" + "%1$s (%2$s) نے آپ کو مدعو کیا" + "یہ ایک بار کا عمل ہے، انتظار کرنے کا شکریہ۔" + "آپکا کھاتہ مرتب کر رہا ہے" + "ایک نئی گفتگو یا کمرہ تخلیق کریں" + "کسی کو پیغام بھیج کر شروع کریں۔" + "ابھی تک کوئی گفتگوئیں نہیں ہیں۔" + "پسندیدگان" + "آپ گفتگو کی ترتیبات میں اپنے پسندیدہ میں گفتگو شامل کر سکتے ہیں۔ + ابھی کے لیے، آپ اپنی دوسری گفتگوئیں دیکھنے کے لیے مرشحات کو غیر منتخب کر سکتے ہیں۔" + "آپ کے پاس ابھی تک پسندیدہ گفتگوئیں نہیں ہیں" + "دعوت نامے" + "آپ کے پاس کوئی زیر التوا دعوتیں نہیں ہیں۔" + "کم ترجیحی" + "آپ اپنی دیگر گفتگئہں دیکھنے کیلئے مرشحات کو غیر منتخب کرسکتے ہیں" + "آپ کے پاس اس انتخاب کے لیے گفتگو ئیں نہیں ہیں۔" + "لوگ" + "آپ کے پاس ابھی تک کوئی براہ راست پیغامات نہیں ہے۔" + "کمرے" + "آپ ابھی تک کسی کمرے میں نہیں ہیں" + "غیر مقروءہ" + "مبارک ہو! +آپ کے پاس کوئی غیر مقروءہ پیغامات نہیں!" + "گفتگوئیں" + "بطور مقروءہ نشانزد کریں" + "بطور غیر مقروءہ نشانزد کریں" + "ایسا لگتا ہے کہ آپ ایک نیا آلہ استعمال کر رہے ہیں۔ اپنے مرموزکردہ پیغامات تک رسائی کیلئے کسی دوسرے آلے سے توثیق کریں۔" + "تصدیق کریں کہ آپ ہی ہیں" + diff --git a/features/home/impl/src/main/res/values-uz/translations.xml b/features/home/impl/src/main/res/values-uz/translations.xml new file mode 100644 index 00000000000..077f9ecc4ae --- /dev/null +++ b/features/home/impl/src/main/res/values-uz/translations.xml @@ -0,0 +1,20 @@ + + + "Qayta tiklashni sozlang" + "Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?" + "Taklifni rad etish" + "Haqiqatan ham bu shaxsiy chatni rad qilmoqchimisiz%1$s ?" + "Chatni rad etish" + "Takliflar yo\'q" + "%1$s(%2$s ) sizni taklif qildi" + "Bu bir martalik jarayon, kutganingiz uchun rahmat." + "Hisobingiz sozlanmoqda." + "Yangi suhbat yoki xona yarating" + "Kimgadir xabar yuborishdan boshlang." + "Hozircha chatlar yo‘q." + "Takliflar" + "Odamlar" + "Suhbatlar" + "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-zh-rTW/translations.xml b/features/home/impl/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 00000000000..1bc2ae181c5 --- /dev/null +++ b/features/home/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,45 @@ + + + "若您遺失了所有現有裝置,則請使用復原金鑰以救援您的密碼學身份與訊息歷史紀錄。" + "設定復原" + "設定備援以保護您的帳號" + "確認您的復原金鑰以維持對金鑰儲存空間與訊息歷史紀錄的存取權。" + "輸入您的復原金鑰" + "忘記了您的復原金鑰?" + "您的金鑰儲存空間並未同步" + "為確保您永遠不會錯過重要通話,請變更設定以允許在手機鎖定時允許全螢幕通知。" + "提升您的通話體驗" + "您確定您想要拒絕加入 %1$s 的邀請嗎?" + "拒絕邀請" + "您確定您要拒絕此與 %1$s 的私人聊天嗎?" + "拒絕聊天" + "沒有邀請" + "%1$s(%2$s)邀請您" + "這是一次性的程序,感謝您耐心等候。" + "正在設定您的帳號。" + "建立新的對話或聊天室" + "從向某人傳送訊息開始。" + "尚無聊天室。" + "我的最愛" + "您可以在聊天設定中將聊天新增至收藏。 +目前,您可以取消選取篩選條件以檢視其他聊天" + "您尚無收藏聊天" + "邀請" + "您沒有任何擱置中的邀請。" + "低優先度" + "您可以取消選取篩選條件以檢視其他聊天" + "您並無此選擇的聊天" + "夥伴" + "您尚無任何私人訊息" + "聊天室" + "您尚未進入任何聊天室" + "未讀" + "恭喜! +您沒有任何未讀的訊息!" + "已傳送加入請求" + "所有聊天室" + "標為已讀" + "標為未讀" + "您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。" + "驗證這是您本人" + diff --git a/features/home/impl/src/main/res/values-zh/translations.xml b/features/home/impl/src/main/res/values-zh/translations.xml new file mode 100644 index 00000000000..4ca91cb7968 --- /dev/null +++ b/features/home/impl/src/main/res/values-zh/translations.xml @@ -0,0 +1,47 @@ + + + "禁用优化" + "通知未送达?" + "生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。" + "设置恢复" + "设置恢复" + "确认恢复密钥,以保持对密钥存储和消息历史的访问。" + "输入恢复密钥" + "忘记了恢复密钥?" + "你的密钥存储已不同步" + "为确保您不会错过重要来电,请更改设置以允许锁屏时的全屏通知。" + "提升通话体验" + "您确定要拒绝加入 %1$s 的邀请吗?" + "拒绝邀请" + "您确定要拒绝与 %1$s 开始私聊吗?" + "拒绝聊天" + "没有邀请" + "%1$s (%2$s)邀请了你" + "这是一个一次性的过程,感谢您的等待。" + "设置您的账户。" + "创建新的对话或聊天室" + "通过向某人发送消息来开始。" + "还没有聊天。" + "收藏夹" + "可以在聊天设置里将聊天添加到收藏夹中。 +现在,可以取消选择过滤器以查看其他对话。" + "您未收藏任何聊天" + "邀请" + "没有待处理的邀请。" + "低优先级" + "您可以取消选择过滤器以查看其他对话" + "您没有关于此选项的聊天" + "用户" + "目前您还没有私信" + "聊天室" + "您尚未进入任何聊天室" + "未读" + "恭喜! +没有任何未读消息!" + "加入请求已发送" + "全部聊天" + "标记为已读" + "标记为未读" + "您似乎正在使用新设备。使用另一台设备进行验证以访问您的加密消息。" + "验证是你本人" + diff --git a/features/home/impl/src/main/res/values/localazy.xml b/features/home/impl/src/main/res/values/localazy.xml new file mode 100644 index 00000000000..58afb065915 --- /dev/null +++ b/features/home/impl/src/main/res/values/localazy.xml @@ -0,0 +1,50 @@ + + + "Disable battery optimization for this app, to make sure all notifications are received." + "Disable optimization" + "Notifications not arriving?" + "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices." + "Set up recovery" + "Set up recovery to protect your account" + "Confirm your recovery key to maintain access to your key storage and message history." + "Enter your recovery key" + "Forgot your recovery key?" + "Your key storage is out of sync" + "To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked." + "Enhance your call experience" + "Are you sure you want to decline the invitation to join %1$s?" + "Decline invite" + "Are you sure you want to decline this private chat with %1$s?" + "Decline chat" + "No Invites" + "%1$s (%2$s) invited you" + "This is a one time process, thanks for waiting." + "Setting up your account." + "Create a new conversation or room" + "Clear filters" + "Get started by messaging someone." + "No chats yet." + "Favourites" + "You can add a chat to your favourites in the chat settings. +For now, you can deselect filters in order to see your other chats" + "You don’t have favourite chats yet" + "Invites" + "You don\'t have any pending invites." + "Low Priority" + "You can deselect filters in order to see your other chats" + "You don’t have chats for this selection" + "People" + "You don’t have any DMs yet" + "Rooms" + "You’re not in any room yet" + "Unreads" + "Congrats! +You don’t have any unread messages!" + "Request to join sent" + "Chats" + "Mark as read" + "Mark as unread" + "This room has been upgraded" + "Looks like you’re using a new device. Verify with another device to access your encrypted messages." + "Verify it’s you" + diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/FakeDateTimeObserver.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/FakeDateTimeObserver.kt new file mode 100644 index 00000000000..6cbebb3135e --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/FakeDateTimeObserver.kt @@ -0,0 +1,19 @@ +/* + * 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.home.impl + +import io.element.android.libraries.androidutils.system.DateTimeObserver +import kotlinx.coroutines.flow.MutableSharedFlow + +class FakeDateTimeObserver : DateTimeObserver { + override val changes = MutableSharedFlow(extraBufferCapacity = 1) + + fun given(event: DateTimeObserver.Event) { + changes.tryEmit(event) + } +} diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt new file mode 100644 index 00000000000..6913ea19bf6 --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -0,0 +1,148 @@ +/* + * 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.home.impl + +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.home.impl.roomlist.aRoomListState +import io.element.android.features.logout.api.direct.aDirectLogoutState +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +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.indicator.api.IndicatorService +import io.element.android.libraries.indicator.test.FakeIndicatorService +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.sync.SyncService +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.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.sync.FakeSyncService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class HomePresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - should start with no user and then load user with success`() = runTest { + val matrixClient = FakeMatrixClient( + userDisplayName = null, + userAvatarUrl = null, + ) + matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL))) + val presenter = createHomePresenter( + client = matrixClient, + rageshakeFeatureAvailability = { false }, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID)) + assertThat(initialState.canReportBug).isFalse() + val withUserState = awaitItem() + assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID) + assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME) + assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL) + assertThat(withUserState.showAvatarIndicator).isFalse() + assertThat(withUserState.isSpaceFeatureEnabled).isFalse() + } + } + + @Test + fun `present - space feature enabled`() = runTest { + val presenter = createHomePresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Space.key to true), + ), + ) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isSpaceFeatureEnabled).isTrue() + } + } + + @Test + fun `present - show avatar indicator`() = runTest { + val indicatorService = FakeIndicatorService() + val presenter = createHomePresenter( + indicatorService = indicatorService, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.showAvatarIndicator).isFalse() + indicatorService.setShowRoomListTopBarIndicator(true) + val finalState = awaitItem() + assertThat(finalState.showAvatarIndicator).isTrue() + } + } + + @Test + fun `present - should start with no user and then load user with error`() = runTest { + val matrixClient = FakeMatrixClient( + userDisplayName = null, + userAvatarUrl = null, + ) + matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION)) + val presenter = createHomePresenter(client = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId)) + // No new state is coming + } + } + + @Test + fun `present - NavigationBar change`() = runTest { + val presenter = createHomePresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats) + initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces)) + val finalState = awaitItem() + assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) + } + } + + private fun TestScope.createHomePresenter( + client: MatrixClient = FakeMatrixClient(), + syncService: SyncService = FakeSyncService(), + snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), + rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, + indicatorService: IndicatorService = FakeIndicatorService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService() + ) = HomePresenter( + client = client, + syncService = syncService, + snackbarDispatcher = snackbarDispatcher, + indicatorService = indicatorService, + logoutPresenter = { aDirectLogoutState() }, + roomListPresenter = { aRoomListState() }, + rageshakeFeatureAvailability = rageshakeFeatureAvailability, + featureFlagService = featureFlagService, + ) +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt similarity index 96% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt rename to features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt index dcf6cdc0325..03fe2efd942 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt @@ -5,11 +5,11 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.datasource +package io.element.android.features.home.impl.datasource import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.roomlist.impl.FakeDateTimeObserver +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 @@ -100,7 +100,7 @@ class RoomListDataSourceTest { roomListRoomSummaryFactory = roomListRoomSummaryFactory, coroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService = notificationSettingsService, - appScope = backgroundScope, + sessionCoroutineScope = backgroundScope, dateTimeObserver = dateTimeObserver, ) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactoryTest.kt similarity index 92% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt rename to features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactoryTest.kt index 86c0981d4f9..95444b6c4f3 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactoryTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.datasource +package io.element.android.features.home.impl.datasource import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt similarity index 97% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt rename to features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt index 67ef79ac817..3f4f45afaeb 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt @@ -5,10 +5,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters import com.google.common.truth.Truth.assertThat -import io.element.android.features.roomlist.impl.R +import io.element.android.features.home.impl.R import org.junit.Test class RoomListFiltersEmptyStateResourcesTest { diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt similarity index 95% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTest.kt rename to features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt index 560e5f938ee..a71afca607f 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt @@ -5,14 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters 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.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy -import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState +import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy +import io.element.android.features.home.impl.filters.selection.FilterSelectionState import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.tests.testutils.awaitLastSequentialItem diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt similarity index 90% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersViewTest.kt rename to features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt index 0041ef988d9..1fd37d92528 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt @@ -5,13 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.filters +package io.element.android.features.home.impl.filters import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.roomlist.impl.R -import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState +import io.element.android.features.home.impl.R +import io.element.android.features.home.impl.filters.selection.FilterSelectionState import io.element.android.libraries.testtags.TestTags import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt new file mode 100644 index 00000000000..55b3f1ffee3 --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.home.impl.model + +import com.google.common.truth.Truth.assertThat +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.RoomNotificationMode +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import kotlinx.collections.immutable.toPersistentList +import org.junit.Test + +class RoomListBaseRoomSummaryTest { + @Test + fun `test default value`() { + val sut = createRoomListRoomSummary( + isMarkedUnread = false, + ) + assertThat(sut.isHighlighted).isFalse() + assertThat(sut.hasNewContent).isFalse() + } + + @Test + fun `test muted room`() { + val sut = createRoomListRoomSummary( + userDefinedNotificationMode = RoomNotificationMode.MUTE, + ) + assertThat(sut.isHighlighted).isFalse() + assertThat(sut.hasNewContent).isFalse() + } + + @Test + fun `test muted room isMarkedUnread set to true`() { + val sut = createRoomListRoomSummary( + isMarkedUnread = true, + userDefinedNotificationMode = RoomNotificationMode.MUTE, + ) + assertThat(sut.isHighlighted).isTrue() + assertThat(sut.hasNewContent).isTrue() + } + + @Test + fun `test muted room with unread message`() { + val sut = createRoomListRoomSummary( + numberOfUnreadNotifications = 1, + userDefinedNotificationMode = RoomNotificationMode.MUTE, + ) + assertThat(sut.isHighlighted).isFalse() + assertThat(sut.hasNewContent).isTrue() + } + + @Test + fun `test isMarkedUnread set to true`() { + val sut = createRoomListRoomSummary( + isMarkedUnread = true, + ) + assertThat(sut.isHighlighted).isTrue() + assertThat(sut.hasNewContent).isTrue() + } + + @Test + fun `when display type is invite then isHighlighted and hasNewContent are false`() { + val sut = createRoomListRoomSummary( + displayType = RoomSummaryDisplayType.INVITE, + ) + assertThat(sut.isHighlighted).isFalse() + assertThat(sut.hasNewContent).isFalse() + } +} + +internal fun createRoomListRoomSummary( + numberOfUnreadMentions: Long = 0, + numberOfUnreadMessages: Long = 0, + numberOfUnreadNotifications: Long = 0, + isMarkedUnread: Boolean = false, + userDefinedNotificationMode: RoomNotificationMode? = null, + isFavorite: Boolean = false, + displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM, + heroes: List = emptyList(), + timestamp: String? = null, + isTombstoned: Boolean = false, +) = RoomListRoomSummary( + id = A_ROOM_ID.value, + roomId = A_ROOM_ID, + name = A_ROOM_NAME, + numberOfUnreadMentions = numberOfUnreadMentions, + numberOfUnreadMessages = numberOfUnreadMessages, + numberOfUnreadNotifications = numberOfUnreadNotifications, + isMarkedUnread = isMarkedUnread, + timestamp = timestamp, + lastMessage = "", + avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem), + displayType = displayType, + userDefinedNotificationMode = userDefinedNotificationMode, + hasRoomCall = false, + isDirect = false, + isFavorite = isFavorite, + canonicalAlias = null, + inviteSender = null, + isDm = false, + heroes = heroes.toPersistentList(), + isTombstoned = isTombstoned, +) 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 new file mode 100644 index 00000000000..b336178d22d --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.home.impl.roomlist + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.home.impl.R +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureCalledOnceWithParam +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.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`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown(hasNewContent = true) + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + ) + rule.clickOn(R.string.screen_roomlist_mark_as_read) + eventsRecorder.assertList( + listOf( + RoomListEvents.HideContextMenu, + RoomListEvents.MarkAsRead(contextMenu.roomId), + ) + ) + } + + @Test + fun `clicking on Mark as unread generates expected Events`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown(hasNewContent = false) + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + ) + rule.clickOn(R.string.screen_roomlist_mark_as_unread) + eventsRecorder.assertList( + listOf( + RoomListEvents.HideContextMenu, + RoomListEvents.MarkAsUnread(contextMenu.roomId), + ) + ) + } + + @Test + fun `clicking on Leave room generates expected Events`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown(isDm = false) + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + ) + rule.clickOn(CommonStrings.action_leave_room) + eventsRecorder.assertList( + listOf( + RoomListEvents.HideContextMenu, + RoomListEvents.LeaveRoom(contextMenu.roomId), + ) + ) + } + + @Test + 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) + rule.setRoomListContextMenu( + contextMenu = contextMenu, + canReportRoom = true, + eventSink = eventsRecorder, + onRoomSettingsClick = EnsureNeverCalledWithParam(), + onReportRoomClick = callback, + ) + rule.clickOn(CommonStrings.action_report_room) + eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) + callback.assertSuccess() + } + + @Test + fun `clicking on Settings invokes the expected callback and generates expected Event`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown() + val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClick = callback, + ) + rule.clickOn(CommonStrings.common_settings) + eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) + callback.assertSuccess() + } + + @Test + fun `clicking on Favourites generates expected Event`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown(isDm = false, isFavorite = false) + val callback = EnsureNeverCalledWithParam() + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClick = callback, + ) + rule.clickOn(CommonStrings.common_favourite) + eventsRecorder.assertList( + listOf( + RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, true), + ) + ) + } + + private fun AndroidComposeTestRule<*, *>.setRoomListContextMenu( + contextMenu: RoomListState.ContextMenu.Shown, + canReportRoom: Boolean = false, + eventSink: (RoomListEvents) -> Unit, + onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + ) { + setSafeContent { + RoomListContextMenu( + contextMenu = contextMenu, + canReportRoom = canReportRoom, + onRoomSettingsClick = onRoomSettingsClick, + onReportRoomClick = onReportRoomClick, + eventSink = eventSink, + ) + } + } +} 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 new file mode 100644 index 00000000000..48a51b87bb0 --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.home.impl.roomlist + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.home.impl.model.aRoomListRoomSummary +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureCalledOnceWithParam +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.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`() { + val eventsRecorder = EventsRecorder() + val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) + rule.setSafeContent { + RoomListDeclineInviteMenu( + menu = menu, + canReportRoom = false, + onDeclineAndBlockClick = EnsureNeverCalledWithParam(), + eventSink = eventsRecorder, + ) + } + rule.clickOn(CommonStrings.action_decline) + eventsRecorder.assertList( + listOf( + RoomListEvents.HideDeclineInviteMenu, + RoomListEvents.DeclineInvite(menu.roomSummary, blockUser = false), + ) + ) + } + + @Test + 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()) + rule.setSafeContent { + RoomListDeclineInviteMenu( + menu = menu, + canReportRoom = true, + onDeclineAndBlockClick = EnsureCalledOnceWithParam(menu.roomSummary, Unit), + eventSink = eventsRecorder, + ) + } + rule.clickOn(CommonStrings.action_decline_and_block) + val expectedEvents = listOf(RoomListEvents.HideDeclineInviteMenu) + eventsRecorder.assertList(expectedEvents) + } + + @Test + fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() { + val eventsRecorder = EventsRecorder() + val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) + rule.setSafeContent { + RoomListDeclineInviteMenu( + menu = menu, + canReportRoom = false, + onDeclineAndBlockClick = EnsureNeverCalledWithParam(), + eventSink = eventsRecorder, + ) + } + rule.clickOn(CommonStrings.action_decline_and_block) + val expectedEvents = listOf( + RoomListEvents.HideDeclineInviteMenu, + RoomListEvents.DeclineInvite(menu.roomSummary, blockUser = true), + ) + eventsRecorder.assertList(expectedEvents) + } + + @Test + fun `clicking on cancel emits the expected Event`() { + val eventsRecorder = EventsRecorder() + val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) + rule.setSafeContent { + RoomListDeclineInviteMenu( + menu = menu, + canReportRoom = false, + onDeclineAndBlockClick = EnsureNeverCalledWithParam(), + eventSink = eventsRecorder, + ) + } + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertList(listOf(RoomListEvents.HideDeclineInviteMenu)) + } +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt similarity index 82% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt rename to features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index a49e020d8bf..bc0163585f1 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -5,70 +5,65 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl +package io.element.android.features.home.impl.roomlist import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Interaction -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.anAcceptDeclineInviteState +import io.element.android.features.home.impl.FakeDateTimeObserver +import io.element.android.features.home.impl.datasource.RoomListDataSource +import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory +import io.element.android.features.home.impl.filters.RoomListFiltersState +import io.element.android.features.home.impl.filters.aRoomListFiltersState +import io.element.android.features.home.impl.model.createRoomListRoomSummary +import io.element.android.features.home.impl.search.RoomListSearchEvents +import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.home.impl.search.aRoomListSearchState +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState -import io.element.android.features.logout.api.direct.aDirectLogoutState -import io.element.android.features.roomlist.impl.datasource.RoomListDataSource -import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory -import io.element.android.features.roomlist.impl.filters.RoomListFiltersState -import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState -import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary -import io.element.android.features.roomlist.impl.search.RoomListSearchEvents -import io.element.android.features.roomlist.impl.search.RoomListSearchState -import io.element.android.features.roomlist.impl.search.aRoomListSearchState -import io.element.android.libraries.androidutils.system.DateTimeObserver import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter -import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState -import io.element.android.libraries.indicator.impl.DefaultIndicatorService 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.SessionId -import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomList -import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.timeline.ReceiptType -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.AN_EXCEPTION 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.A_SESSION_ID -import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore +import io.element.android.libraries.push.api.battery.aBatteryOptimizationState import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner import io.element.android.services.analytics.api.AnalyticsService @@ -82,7 +77,6 @@ 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.flow.MutableSharedFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest @@ -94,79 +88,17 @@ class RoomListPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - @Test - fun `present - should start with no user and then load user with success`() = runTest { - val matrixClient = FakeMatrixClient( - userDisplayName = null, - userAvatarUrl = null, - ) - matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL))) - val presenter = createRoomListPresenter( - client = matrixClient, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID)) - val withUserState = awaitItem() - assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID) - assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME) - assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL) - assertThat(withUserState.showAvatarIndicator).isTrue() - } - } - - @Test - fun `present - show avatar indicator`() = runTest { - val encryptionService = FakeEncryptionService() - val sessionVerificationService = FakeSessionVerificationService() - val matrixClient = FakeMatrixClient( - encryptionService = encryptionService, - sessionVerificationService = sessionVerificationService, - ) - val presenter = createRoomListPresenter( - client = matrixClient, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.showAvatarIndicator).isTrue() - sessionVerificationService.emitNeedsSessionVerification(false) - encryptionService.emitBackupState(BackupState.ENABLED) - val finalState = awaitItem() - assertThat(finalState.showAvatarIndicator).isFalse() - } - } - - @Test - fun `present - should start with no user and then load user with error`() = runTest { - val matrixClient = FakeMatrixClient( - userDisplayName = null, - userAvatarUrl = null, - ) - matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION)) - val presenter = createRoomListPresenter(client = matrixClient) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId)) - // No new state is coming - } - } - @Test fun `present - load 1 room with success`() = runTest { val roomListService = FakeRoomListService() val matrixClient = FakeMatrixClient( roomListService = roomListService ) - val presenter = createRoomListPresenter(client = matrixClient) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createRoomListPresenter( + client = matrixClient, + seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)), + ) + presenter.test { val initialState = consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Skeleton }.last() assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) @@ -188,6 +120,7 @@ class RoomListPresenterTest { timestamp = "0 TimeOrDate true", ) ) + assertThat(withRoomsState.contentAsRooms().seenRoomInvites).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3) cancelAndIgnoreRemainingEvents() } } @@ -265,7 +198,7 @@ class RoomListPresenterTest { @Test fun `present - show context menu`() = runTest { - val room = FakeMatrixRoom() + val room = FakeBaseRoom() val client = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } @@ -286,8 +219,8 @@ class RoomListPresenterTest { isDm = false, isFavorite = false, markAsUnreadFeatureFlagEnabled = true, - eventCacheFeatureFlagEnabled = false, hasNewContent = false, + displayClearRoomCacheAction = false, ) ) } @@ -304,8 +237,37 @@ class RoomListPresenterTest { isDm = false, isFavorite = true, markAsUnreadFeatureFlagEnabled = true, - eventCacheFeatureFlagEnabled = false, hasNewContent = false, + displayClearRoomCacheAction = false, + ) + ) + } + } + } + + @Test + fun `present - show context menu with view source on`() = runTest { + val presenter = createRoomListPresenter( + appPreferencesStore = InMemoryAppPreferencesStore( + isDeveloperModeEnabled = true, + ) + ) + presenter.test { + val initialState = awaitItem() + val summary = createRoomListRoomSummary() + initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) + awaitItem().also { state -> + assertThat(state.contextMenu) + .isEqualTo( + RoomListState.ContextMenu.Shown( + roomId = summary.roomId, + roomName = summary.name, + isDm = false, + isFavorite = false, + markAsUnreadFeatureFlagEnabled = true, + // true here. + hasNewContent = false, + displayClearRoomCacheAction = true, ) ) } @@ -314,7 +276,7 @@ class RoomListPresenterTest { @Test fun `present - hide context menu`() = runTest { - val room = FakeMatrixRoom() + val room = FakeBaseRoom() val client = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } @@ -335,8 +297,8 @@ class RoomListPresenterTest { isDm = false, isFavorite = false, markAsUnreadFeatureFlagEnabled = true, - eventCacheFeatureFlagEnabled = false, hasNewContent = false, + displayClearRoomCacheAction = false, ) ) @@ -424,7 +386,7 @@ class RoomListPresenterTest { @Test fun `present - when set is favorite event is emitted, then the action is called`() = runTest { val setIsFavoriteResult = lambdaRecorder { _: Boolean -> Result.success(Unit) } - val room = FakeMatrixRoom( + val room = FakeBaseRoom( setIsFavoriteResult = setIsFavoriteResult ) val analyticsService = FakeAnalyticsService() @@ -471,9 +433,18 @@ class RoomListPresenterTest { @Test fun `present - check that the room is marked as read with correct RR and as unread`() = runTest { - val room = FakeMatrixRoom() - val room2 = FakeMatrixRoom(roomId = A_ROOM_ID_2) - val room3 = FakeMatrixRoom(roomId = A_ROOM_ID_3) + val markAsReadResult = lambdaRecorder> { Result.success(Unit) } + val markAsReadResult3 = lambdaRecorder> { Result.success(Unit) } + val room = FakeBaseRoom( + markAsReadResult = markAsReadResult, + ) + val room2 = FakeBaseRoom( + roomId = A_ROOM_ID_2, + ) + val room3 = FakeBaseRoom( + roomId = A_ROOM_ID_3, + markAsReadResult = markAsReadResult3, + ) val allRooms = setOf(room, room2, room3) val sessionPreferencesStore = InMemorySessionPreferencesStore() val matrixClient = FakeMatrixClient().apply { @@ -497,21 +468,19 @@ class RoomListPresenterTest { }.test { val initialState = awaitItem() allRooms.forEach { - assertThat(it.markAsReadCalls).isEmpty() assertThat(it.setUnreadFlagCalls).isEmpty() } initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID)) - assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ)) + markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ)) assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false)) clearMessagesForRoomLambda.assertions().isCalledOnce() .with(value(A_SESSION_ID), value(A_ROOM_ID)) initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID_2)) - assertThat(room2.markAsReadCalls).isEmpty() assertThat(room2.setUnreadFlagCalls).isEqualTo(listOf(true)) // Test again with private read receipts sessionPreferencesStore.setSendPublicReadReceipts(false) initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID_3)) - assertThat(room3.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ_PRIVATE)) + markAsReadResult3.assertions().isCalledOnce().with(value(ReceiptType.READ_PRIVATE)) assertThat(room3.setUnreadFlagCalls).isEqualTo(listOf(false)) clearMessagesForRoomLambda.assertions().isCalledExactly(2) .withSequence( @@ -538,7 +507,8 @@ class RoomListPresenterTest { roomListService = roomListService, ) val roomSummary = aRoomSummary( - currentUserMembership = CurrentUserMembership.INVITED + currentUserMembership = CurrentUserMembership.INVITED, + inviter = aRoomMember(), ) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRooms(listOf(roomSummary)) @@ -554,16 +524,16 @@ class RoomListPresenterTest { val roomListRoomSummary = state.contentAsRooms().summaries.first { it.id == roomSummary.roomId.value } + state.eventSink(RoomListEvents.AcceptInvite(roomListRoomSummary)) - state.eventSink(RoomListEvents.DeclineInvite(roomListRoomSummary)) + state.eventSink(RoomListEvents.DeclineInvite(roomListRoomSummary, blockUser = false)) val inviteData = roomListRoomSummary.toInviteData() - assert(eventSinkRecorder) .isCalledExactly(2) .withSequence( listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))), - listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))), + listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false))), ) } } @@ -631,8 +601,6 @@ class RoomListPresenterTest { private fun TestScope.createRoomListPresenter( client: MatrixClient = FakeMatrixClient(), - syncService: SyncService = FakeSyncService(), - snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), leaveRoomState: LeaveRoomState = aLeaveRoomState(), dateFormatter: DateFormatter = FakeDateFormatter(), roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), @@ -643,10 +611,10 @@ class RoomListPresenterTest { searchPresenter: Presenter = Presenter { aRoomListSearchState() }, acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), ) = RoomListPresenter( client = client, - syncService = syncService, - snackbarDispatcher = snackbarDispatcher, leaveRoomPresenter = { leaveRoomState }, roomListDataSource = RoomListDataSource( roomListService = client.roomListService, @@ -656,29 +624,19 @@ class RoomListPresenterTest { ), coroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService = client.notificationSettingsService(), - appScope = backgroundScope, + sessionCoroutineScope = backgroundScope, dateTimeObserver = FakeDateTimeObserver(), ), featureFlagService = featureFlagService, - indicatorService = DefaultIndicatorService( - sessionVerificationService = client.sessionVerificationService(), - encryptionService = client.encryptionService(), - ), searchPresenter = searchPresenter, sessionPreferencesStore = sessionPreferencesStore, filtersPresenter = filtersPresenter, analyticsService = analyticsService, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() }, + batteryOptimizationPresenter = { aBatteryOptimizationState() }, notificationCleaner = notificationCleaner, - logoutPresenter = { aDirectLogoutState() }, + appPreferencesStore = appPreferencesStore, + seenInvitesStore = seenInvitesStore, ) } - -class FakeDateTimeObserver : DateTimeObserver { - override val changes = MutableSharedFlow(extraBufferCapacity = 1) - - fun given(event: DateTimeObserver.Event) { - changes.tryEmit(event) - } -} diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt new file mode 100644 index 00000000000..3837fe6f55e --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -0,0 +1,10 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.home.impl.roomlist + +internal fun RoomListState.contentAsRooms() = contentState as RoomListContentState.Rooms diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt similarity index 82% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt rename to features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt index ee7989cafca..0465926abc5 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl +package io.element.android.features.home.impl.roomlist import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule @@ -16,8 +16,12 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.roomlist.impl.components.RoomListMenuAction -import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType +import io.element.android.features.home.impl.HomeView +import io.element.android.features.home.impl.R +import io.element.android.features.home.impl.aHomeState +import io.element.android.features.home.impl.components.RoomListMenuAction +import io.element.android.features.home.impl.model.RoomListRoomSummary +import io.element.android.features.home.impl.model.RoomSummaryDisplayType import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -26,15 +30,19 @@ 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.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() + @get:Rule + val rule = createAndroidComposeRule() + @Config(qualifiers = "h1024dp") @Test fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() { val eventsRecorder = EventsRecorder() @@ -48,7 +56,7 @@ class RoomListViewTest { eventsRecorder.assertList( listOf( RoomListEvents.UpdateVisibleRange(IntRange.EMPTY), - RoomListEvents.UpdateVisibleRange(0 until 2), + RoomListEvents.UpdateVisibleRange(0..5), ) ) } @@ -167,6 +175,29 @@ class RoomListViewTest { eventsRecorder.assertEmpty() } + @Test + fun `clicking on a room twice invokes the expected callback only once`() { + val eventsRecorder = EventsRecorder() + val state = aRoomListState( + eventSink = eventsRecorder, + ) + val room0 = state.contentAsRooms().summaries.first { + it.displayType == RoomSummaryDisplayType.ROOM + } + ensureCalledOnceWithParam(room0.roomId) { callback -> + rule.setRoomListView( + state = state, + onRoomClick = callback, + ) + // Remove automatic initial events + eventsRecorder.clear() + rule.onNodeWithText(room0.lastMessage!!.toString()) + .performClick() + .performClick() + } + eventsRecorder.assertEmpty() + } + @Test fun `long clicking on a room emits the expected Event`() { val eventsRecorder = EventsRecorder() @@ -226,23 +257,11 @@ class RoomListViewTest { rule.clickOn(CommonStrings.action_accept) rule.clickOn(CommonStrings.action_decline) eventsRecorder.assertList( - listOf(RoomListEvents.AcceptInvite(invitedRoom), RoomListEvents.DeclineInvite(invitedRoom)), - ) - } - - @Test - fun `clicking on logout and migrate calls the migration clicked callback`() { - val state = aRoomListState( - contentState = aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration), - eventSink = {}, - ) - ensureCalledOnce { callback -> - rule.setRoomListView( - state = state, - onMigrateToNativeSlidingSyncClick = callback, + listOf( + RoomListEvents.AcceptInvite(invitedRoom), + RoomListEvents.ShowDeclineInviteMenu(invitedRoom), ) - rule.clickOn(R.string.banner_migrate_to_native_sliding_sync_action) - } + ) } } @@ -255,12 +274,12 @@ private fun AndroidComposeTestRule.setRoomL onCreateRoomClick: () -> Unit = EnsureNeverCalled(), onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), - onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(), - onMigrateToNativeSlidingSyncClick: () -> Unit = EnsureNeverCalled() + onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onDeclineInviteAndBlockUser: (RoomListRoomSummary) -> Unit = EnsureNeverCalledWithParam(), ) { - setContent { - RoomListView( - state = state, + setSafeContent { + HomeView( + homeState = aHomeState(roomListState = state), onRoomClick = onRoomClick, onSettingsClick = onSettingsClick, onSetUpRecoveryClick = onSetUpRecoveryClick, @@ -268,8 +287,8 @@ private fun AndroidComposeTestRule.setRoomL onCreateRoomClick = onCreateRoomClick, onRoomSettingsClick = onRoomSettingsClick, onMenuActionClick = onMenuActionClick, - onRoomDirectorySearchClick = onRoomDirectorySearchClick, - onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, + onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser, + onReportRoomClick = onReportRoomClick, acceptDeclineInviteView = { }, ) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt similarity index 82% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt rename to features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt index 82fef235081..7649b6141ee 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt @@ -5,18 +5,15 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomlist.impl.search +package io.element.android.features.home.impl.search 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.roomlist.impl.datasource.aRoomListRoomSummaryFactory +import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter -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.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.test.room.aRoomSummary @@ -118,26 +115,10 @@ class RoomListSearchPresenterTest { } } } - - @Test - fun `present - room directory search`() = runTest { - val featureFlagService = FakeFeatureFlagService() - featureFlagService.setFeatureEnabled(FeatureFlags.RoomDirectorySearch, true) - val presenter = createRoomListSearchPresenter(featureFlagService = featureFlagService) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().let { state -> - assertThat(state.isRoomDirectorySearchEnabled).isTrue() - } - } - } } fun TestScope.createRoomListSearchPresenter( roomListService: RoomListService = FakeRoomListService(), - featureFlagService: FeatureFlagService = FakeFeatureFlagService(), ): RoomListSearchPresenter { return RoomListSearchPresenter( dataSource = RoomListSearchDataSource( @@ -148,6 +129,5 @@ fun TestScope.createRoomListSearchPresenter( ), coroutineDispatchers = testCoroutineDispatchers(), ), - featureFlagService = featureFlagService, ) } diff --git a/features/invite/api/build.gradle.kts b/features/invite/api/build.gradle.kts index c4039e2382b..d17b392e594 100644 --- a/features/invite/api/build.gradle.kts +++ b/features/invite/api/build.gradle.kts @@ -7,6 +7,7 @@ plugins { id("io.element.android-compose-library") + id("kotlin-parcelize") } android { @@ -16,5 +17,7 @@ android { dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) implementation(projects.services.analytics.api) } 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 new file mode 100644 index 00000000000..eb1dfd3df67 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt @@ -0,0 +1,38 @@ +/* + * 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.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 kotlinx.parcelize.Parcelize + +@Parcelize +data class InviteData( + val roomId: RoomId, + val roomName: String, + val isDm: Boolean, +) : Parcelable + +fun RoomPreviewInfo.toInviteData(): InviteData { + return InviteData( + roomId = roomId, + roomName = name ?: roomId.value, + isDm = false, + ) +} + +fun RoomInfo.toInviteData(): InviteData { + return InviteData( + roomId = id, + roomName = name ?: id.value, + isDm = isDm, + ) +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt new file mode 100644 index 00000000000..682970ffe7d --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt @@ -0,0 +1,37 @@ +/* + * 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.invite.api + +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.flow.Flow + +interface SeenInvitesStore { + /** + * Returns a flow of seen room IDs of invitation. + */ + fun seenRoomIds(): Flow> + + /** + * Mark the invitation as seen. + * Call this when the invitation details are shown to the user. + * @param roomId the room ID of the invitation to mark as seen. + */ + suspend fun markAsSeen(roomId: RoomId) + + /** + * Mark the invitation as unseen. + * Call this when the invitation has been accepted or declined. + * @param roomId the room ID of the invitation to mark as unseen. + */ + suspend fun markAsUnSeen(roomId: RoomId) + + /** + * Delete the store. + */ + suspend fun clear() +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt new file mode 100644 index 00000000000..015fc161822 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt @@ -0,0 +1,15 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.invite.api.acceptdecline + +import io.element.android.features.invite.api.InviteData + +interface AcceptDeclineInviteEvents { + data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents + data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteState.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt similarity index 88% rename from features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteState.kt rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt index ad883119cb7..f4645383df6 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteState.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.invite.api.response +package io.element.android.features.invite.api.acceptdecline import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt new file mode 100644 index 00000000000..78be5ebd165 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt @@ -0,0 +1,22 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.invite.api.acceptdecline + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.matrix.api.core.RoomId + +interface AcceptDeclineInviteView { + @Composable + fun Render( + state: AcceptDeclineInviteState, + onAcceptInviteSuccess: (RoomId) -> Unit, + onDeclineInviteSuccess: (RoomId) -> Unit, + modifier: Modifier, + ) +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt new file mode 100644 index 00000000000..f0c43663512 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/ConfirmingDeclineInvite.kt @@ -0,0 +1,13 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.invite.api.acceptdecline + +import io.element.android.features.invite.api.InviteData +import io.element.android.libraries.architecture.AsyncAction + +data class ConfirmingDeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : AsyncAction.Confirming diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt new file mode 100644 index 00000000000..27080ee354c --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt @@ -0,0 +1,17 @@ +/* + * 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.invite.api.declineandblock + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.features.invite.api.InviteData +import io.element.android.libraries.architecture.FeatureEntryPoint + +interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint { + fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node +} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt deleted file mode 100644 index 6aad688e77a..00000000000 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.api.response - -interface AcceptDeclineInviteEvents { - data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents - data class DeclineInvite(val invite: InviteData) : AcceptDeclineInviteEvents -} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt deleted file mode 100644 index 810a97bac58..00000000000 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.api.response - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.core.RoomId - -open class AcceptDeclineInviteStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - anAcceptDeclineInviteState(), - anAcceptDeclineInviteState( - declineAction = ConfirmingDeclineInvite( - InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice") - ), - ), - anAcceptDeclineInviteState( - declineAction = ConfirmingDeclineInvite( - InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room") - ), - ), - anAcceptDeclineInviteState( - acceptAction = AsyncAction.Failure(Throwable("Whoops")), - ), - anAcceptDeclineInviteState( - declineAction = AsyncAction.Failure(Throwable("Whoops")), - ), - ) -} - -fun anAcceptDeclineInviteState( - acceptAction: AsyncAction = AsyncAction.Uninitialized, - declineAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (AcceptDeclineInviteEvents) -> Unit = {} -) = AcceptDeclineInviteState( - acceptAction = acceptAction, - declineAction = declineAction, - eventSink = eventSink, -) diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteView.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteView.kt deleted file mode 100644 index cdcd5b15ecd..00000000000 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteView.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.api.response - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import io.element.android.libraries.matrix.api.core.RoomId - -interface AcceptDeclineInviteView { - @Composable - fun Render( - state: AcceptDeclineInviteState, - onAcceptInvite: (RoomId) -> Unit, - onDeclineInvite: (RoomId) -> Unit, - modifier: Modifier, - ) -} diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/ConfirmingDeclineInvite.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/ConfirmingDeclineInvite.kt deleted file mode 100644 index bf8fa4f0689..00000000000 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/ConfirmingDeclineInvite.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.api.response - -import io.element.android.libraries.architecture.AsyncAction - -data class ConfirmingDeclineInvite( - val inviteData: InviteData, -) : AsyncAction.Confirming diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt deleted file mode 100644 index 20d3ba298f6..00000000000 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/InviteData.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.api.response - -import io.element.android.libraries.matrix.api.core.RoomId - -data class InviteData( - val roomId: RoomId, - val roomName: String, - val isDm: Boolean, -) diff --git a/features/invite/impl/build.gradle.kts b/features/invite/impl/build.gradle.kts index 8c00ac3d23c..d6337f65c9f 100644 --- a/features/invite/impl/build.gradle.kts +++ b/features/invite/impl/build.gradle.kts @@ -14,6 +14,11 @@ plugins { android { namespace = "io.element.android.features.invite.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } setupAnvil() @@ -21,6 +26,7 @@ setupAnvil() dependencies { api(projects.features.invite.api) implementation(libs.androidx.datastore.preferences) + implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) @@ -35,8 +41,12 @@ dependencies { testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) + testImplementation(projects.features.invite.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) testImplementation(projects.services.analytics.test) testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt new file mode 100644 index 00000000000..716cd7f9070 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/AcceptInvite.kt @@ -0,0 +1,58 @@ +/* + * 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.invite.impl + +import com.squareup.anvil.annotations.ContributesBinding +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.core.extensions.mapFailure +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.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.exception.ClientException +import io.element.android.libraries.matrix.api.exception.ErrorKind +import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.libraries.push.api.notifications.NotificationCleaner +import javax.inject.Inject + +interface AcceptInvite { + suspend operator fun invoke(roomId: RoomId): Result + + sealed class Failures : Exception() { + data object InvalidInvite : Failures() + } +} + +@ContributesBinding(SessionScope::class) +class DefaultAcceptInvite @Inject constructor( + private val client: MatrixClient, + private val joinRoom: JoinRoom, + private val notificationCleaner: NotificationCleaner, + private val seenInvitesStore: SeenInvitesStore, +) : AcceptInvite { + override suspend fun invoke(roomId: RoomId): Result { + return joinRoom( + roomIdOrAlias = roomId.toRoomIdOrAlias(), + serverNames = emptyList(), + trigger = JoinedRoom.Trigger.Invite, + ).onSuccess { + notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId) + seenInvitesStore.markAsUnSeen(roomId) + }.mapFailure { + if (it is ClientException.MatrixApi) { + when (it.kind) { + ErrorKind.Unknown -> AcceptInvite.Failures.InvalidInvite + else -> it + } + } else { + it + } + }.map { roomId } + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt new file mode 100644 index 00000000000..9276f373652 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DeclineInvite.kt @@ -0,0 +1,74 @@ +/* + * 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.invite.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.invite.api.SeenInvitesStore +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.push.api.notifications.NotificationCleaner +import javax.inject.Inject + +interface DeclineInvite { + suspend operator fun invoke( + roomId: RoomId, + blockUser: Boolean, + reportRoom: Boolean, + reportReason: String? + ): Result + + sealed class Exception : kotlin.Exception() { + data object RoomNotFound : Exception() + data object DeclineInviteFailed : Exception() + data object ReportRoomFailed : Exception() + data object BlockUserFailed : Exception() + } +} + +@ContributesBinding(SessionScope::class) +class DefaultDeclineInvite @Inject constructor( + private val client: MatrixClient, + private val notificationCleaner: NotificationCleaner, + private val seenInvitesStore: SeenInvitesStore, +) : DeclineInvite { + override suspend fun invoke( + roomId: RoomId, + blockUser: Boolean, + reportRoom: Boolean, + reportReason: String? + ): Result { + val room = client.getRoom(roomId) ?: return Result.failure(DeclineInvite.Exception.RoomNotFound) + room.use { + room.leave() + .onFailure { return Result.failure(DeclineInvite.Exception.DeclineInviteFailed) } + .onSuccess { + notificationCleaner.clearMembershipNotificationForRoom( + sessionId = client.sessionId, + roomId = roomId + ) + seenInvitesStore.markAsUnSeen(roomId) + } + + if (blockUser) { + val userIdToBlock = room.info().inviter?.userId + if (userIdToBlock != null) { + client + .ignoreUser(userIdToBlock) + .onFailure { return Result.failure(DeclineInvite.Exception.BlockUserFailed) } + } + } + if (reportRoom) { + room + .reportRoom(reportReason) + .onFailure { return Result.failure(DeclineInvite.Exception.ReportRoomFailed) } + } + } + return Result.success(roomId) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt new file mode 100644 index 00000000000..38295daa27b --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt @@ -0,0 +1,79 @@ +/* + * 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.invite.impl + +import android.content.Context +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringSetPreferencesKey +import androidx.datastore.preferences.preferencesDataStoreFile +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.androidutils.hash.hash +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.observer.SessionListener +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val seenInvitesKey = stringSetPreferencesKey("seenInvites") + +class DefaultSeenInvitesStore( + context: Context, + sessionId: SessionId, + sessionCoroutineScope: CoroutineScope, + sessionObserver: SessionObserver, +) : SeenInvitesStore { + init { + sessionObserver.addListener(object : SessionListener { + override suspend fun onSessionCreated(userId: String) = Unit + override suspend fun onSessionDeleted(userId: String) { + if (sessionId.value == userId) { + clear() + } + } + }) + } + + private val dataStoreFile = sessionId.value.hash().take(16).let { hashedUserId -> + context.preferencesDataStoreFile("session_${hashedUserId}_seen-invites") + } + + private val store = PreferenceDataStoreFactory.create( + scope = sessionCoroutineScope, + migrations = emptyList(), + ) { + dataStoreFile + } + + override fun seenRoomIds(): Flow> = + store.data.map { prefs -> + prefs[seenInvitesKey] + .orEmpty() + .map { RoomId(it) } + .toSet() + } + + override suspend fun markAsSeen(roomId: RoomId) { + store.edit { prefs -> + prefs[seenInvitesKey] = prefs[seenInvitesKey].orEmpty() + roomId.value + } + } + + override suspend fun markAsUnSeen(roomId: RoomId) { + store.edit { prefs -> + prefs[seenInvitesKey] = prefs[seenInvitesKey].orEmpty() - roomId.value + } + } + + override suspend fun clear() { + dataStoreFile.safeDelete() + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt new file mode 100644 index 00000000000..256214e5d2a --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt @@ -0,0 +1,44 @@ +/* + * 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.invite.impl + +import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import kotlinx.coroutines.CoroutineScope +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultSeenInvitesStoreFactory @Inject constructor( + @ApplicationContext private val context: Context, + private val sessionObserver: SessionObserver, +) : SeenInvitesStoreFactory { + // We can have only one class accessing a single data store, so keep a cache of them. + private val cache = ConcurrentHashMap() + + override fun getOrCreate( + sessionId: SessionId, + sessionCoroutineScope: CoroutineScope, + ): SeenInvitesStore { + return cache.getOrPut(sessionId) { + DefaultSeenInvitesStore( + context = context, + sessionId = sessionId, + sessionCoroutineScope = sessionCoroutineScope, + sessionObserver = sessionObserver, + ) + } + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt new file mode 100644 index 00000000000..4d681e84624 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt @@ -0,0 +1,19 @@ +/* + * 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.invite.impl + +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.CoroutineScope + +interface SeenInvitesStoreFactory { + fun getOrCreate( + sessionId: SessionId, + sessionCoroutineScope: CoroutineScope, + ): SeenInvitesStore +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt new file mode 100644 index 00000000000..6dd75761d31 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt @@ -0,0 +1,103 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.invite.impl.acceptdecline + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite +import io.element.android.features.invite.impl.AcceptInvite +import io.element.android.features.invite.impl.DeclineInvite +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.matrix.api.core.RoomId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class AcceptDeclineInvitePresenter @Inject constructor( + private val acceptInvite: AcceptInvite, + private val declineInvite: DeclineInvite, +) : Presenter { + @Composable + override fun present(): AcceptDeclineInviteState { + val localCoroutineScope = rememberCoroutineScope() + val acceptedAction: MutableState> = + remember { mutableStateOf(AsyncAction.Uninitialized) } + val declinedAction: MutableState> = + remember { mutableStateOf(AsyncAction.Uninitialized) } + + fun handleEvents(event: AcceptDeclineInviteEvents) { + when (event) { + is AcceptDeclineInviteEvents.AcceptInvite -> { + localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) + } + + is AcceptDeclineInviteEvents.DeclineInvite -> { + val inviteData = event.invite + if (event.shouldConfirm) { + declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser) + } else { + localCoroutineScope.declineInvite( + inviteData = inviteData, + blockUser = event.blockUser, + declinedAction = declinedAction, + ) + } + } + is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> { + declinedAction.value = AsyncAction.Uninitialized + } + + is InternalAcceptDeclineInviteEvents.DismissAcceptError -> { + acceptedAction.value = AsyncAction.Uninitialized + } + + is InternalAcceptDeclineInviteEvents.DismissDeclineError -> { + declinedAction.value = AsyncAction.Uninitialized + } + } + } + + return AcceptDeclineInviteState( + acceptAction = acceptedAction.value, + declineAction = declinedAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.acceptInvite( + roomId: RoomId, + acceptedAction: MutableState>, + ) = launch { + acceptedAction.runUpdatingState { + acceptInvite(roomId) + } + } + + private fun CoroutineScope.declineInvite( + inviteData: InviteData, + blockUser: Boolean, + declinedAction: MutableState>, + ) = launch { + declinedAction.runUpdatingState { + declineInvite( + roomId = inviteData.roomId, + blockUser = blockUser, + reportRoom = false, + reportReason = null + ) + } + } +} 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 new file mode 100644 index 00000000000..9896de1d3e9 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt @@ -0,0 +1,63 @@ +/* + * 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.invite.impl.acceptdecline + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite +import io.element.android.features.invite.impl.AcceptInvite +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId + +open class AcceptDeclineInviteStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anAcceptDeclineInviteState(), + anAcceptDeclineInviteState( + declineAction = ConfirmingDeclineInvite( + InviteData( + roomId = RoomId("!room:matrix.org"), + isDm = true, + roomName = "Alice" + ), + blockUser = false, + ), + ), + anAcceptDeclineInviteState( + declineAction = ConfirmingDeclineInvite( + InviteData( + roomId = RoomId("!room:matrix.org"), + isDm = true, + roomName = "Alice" + ), + blockUser = true, + ), + ), + anAcceptDeclineInviteState( + acceptAction = AsyncAction.Failure(RuntimeException("Error while accepting invite")), + ), + anAcceptDeclineInviteState( + acceptAction = AsyncAction.Failure(AcceptInvite.Failures.InvalidInvite), + ), + anAcceptDeclineInviteState( + declineAction = AsyncAction.Failure(RuntimeException("Error while declining invite")), + ), + ) +} + +private fun anAcceptDeclineInviteState( + acceptAction: AsyncAction = AsyncAction.Uninitialized, + declineAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (AcceptDeclineInviteEvents) -> Unit = {} +) = AcceptDeclineInviteState( + acceptAction = acceptAction, + declineAction = declineAction, + eventSink = eventSink, +) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt new file mode 100644 index 00000000000..9c672594e06 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt @@ -0,0 +1,126 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.invite.impl.acceptdecline + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite +import io.element.android.features.invite.impl.AcceptInvite +import io.element.android.features.invite.impl.R +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun AcceptDeclineInviteView( + state: AcceptDeclineInviteState, + onAcceptInviteSuccess: (RoomId) -> Unit, + onDeclineInviteSuccess: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + AsyncActionView( + async = state.acceptAction, + onSuccess = onAcceptInviteSuccess, + onErrorDismiss = { + state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError) + }, + errorTitle = { + stringResource(CommonStrings.common_something_went_wrong) + }, + errorMessage = { error -> + if (error is AcceptInvite.Failures.InvalidInvite) { + stringResource(CommonStrings.error_invalid_invite) + } else { + stringResource(CommonStrings.error_network_or_server_issue) + } + } + ) + AsyncActionView( + async = state.declineAction, + onSuccess = onDeclineInviteSuccess, + onErrorDismiss = { + state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError) + }, + errorTitle = { + stringResource(CommonStrings.common_something_went_wrong) + }, + errorMessage = { + stringResource(CommonStrings.error_network_or_server_issue) + }, + confirmationDialog = { confirming -> + // Note: confirming will always be of type ConfirmingDeclineInvite. + if (confirming is ConfirmingDeclineInvite) { + DeclineConfirmationDialog( + invite = confirming.inviteData, + blockUser = confirming.blockUser, + onConfirmClick = { + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite( + confirming.inviteData, + blockUser = confirming.blockUser, + shouldConfirm = false + ) + ) + }, + onDismissClick = { + state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite) + } + ) + } + } + ) + } +} + +@Composable +private fun DeclineConfirmationDialog( + invite: InviteData, + blockUser: Boolean, + onConfirmClick: () -> Unit, + onDismissClick: () -> Unit, + modifier: Modifier = Modifier +) { + ConfirmationDialog( + modifier = modifier, + content = stringResource(R.string.screen_invites_decline_chat_message, invite.roomName), + title = if (blockUser) { + stringResource(R.string.screen_join_room_decline_and_block_alert_title) + } else { + stringResource(R.string.screen_invites_decline_chat_title) + }, + submitText = if (blockUser) { + stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation) + } else { + stringResource(CommonStrings.action_decline) + }, + cancelText = stringResource(CommonStrings.action_cancel), + onSubmitClick = onConfirmClick, + onDismiss = onDismissClick, + ) +} + +@PreviewsDayNight +@Composable +internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) = + ElementPreview { + AcceptDeclineInviteView( + state = state, + onAcceptInviteSuccess = {}, + onDeclineInviteSuccess = {}, + ) + } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt new file mode 100644 index 00000000000..e37582f63f5 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/DefaultAcceptDeclineInviteView.kt @@ -0,0 +1,35 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.invite.impl.acceptdecline + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInviteView { + @Composable + override fun Render( + state: AcceptDeclineInviteState, + onAcceptInviteSuccess: (RoomId) -> Unit, + onDeclineInviteSuccess: (RoomId) -> Unit, + modifier: Modifier, + ) { + AcceptDeclineInviteView( + state = state, + onAcceptInviteSuccess = onAcceptInviteSuccess, + onDeclineInviteSuccess = onDeclineInviteSuccess, + modifier = modifier + ) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt new file mode 100644 index 00000000000..44234304322 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt @@ -0,0 +1,16 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.invite.impl.acceptdecline + +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents + +sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents { + data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents + data object DismissAcceptError : InternalAcceptDeclineInviteEvents + data object DismissDeclineError : InternalAcceptDeclineInviteEvents +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt new file mode 100644 index 00000000000..033d2e58def --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt @@ -0,0 +1,16 @@ +/* + * 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.invite.impl.declineandblock + +sealed interface DeclineAndBlockEvents { + data class UpdateReportReason(val reason: String) : DeclineAndBlockEvents + data object ToggleReportRoom : DeclineAndBlockEvents + data object ToggleBlockUser : DeclineAndBlockEvents + data object Decline : DeclineAndBlockEvents + data object ClearDeclineAction : DeclineAndBlockEvents +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt new file mode 100644 index 00000000000..100a5de4b86 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt @@ -0,0 +1,43 @@ +/* + * 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.invite.impl.declineandblock + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.invite.api.InviteData +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class DeclineAndBlockNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: DeclineAndBlockPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + data class Inputs(val inviteData: InviteData) : NodeInputs + + private val inviteData = inputs().inviteData + private val presenter = presenterFactory.create(inviteData) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + DeclineAndBlockView( + state = state, + onBackClick = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt new file mode 100644 index 00000000000..9a18aa746bd --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt @@ -0,0 +1,92 @@ +/* + * 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.invite.impl.declineandblock + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.impl.DeclineInvite +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class DeclineAndBlockPresenter @AssistedInject constructor( + @Assisted private val inviteData: InviteData, + private val declineInvite: DeclineInvite, + private val snackbarDispatcher: SnackbarDispatcher, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(inviteData: InviteData): DeclineAndBlockPresenter + } + + @Composable + override fun present(): DeclineAndBlockState { + var reportReason by rememberSaveable { mutableStateOf("") } + var blockUser by rememberSaveable { mutableStateOf(true) } + var reportRoom by rememberSaveable { mutableStateOf(false) } + val declineAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: DeclineAndBlockEvents) { + when (event) { + DeclineAndBlockEvents.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized + DeclineAndBlockEvents.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction) + DeclineAndBlockEvents.ToggleBlockUser -> blockUser = !blockUser + DeclineAndBlockEvents.ToggleReportRoom -> reportRoom = !reportRoom + is DeclineAndBlockEvents.UpdateReportReason -> reportReason = event.reason + } + } + + return DeclineAndBlockState( + reportRoom = reportRoom, + reportReason = reportReason, + blockUser = blockUser, + declineAction = declineAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.decline( + reason: String, + blockUser: Boolean, + reportRoom: Boolean, + action: MutableState> + ) = launch { + action.value = AsyncAction.Loading + declineInvite( + roomId = inviteData.roomId, + blockUser = blockUser, + reportRoom = reportRoom, + reportReason = reason + ).onSuccess { + action.value = AsyncAction.Success(Unit) + }.onFailure { error -> + if (error is DeclineInvite.Exception.DeclineInviteFailed) { + action.value = AsyncAction.Failure(error) + } else { + action.value = AsyncAction.Uninitialized + snackbarDispatcher.post(SnackbarMessage(CommonStrings.error_unknown)) + } + } + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt new file mode 100644 index 00000000000..0ee7444b2ce --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt @@ -0,0 +1,20 @@ +/* + * 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.invite.impl.declineandblock + +import io.element.android.libraries.architecture.AsyncAction + +data class DeclineAndBlockState( + val reportRoom: Boolean, + val reportReason: String, + val blockUser: Boolean, + val declineAction: AsyncAction, + val eventSink: (DeclineAndBlockEvents) -> Unit +) { + val canDecline = blockUser || reportRoom && reportReason.isNotEmpty() +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt new file mode 100644 index 00000000000..87016d13cca --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt @@ -0,0 +1,45 @@ +/* + * 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.invite.impl.declineandblock + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction + +open class DeclineAndBlockStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aDeclineAndBlockState(), + aDeclineAndBlockState( + reportRoom = true, + reportReason = "Inappropriate content", + ), + aDeclineAndBlockState( + blockUser = true, + ), + aDeclineAndBlockState( + declineAction = AsyncAction.Loading, + ), + aDeclineAndBlockState( + declineAction = AsyncAction.Failure(Exception("Failed to decline")), + ), + ) +} + +fun aDeclineAndBlockState( + reportRoom: Boolean = false, + reportReason: String = "", + blockUser: Boolean = false, + declineAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (DeclineAndBlockEvents) -> Unit = {}, +) = DeclineAndBlockState( + reportRoom = reportRoom, + reportReason = reportReason, + blockUser = blockUser, + declineAction = declineAction, + eventSink = eventSink, +) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt new file mode 100644 index 00000000000..05110f982c1 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt @@ -0,0 +1,152 @@ +/* + * 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.invite.impl.declineandblock + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +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.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.invite.impl.R +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.list.ListItemContent +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.ListItem +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeclineAndBlockView( + state: DeclineAndBlockState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + + val isDeclining = state.declineAction is AsyncAction.Loading + AsyncActionView( + async = state.declineAction, + onSuccess = { onBackClick() }, + errorMessage = { stringResource(CommonStrings.error_unknown) }, + onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) }, + onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) } + ) + + Scaffold( + topBar = { + TopAppBar( + titleStr = stringResource(R.string.screen_decline_and_block_title), + navigationIcon = { + BackButton(onClick = onBackClick) + } + ) + }, + modifier = modifier + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .imePadding() + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(vertical = 16.dp) + ) { + ListItem( + modifier = Modifier.padding(end = 8.dp), + headlineContent = { + Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_title)) + }, + supportingContent = { + Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_description)) + }, + onClick = { + state.eventSink(DeclineAndBlockEvents.ToggleBlockUser) + }, + trailingContent = ListItemContent.Switch(checked = state.blockUser) + ) + + Spacer(modifier = Modifier.height(24.dp)) + ListItem( + modifier = Modifier.padding(end = 8.dp), + headlineContent = { + Text(text = stringResource(CommonStrings.action_report_room)) + }, + supportingContent = { + Text(text = stringResource(R.string.screen_decline_and_block_report_user_option_description)) + }, + onClick = { + state.eventSink(DeclineAndBlockEvents.ToggleReportRoom) + }, + trailingContent = ListItemContent.Switch(checked = state.reportRoom) + ) + + if (state.reportRoom) { + Spacer(modifier = Modifier.height(24.dp)) + TextField( + value = state.reportReason, + onValueChange = { state.eventSink(DeclineAndBlockEvents.UpdateReportReason(it)) }, + placeholder = stringResource(R.string.screen_decline_and_block_report_user_reason_placeholder), + minLines = 3, + enabled = !isDeclining, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .heightIn(min = 90.dp), + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + Button( + text = stringResource(CommonStrings.action_decline), + destructive = true, + showProgress = isDeclining, + enabled = !isDeclining && state.canDecline, + onClick = { + focusManager.clearFocus(force = true) + state.eventSink(DeclineAndBlockEvents.Decline) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun DeclineAndBlockViewPreview( + @PreviewParameter(DeclineAndBlockStateProvider::class) state: DeclineAndBlockState +) = ElementPreview { + DeclineAndBlockView( + state = state, + onBackClick = {}, + ) +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt new file mode 100644 index 00000000000..5eab22dd912 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt @@ -0,0 +1,25 @@ +/* + * 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.invite.impl.declineandblock + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultDeclineAndBlockEntryPoint @Inject constructor() : DeclineInviteAndBlockEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node { + val inputs = DeclineAndBlockNode.Inputs(inviteData) + return parentNode.createNode(buildContext, plugins = listOf(inputs)) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt index 6a117b621b9..4f5da603a6a 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt @@ -10,14 +10,31 @@ package io.element.android.features.invite.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter +import dagger.Provides +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.impl.SeenInvitesStoreFactory +import io.element.android.features.invite.impl.acceptdecline.AcceptDeclineInvitePresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient @ContributesTo(SessionScope::class) @Module interface InviteModule { @Binds fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter + + companion object { + @Provides + fun providesSeenInvitesStore( + factory: SeenInvitesStoreFactory, + matrixClient: MatrixClient, + ): SeenInvitesStore { + return factory.getOrCreate( + matrixClient.sessionId, + matrixClient.sessionCoroutineScope, + ) + } + } } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt deleted file mode 100644 index bfc751f03a9..00000000000 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.impl.response - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.ConfirmingDeclineInvite -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.architecture.runUpdatingState -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias -import io.element.android.libraries.matrix.api.room.join.JoinRoom -import io.element.android.libraries.push.api.notifications.NotificationCleaner -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import javax.inject.Inject - -class AcceptDeclineInvitePresenter @Inject constructor( - private val client: MatrixClient, - private val joinRoom: JoinRoom, - private val notificationCleaner: NotificationCleaner, -) : Presenter { - @Composable - override fun present(): AcceptDeclineInviteState { - val localCoroutineScope = rememberCoroutineScope() - val acceptedAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } - val declinedAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } - - fun handleEvents(event: AcceptDeclineInviteEvents) { - when (event) { - is AcceptDeclineInviteEvents.AcceptInvite -> { - localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) - } - - is AcceptDeclineInviteEvents.DeclineInvite -> { - declinedAction.value = ConfirmingDeclineInvite(event.invite) - } - - is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> { - localCoroutineScope.declineInvite(event.roomId, declinedAction) - } - - is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> { - declinedAction.value = AsyncAction.Uninitialized - } - - is InternalAcceptDeclineInviteEvents.DismissAcceptError -> { - acceptedAction.value = AsyncAction.Uninitialized - } - - is InternalAcceptDeclineInviteEvents.DismissDeclineError -> { - declinedAction.value = AsyncAction.Uninitialized - } - } - } - - return AcceptDeclineInviteState( - acceptAction = acceptedAction.value, - declineAction = declinedAction.value, - eventSink = ::handleEvents - ) - } - - private fun CoroutineScope.acceptInvite( - roomId: RoomId, - acceptedAction: MutableState>, - ) = launch { - acceptedAction.runUpdatingState { - joinRoom( - roomIdOrAlias = roomId.toRoomIdOrAlias(), - serverNames = emptyList(), - trigger = JoinedRoom.Trigger.Invite, - ) - .onSuccess { - notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId) - } - .map { roomId } - } - } - - private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState>) = launch { - suspend { - client.getPendingRoom(roomId)?.use { - it.leave().getOrThrow() - notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId) - } - roomId - }.runCatchingUpdatingState(declinedAction) - } -} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt deleted file mode 100644 index b40a08fdbc1..00000000000 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.impl.response - -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider -import io.element.android.features.invite.api.response.ConfirmingDeclineInvite -import io.element.android.features.invite.api.response.InviteData -import io.element.android.features.invite.impl.R -import io.element.android.libraries.designsystem.components.async.AsyncActionView -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.ui.strings.CommonStrings - -@Composable -fun AcceptDeclineInviteView( - state: AcceptDeclineInviteState, - onAcceptInvite: (RoomId) -> Unit, - onDeclineInvite: (RoomId) -> Unit, - modifier: Modifier = Modifier, -) { - Box(modifier = modifier) { - AsyncActionView( - async = state.acceptAction, - onSuccess = onAcceptInvite, - onErrorDismiss = { - state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError) - }, - ) - AsyncActionView( - async = state.declineAction, - onSuccess = onDeclineInvite, - onErrorDismiss = { - state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError) - }, - confirmationDialog = { confirming -> - // Note: confirming will always be of type ConfirmingDeclineInvite. - if (confirming is ConfirmingDeclineInvite) { - DeclineConfirmationDialog( - invite = confirming.inviteData, - onConfirmClick = { - state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(confirming.inviteData.roomId)) - }, - onDismissClick = { - state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite) - } - ) - } - } - ) - } -} - -@Composable -private fun DeclineConfirmationDialog( - invite: InviteData, - onConfirmClick: () -> Unit, - onDismissClick: () -> Unit, - modifier: Modifier = Modifier -) { - val contentResource = if (invite.isDm) { - R.string.screen_invites_decline_direct_chat_message - } else { - R.string.screen_invites_decline_chat_message - } - - val titleResource = if (invite.isDm) { - R.string.screen_invites_decline_direct_chat_title - } else { - R.string.screen_invites_decline_chat_title - } - - ConfirmationDialog( - modifier = modifier, - content = stringResource(contentResource, invite.roomName), - title = stringResource(titleResource), - submitText = stringResource(CommonStrings.action_decline), - cancelText = stringResource(CommonStrings.action_cancel), - onSubmitClick = onConfirmClick, - onDismiss = onDismissClick, - ) -} - -@PreviewsDayNight -@Composable -internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) = - ElementPreview { - AcceptDeclineInviteView( - state = state, - onAcceptInvite = {}, - onDeclineInvite = {}, - ) - } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt deleted file mode 100644 index 8d52c26df7d..00000000000 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.impl.response - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.AcceptDeclineInviteView -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.core.RoomId -import javax.inject.Inject - -@ContributesBinding(SessionScope::class) -class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInviteView { - @Composable - override fun Render( - state: AcceptDeclineInviteState, - onAcceptInvite: (RoomId) -> Unit, - onDeclineInvite: (RoomId) -> Unit, - modifier: Modifier, - ) { - AcceptDeclineInviteView( - state = state, - onAcceptInvite = onAcceptInvite, - onDeclineInvite = onDeclineInvite, - modifier = modifier - ) - } -} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt deleted file mode 100644 index 55072189e98..00000000000 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.impl.response - -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.libraries.matrix.api.core.RoomId - -sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents { - data class ConfirmDeclineInvite(val roomId: RoomId) : InternalAcceptDeclineInviteEvents - data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents - data object DismissAcceptError : InternalAcceptDeclineInviteEvents - data object DismissDeclineError : InternalAcceptDeclineInviteEvents -} diff --git a/features/invite/impl/src/main/res/values-bg/translations.xml b/features/invite/impl/src/main/res/values-bg/translations.xml index 3418954ad6e..5e3c9a6fbdf 100644 --- a/features/invite/impl/src/main/res/values-bg/translations.xml +++ b/features/invite/impl/src/main/res/values-bg/translations.xml @@ -1,5 +1,6 @@ + "Блокиране на потребителя" "Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?" "Отказване на покана" "Няма покани" diff --git a/features/invite/impl/src/main/res/values-cs/translations.xml b/features/invite/impl/src/main/res/values-cs/translations.xml index d4c60464b3b..f5762693435 100644 --- a/features/invite/impl/src/main/res/values-cs/translations.xml +++ b/features/invite/impl/src/main/res/values-cs/translations.xml @@ -1,9 +1,18 @@ + "Od tohoto uživatele neuvidíte žádné zprávy ani pozvánky do místnosti" + "Zablokovat uživatele" + "Nahlaste tuto místnost svému poskytovateli účtu." + "Popište důvod nahlášení…" + "Odmítnout a zablokovat" "Opravdu chcete odmítnout pozvánku do %1$s?" "Odmítnout pozvání" "Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?" "Odmítnout chat" "Žádné pozvánky" "%1$s (%2$s) vás pozval(a)" + "Ano, odmítnout a zablokovat" + "Opravdu chcete odmítnout pozvánku do této místnosti? Tím také zabráníte tomu, aby vás %1$s kontaktoval(a) nebo pozval(a) do místností." + "Odmítnout pozvání a zablokovat" + "Odmítnout a zablokovat" diff --git a/features/invite/impl/src/main/res/values-cy/translations.xml b/features/invite/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..a84fa2397a9 --- /dev/null +++ b/features/invite/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,18 @@ + + + "Fyddwch chi ddim yn gweld unrhyw negeseuon neu wahoddiadau ystafell gan y defnyddiwr hwn" + "Rhwystro defnyddiwr" + "Adrodd am yr ystafell hon i ddarparwr eich cyfrif." + "Disgrifiwch y rheswm dros adrodd…" + "Gwrthod a rhwystro" + "Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?" + "Gwrthod y gwahoddiad" + "Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?" + "Gwrthod sgwrs" + "Dim Gwahoddiadau" + "Mae %1$s (%2$s) wedi eich gwahodd" + "Iawn, gwrthod a rhwystro" + "Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â\'r ystafell hon? Bydd hyn hefyd yn atal %1$s rhag cysylltu â chi neu eich gwahodd i ystafelloedd." + "Gwrthod gwahoddiad a rhwystro" + "Gwrthod a rhwystro" + diff --git a/features/invite/impl/src/main/res/values-da/translations.xml b/features/invite/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..6093a431bc5 --- /dev/null +++ b/features/invite/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,18 @@ + + + "Du vil ikke se nogen beskeder eller rum-invitationer fra denne bruger" + "Bloker bruger" + "Anmeld dette rum til din kontoudbyder." + "Beskriv årsagen til anmeldelsen…" + "Afvis og blokér" + "Er du sikker på, at du vil afvise invitationen til at deltage i %1$s?" + "Afvis invitation" + "Er du sikker på, at du vil afvise denne private samtale med %1$s?" + "Afvis samtale" + "Ingen invitationer" + "%1$s(%2$s ) inviterede dig" + "Ja, afvis og blokér" + "Er du sikker på, at du vil afvise invitationen til at deltage i dette rum? Dette forhindrer også %1$s i at kontakte dig eller invitere dig til andre rum." + "Afvis invitation og blokér" + "Afvis og blokér" + diff --git a/features/invite/impl/src/main/res/values-de/translations.xml b/features/invite/impl/src/main/res/values-de/translations.xml index bb38811a7a3..e3937fbda13 100644 --- a/features/invite/impl/src/main/res/values-de/translations.xml +++ b/features/invite/impl/src/main/res/values-de/translations.xml @@ -1,9 +1,18 @@ - "Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?" + "Sie werden keine Nachrichten oder Chateinladungen von diesem Nutzer sehen." + "Benutzer blockieren" + "Melden Sie diesen Raum Ihrem Kontoanbieter." + "Nennen Sie den Grund für die Meldung…" + "Ablehnen und blockieren" + "Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?" "Einladung ablehnen" - "Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?" + "Möchten Sie diesen privaten Chat mit %1$s wirklich ablehnen?" "Einladung ablehnen" "Keine Einladungen" "%1$s (%2$s) hat dich eingeladen" + "Ja, ablehnen & blockieren" + "Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt." + "Einladung ablehnen & Nutzer blockieren" + "Ablehnen und blockieren" diff --git a/features/invite/impl/src/main/res/values-el/translations.xml b/features/invite/impl/src/main/res/values-el/translations.xml index 78f2871617a..96f10b29506 100644 --- a/features/invite/impl/src/main/res/values-el/translations.xml +++ b/features/invite/impl/src/main/res/values-el/translations.xml @@ -1,9 +1,18 @@ + "Δεν θα βλέπετε μηνύματα ή προσκλήσεις αίθουσας από αυτόν τον χρήστη" + "Αποκλεισμός χρήστη" + "Αναφέρετε αυτή την αίθουσα στον πάροχο του λογαριασμού σας." + "Περιγράψτε τον λόγο αναφοράς…" + "Απόρριψη και αποκλεισμός" "Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;" "Απόρριψη πρόσκλησης" "Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;" "Απόρριψη συνομιλίας" "Χωρίς προσκλήσεις" "%1$s (%2$s) σέ προσκάλεσε" + "Ναι, απόρριψη και αποκλεισμός" + "Είστε βέβαιοι ότι θέλετε να απορρίψετε την πρόσκληση συμμετοχής σε αυτήν την αίθουσα; Αυτό θα εμποδίσει επίσης τον χρήστη %1$s να επικοινωνήσει μαζί σας ή να σας προσκαλέσει σε αίθουσες." + "Απόρριψη πρόσκλησης και αποκλεισμός" + "Απόρριψη και αποκλεισμός" diff --git a/features/invite/impl/src/main/res/values-es/translations.xml b/features/invite/impl/src/main/res/values-es/translations.xml index 890df29447a..a88f9bc28db 100644 --- a/features/invite/impl/src/main/res/values-es/translations.xml +++ b/features/invite/impl/src/main/res/values-es/translations.xml @@ -1,9 +1,18 @@ + "No verás ningún mensaje ni invitación a sala que provenga de este usuario" + "Bloquear usuario" + "Denunciar esta sala a tu proveedor de cuentas." + "Describe el motivo de la denuncia…" + "Rechazar y bloquear" "¿Estás seguro de que quieres rechazar la invitación a unirte a %1$s?" "Rechazar la invitación" - "¿Estás seguro de que quieres rechazar este chat privado con%1$s?" + "¿Estás seguro de que quieres rechazar este chat privado con %1$s?" "Rechazar el chat" "Sin invitaciones" "%1$s (%2$s) te invitó" + "Sí, rechazar y bloquear" + "¿Estás seguro de que deseas rechazar la invitación para unirte a esta sala? Esto también impedirá que %1$s pueda contactar contigo o invitarte a salas." + "Rechazar invitación y bloquear" + "Rechazar y bloquear" diff --git a/features/invite/impl/src/main/res/values-et/translations.xml b/features/invite/impl/src/main/res/values-et/translations.xml index 316ba75ef9a..91906fa8778 100644 --- a/features/invite/impl/src/main/res/values-et/translations.xml +++ b/features/invite/impl/src/main/res/values-et/translations.xml @@ -1,9 +1,18 @@ + "Sa ei näe enam selle kasutaja saadetud sõnumeid ja jututubade kutseid" + "Blokeeri kasutaja" + "Teata sellest jututoast oma teenusepakkujale." + "Kirjelda teatamise põhjust…" + "Keeldu ja blokeeri" "Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?" "Lükka kutse tagasi" "Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?" "Keeldu vestlusest" "Kutseid pole" "%1$s (%2$s) saatis sulle kutse" + "Jah, keeldu ja blokeeri" + "Kas sa oled kindel, et soovid keelduda kutsest sellesse jututuppa? Samaga kaob kasutajal %1$s võimalus sinuga suhelda ja saata sulle jututubade kutseid." + "Keeldu kutsest ja blokeeri" + "Keeldu ja blokeeri" diff --git a/features/invite/impl/src/main/res/values-eu/translations.xml b/features/invite/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..d60aeced771 --- /dev/null +++ b/features/invite/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,9 @@ + + + "Ziur %1$s(e)ra batzeko gonbidapena baztertu nahi duzula?" + "Baztertu gonbidapena" + "Ziur %1$s(r)en txat pribatua baztertu nahi duzula?" + "Baztertu txata" + "Ez dago gonbidapenik" + "%1$s(e)k (%2$s) gonbidatu zaitu" + diff --git a/features/invite/impl/src/main/res/values-fa/translations.xml b/features/invite/impl/src/main/res/values-fa/translations.xml index 8102a3be578..9cbad94f91a 100644 --- a/features/invite/impl/src/main/res/values-fa/translations.xml +++ b/features/invite/impl/src/main/res/values-fa/translations.xml @@ -1,9 +1,15 @@ + "انسداد کاربر" + "شرح دلیل گزارش…" + "رد و انسداد" "مطمئنید که می‌خواهید دعوت پیوستن به %1$s را رد کنید؟" "رد دعوت" "مطمئنید که می‌خواهید این گپ خصوصی با %1$s را رد کنید؟" "رد گپ" "بدون دعوت" "%1$s (%2$s) دعوتتان کرد" + "بله. رد و انسداد" + "رد دعوت و انسداد" + "رد و انسداد" diff --git a/features/invite/impl/src/main/res/values-fi/translations.xml b/features/invite/impl/src/main/res/values-fi/translations.xml index bf5b83a8c6f..47ad368d738 100644 --- a/features/invite/impl/src/main/res/values-fi/translations.xml +++ b/features/invite/impl/src/main/res/values-fi/translations.xml @@ -1,9 +1,18 @@ + "Et tule näkemään viestejä tai kutsuja tältä käyttäjältä" + "Estä käyttäjä" + "Ilmoita tästä huoneesta palveluntarjoajallesi." + "Kerro syy ilmoittamiseen…" + "Hylkää ja estä" "Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?" "Hylkää kutsu" "Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?" "Hylkää keskustelu" "Ei kutsuja" "%1$s (%2$s) kutsui sinut" + "Kyllä, hylkää ja estä" + "Oletko varma, että haluat kieltäytyä kutsusta liittyä tähän huoneeseen? Tämä estää myös käyttäjää %1$s ottamasta sinuun yhteyttä tai kutsumasta sinua huoneisiin." + "Hylkää kutsu ja estä" + "Hylkää ja estä" diff --git a/features/invite/impl/src/main/res/values-fr/translations.xml b/features/invite/impl/src/main/res/values-fr/translations.xml index 45c29b35723..1775da05389 100644 --- a/features/invite/impl/src/main/res/values-fr/translations.xml +++ b/features/invite/impl/src/main/res/values-fr/translations.xml @@ -1,9 +1,18 @@ + "Vous ne verrez aucun messages ou invitation à un salon de la part de cet utilisateur" + "Bloquer l’utilisateur" + "Signalez ce salon à votre fournisseur de compte." + "Décrivez la raison du signalement…" + "Refuser et bloquer" "Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?" "Refuser l’invitation" "Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?" "Refuser l’invitation" "Aucune invitation" "%1$s (%2$s) vous a invité(e)" + "Oui, refuser et bloquer" + "Êtes-vous sûr de vouloir refuser l’invitation à rejoindre ce salon ? Cela empêchera également %1$s de vous contacter ou de vous inviter dans les salons." + "Refuser l’invitation et bloquer" + "Refuser et bloquer" diff --git a/features/invite/impl/src/main/res/values-hu/translations.xml b/features/invite/impl/src/main/res/values-hu/translations.xml index e7faddd8f95..97595ed421d 100644 --- a/features/invite/impl/src/main/res/values-hu/translations.xml +++ b/features/invite/impl/src/main/res/values-hu/translations.xml @@ -1,9 +1,18 @@ + "Ettől a felhasználótól nem fog többé üzeneteket vagy meghívásokat látni." + "Felhasználó letiltása" + "A szoba jelentése a fiókszolgáltatójának." + "Írja le a jelentés okát…" + "Elutasítás és blokkolás" "Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?" "Meghívás elutasítása" "Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?" "Csevegés elutasítása" "Nincsenek meghívások" "%1$s (%2$s) meghívta" + "Igen, elutasítás és blokkolás" + "Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez a szobához? Ez azt is megakadályozza, hogy %1$s kapcsolatba lépjen Önnel, vagy szobákba hívja." + "Meghívó elutasítása és blokkolás" + "Elutasítás és blokkolás" diff --git a/features/invite/impl/src/main/res/values-in/translations.xml b/features/invite/impl/src/main/res/values-in/translations.xml index 97866a90433..6f8ab156103 100644 --- a/features/invite/impl/src/main/res/values-in/translations.xml +++ b/features/invite/impl/src/main/res/values-in/translations.xml @@ -1,9 +1,18 @@ + "Anda tidak akan melihat pesan atau undangan ruangan dari pengguna ini" + "Blokir pengguna" + "Laporkan ruangan ini ke penyedia akun Anda." + "Jelaskan alasan untuk melaporkan…" + "Tolak dan blokir" "Apakah Anda yakin ingin menolak undangan untuk bergabung ke %1$s?" "Tolak undangan" "Apakah Anda yakin ingin menolak obrolan pribadi dengan %1$s?" "Tolak obrolan" "Tidak ada undangan" "%1$s (%2$s) mengundang Anda" + "Ya, tolak & blokir" + "Apakah Anda yakin ingin menolak undangan untuk bergabung dengan ruangan ini? Ini juga akan mencegah %1$s menghubungi Anda atau mengundang Anda ke ruangan." + "Tolak undangan & blokir" + "Tolak dan blokir" diff --git a/features/invite/impl/src/main/res/values-it/translations.xml b/features/invite/impl/src/main/res/values-it/translations.xml index 7da765090e3..d88183f90ac 100644 --- a/features/invite/impl/src/main/res/values-it/translations.xml +++ b/features/invite/impl/src/main/res/values-it/translations.xml @@ -1,9 +1,18 @@ + "Non vedrai alcun messaggio o invito ad una stanza da parte di questo utente" + "Blocca utente" + "Segnala questa stanza al fornitore del tuo account." + "Descrivi il motivo della segnalazione…" + "Rifiuta e blocca" "Vuoi davvero rifiutare l\'invito ad entrare in %1$s?" "Rifiuta l\'invito" "Vuoi davvero rifiutare questa conversazione privata con %1$s?" "Rifiuta l\'invito alla conversazione" "Nessun invito" "%1$s (%2$s) ti ha invitato" + "Sì, rifiuta e blocca" + "Sei sicuro di voler rifiutare l\'invito a entrare in questa stanza? Ciò impedirà a %1$s di contattarti o invitarti nuovamente in una stanza." + "Rifiuta invito e blocca" + "Rifiuta e blocca" diff --git a/features/invite/impl/src/main/res/values-lt/translations.xml b/features/invite/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..c94b61fd5d5 --- /dev/null +++ b/features/invite/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,9 @@ + + + "Ar tikrai norite atmesti kvietimą prisijungti prie %1$s?" + "Atmesti kvietimą" + "Ar tikrai norite atmesti šį privatų pokalbį su %1$s ?" + "Atmesti pokalbį" + "Jokių kvietimų" + "%1$s(%2$s) pakvietė Jus" + diff --git a/features/invite/impl/src/main/res/values-nb/translations.xml b/features/invite/impl/src/main/res/values-nb/translations.xml index 59b1fffb086..25234663487 100644 --- a/features/invite/impl/src/main/res/values-nb/translations.xml +++ b/features/invite/impl/src/main/res/values-nb/translations.xml @@ -1,9 +1,18 @@ + "Du vil ikke se noen meldinger eller rominvitasjoner fra denne brukeren" + "Blokker bruker" + "Rapporter dette rommet til din kontoleverandør." + "Beskriv årsaken for å rapportere…" + "Avslå og blokker" "Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?" "Avvis invitasjon" "Er du sikker på at du vil avslå denne private chatten med %1$s?" "Avslå chat" "Ingen invitasjoner" "%1$s(%2$s) inviterte deg" + "Ja, avslå og blokker" + "Er du sikker på at du vil avslå invitasjonen til å bli med i dette rommet? Dette vil også forhindre %1$s fra å kontakte deg eller invitere deg til rom." + "Avslå invitasjon og blokker" + "Avslå og blokker" diff --git a/features/invite/impl/src/main/res/values-pl/translations.xml b/features/invite/impl/src/main/res/values-pl/translations.xml index 95283e4cbab..e8636fc3dc1 100644 --- a/features/invite/impl/src/main/res/values-pl/translations.xml +++ b/features/invite/impl/src/main/res/values-pl/translations.xml @@ -1,9 +1,18 @@ + "Nie zobaczysz żadnych wiadomości ani zaproszeń od tego użytkownika" + "Zablokuj użytkownika" + "Zgłoś pokój dostawcy swojego konta." + "Opisz powód zgłoszenia…" + "Odrzuć i zablokuj" "Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?" "Odrzuć zaproszenie" "Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?" "Odrzuć czat" "Brak zaproszeń" "%1$s (%2$s) zaprosił Cię" + "Tak, odrzuć i zablokuj" + "Czy na pewno chcesz odrzucić zaproszenie dołączenia do tego pokoju? %1$s nie będzie mógł się również z Tobą skontaktować, ani zaprosić Cię do pokoju." + "Odrzuć zaproszenie i zablokuj" + "Odrzuć i zablokuj" diff --git a/features/invite/impl/src/main/res/values-pt-rBR/translations.xml b/features/invite/impl/src/main/res/values-pt-rBR/translations.xml index 9cd284690cb..43569cdd312 100644 --- a/features/invite/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/invite/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,9 +1,18 @@ + "Você não verá nenhuma mensagem ou convite de sala deste usuário" + "Bloquear usuário" + "Denuncie esta sala ao fornecedor da sua conta." + "Descreva o motivo da denúncia…" + "Recusar e bloquear" "Tem certeza de que deseja recusar o convite para ingressar em %1$s?" "Recusar convite" "Tem certeza de que deseja recusar esse chat privado com %1$s?" "Recusar chat" "Sem convites" "%1$s(%2$s) convidou você" + "Sim, recusar e bloquear" + "Você tem certeza de que deseja recusar o convite para participar desta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para as salas." + "Recusar convite e bloquear" + "Recusar e bloquear" diff --git a/features/invite/impl/src/main/res/values-pt/translations.xml b/features/invite/impl/src/main/res/values-pt/translations.xml index a7f257261ae..5813c9e54f5 100644 --- a/features/invite/impl/src/main/res/values-pt/translations.xml +++ b/features/invite/impl/src/main/res/values-pt/translations.xml @@ -1,9 +1,18 @@ + "Não verás quaisquer mensagens ou convites deste utilizador" + "Bloquear utilizador" + "Denunciar esta sala ao teu operador de conta." + "Descreve a razão de denúncia…" + "Rejeitar e bloquear" "Tens a certeza que queres rejeitar o convite para entra em %1$s?" "Rejeitar convite" "Tem a certeza que queres rejeitar esta conversa privada com %1$s?" "Rejeitar conversa" "Sem convites" "%1$s (%2$s) convidou-te" + "Sim, recusar & bloquear" + "Tens a certeza de que queres recusar o convite para entrar nesta sala? Isto também evitará que %1$s te contacte ou te convide para salas." + "Recusar convite & bloquear" + "Recusar e bloquear" diff --git a/features/invite/impl/src/main/res/values-ru/translations.xml b/features/invite/impl/src/main/res/values-ru/translations.xml index b13ca06a9df..90d467e62cc 100644 --- a/features/invite/impl/src/main/res/values-ru/translations.xml +++ b/features/invite/impl/src/main/res/values-ru/translations.xml @@ -1,9 +1,18 @@ + "Вы не увидите сообщений или приглашений в комнату от этого пользователя" + "Заблокировать пользователя" + "Сообщите об этой комнате своему поставщику учетной записи." + "Опишите причину жалобы…" + "Отклонить и заблокировать" "Вы уверены, что хотите отклонить приглашение в %1$s?" "Отклонить приглашение" "Вы уверены, что хотите отказаться от личного общения с %1$s?" "Отклонить чат" "Нет приглашений" "%1$s (%2$s) пригласил вас" + "Да, отклонить и заблокировать" + "Вы действительно хотите отклонить приглашение в эту комнате? Это также предотвратит %1$s возможность связываться с вами или приглашать вас в комнаты." + "Отклонить приглашение и заблокировать" + "Отклонить и заблокировать" diff --git a/features/invite/impl/src/main/res/values-sk/translations.xml b/features/invite/impl/src/main/res/values-sk/translations.xml index 2875466fc7c..a14b5b0dae5 100644 --- a/features/invite/impl/src/main/res/values-sk/translations.xml +++ b/features/invite/impl/src/main/res/values-sk/translations.xml @@ -1,9 +1,18 @@ + "Od tohto používateľa sa vám nezobrazia žiadne správy ani pozvánky do miestnosti" + "Zablokovať používateľa" + "Nahlásiť túto miestnosť poskytovateľovi účtu." + "Opíšte dôvod nahlásenia…" + "Odmietnuť a zablokovať" "Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?" "Odmietnuť pozvanie" "Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?" "Odmietnuť konverzáciu" "Žiadne pozvánky" "%1$s (%2$s) vás pozval/a" + "Áno, odmietnuť a zablokovať" + "Ste si istí, že chcete odmietnuť pozvanie na vstup do tejto miestnosti? To tiež zabráni tomu, aby vás %1$s kontaktoval/a alebo vás pozval/a do miestností." + "Odmietnuť pozvánku a zablokovať" + "Odmietnuť a zablokovať" diff --git a/features/invite/impl/src/main/res/values-sv/translations.xml b/features/invite/impl/src/main/res/values-sv/translations.xml index 6a091b096b2..a57336b50e4 100644 --- a/features/invite/impl/src/main/res/values-sv/translations.xml +++ b/features/invite/impl/src/main/res/values-sv/translations.xml @@ -1,9 +1,18 @@ + "Du kommer inte att se några meddelanden eller rumsinbjudningar från den här användaren" + "Blockera användare" + "Rapportera det här rummet till din kontoleverantör." + "Beskriv skälet för anmälan …" + "Avvisa och blockera" "Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?" "Avböj inbjudan" "Är du säker på att du vill avböja denna privata chatt med %1$s?" "Avböj chatt" "Inga inbjudningar" "%1$s (%2$s) bjöd in dig" + "Ja, avvisa och blockera" + "Är du säker på att du vill avvisa inbjudan att gå med i det här rummet? Detta kommer också att hindra %1$s från att kontakta dig eller bjuda in dig till rum." + "Avvisa inbjudan och blockera" + "Avvisa och blockera" diff --git a/features/invite/impl/src/main/res/values-tr/translations.xml b/features/invite/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..7c60a22b8a5 --- /dev/null +++ b/features/invite/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,13 @@ + + + "%1$s katılma davetini reddetmek istediğinizden emin misiniz?" + "Daveti reddet" + "%1$s ile bu özel sohbeti reddetmek istediğinizden emin misiniz?" + "Sohbeti reddet" + "Davet Yok" + "%1$s (%2$s) sizi davet etti" + "Evet, reddet ve engelle" + "Bu odaya katılma davetini reddetmek istediğinizden emin misiniz? Bu aynı zamanda %1$s sizinle iletişim kurmasını veya sizi odalara davet etmesini de engeller." + "Daveti reddet ve engelle" + "Reddet ve engelle" + diff --git a/features/invite/impl/src/main/res/values-uk/translations.xml b/features/invite/impl/src/main/res/values-uk/translations.xml index fa535213bae..9a5aef72987 100644 --- a/features/invite/impl/src/main/res/values-uk/translations.xml +++ b/features/invite/impl/src/main/res/values-uk/translations.xml @@ -1,9 +1,18 @@ + "Ви не бачитимете повідомлень або запрошень у кімнату від цього користувача" + "Заблокувати користувача" + "Поскаржитися на цю кімнату постачальнику облікового запису." + "Опишіть причину скарги…" + "Відхилити та заблокувати" "Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?" "Відхилити запрошення" "Ви дійсно хочете відмовитися від приватної бесіди з %1$s?" "Відхилити бесіду" "Немає запрошень" "%1$s (%2$s) запрошує вас" + "Так, відхилити та заблокувати" + "Ви впевнені, що хочете відхилити запрошення приєднатися до цієї кімнати? Це також завадить %1$s зв\'язатися з вами або запрошувати вас в кімнати." + "Відхилити запрошення та заблокувати" + "Відхилити та заблокувати" diff --git a/features/invite/impl/src/main/res/values-ur/translations.xml b/features/invite/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..641f5e7b7e7 --- /dev/null +++ b/features/invite/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,9 @@ + + + "کیا آپکو یقین ہے کہ آپ %1$s میں شامل ہونے کی درخواست مسترد کرنا چاہتے ہیں؟" + "دعوت مسترد کریں" + "کیا آپکو یقین ہے کہ آپ %1$s کیساتھ نجی گفتگو مسترد کرنا چاہتے ہیں؟" + "گفتگو مسترد کریں" + "کوئی دعوت نامے نہیں" + "%1$s (%2$s) نے آپ کو مدعو کیا" + diff --git a/features/invite/impl/src/main/res/values-zh-rTW/translations.xml b/features/invite/impl/src/main/res/values-zh-rTW/translations.xml index 4c7d99280fa..5b75fcd726a 100644 --- a/features/invite/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/invite/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,9 +1,18 @@ + "您將不會看到來自此使用者的任何訊息或聊天室邀請" + "封鎖使用者" + "向您的帳號提供者回報此聊天室。" + "說明回報的原因……" + "拒絕並封鎖" "您確定您想要拒絕加入 %1$s 的邀請嗎?" "拒絕邀請" "您確定您要拒絕此與 %1$s 的私人聊天嗎?" "拒絕聊天" "沒有邀請" "%1$s(%2$s)邀請您" + "是的,拒絕並封鎖" + "您確定要拒絕加入此聊天室的邀請嗎?這也會防止 %1$s 聯絡您或邀請您加入聊天室。" + "拒絕邀請並封鎖" + "拒絕並封鎖" diff --git a/features/invite/impl/src/main/res/values/localazy.xml b/features/invite/impl/src/main/res/values/localazy.xml index 7c2c0194667..425602595f1 100644 --- a/features/invite/impl/src/main/res/values/localazy.xml +++ b/features/invite/impl/src/main/res/values/localazy.xml @@ -1,9 +1,18 @@ + "You will not see any messages or room invites from this user" + "Block user" + "Report this room to your account provider." + "Describe the reason to report…" + "Decline and block" "Are you sure you want to decline the invitation to join %1$s?" "Decline invite" "Are you sure you want to decline this private chat with %1$s?" "Decline chat" "No Invites" "%1$s (%2$s) invited you" + "Yes, decline & block" + "Are you sure you want to decline the invite to join this room? This will also prevent %1$s from contacting you or inviting you to rooms." + "Decline invite & block" + "Decline and block" diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt new file mode 100644 index 00000000000..eeefe688a17 --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultAcceptInviteTest.kt @@ -0,0 +1,94 @@ +/* + * 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.invite.impl + +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.features.invite.test.InMemorySeenInvitesStore +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom +import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner +import io.element.android.tests.testutils.lambda.any +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultAcceptInviteTest { + private val roomId = A_ROOM_ID + private val client = FakeMatrixClient() + private val seenInvitesStore = InMemorySeenInvitesStore(initialRoomIds = setOf(roomId)) + + private val clearMembershipNotificationForRoomLambda = + lambdaRecorder { _, _ -> } + private val notificationCleaner = + FakeNotificationCleaner(clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda) + + @Test + fun `accept invite success scenario`() = runTest { + val joinRoomLambda = + lambdaRecorder, JoinedRoom.Trigger, Result> { _, _, _ -> + Result.success(Unit) + } + + val acceptInvite = DefaultAcceptInvite( + client = client, + notificationCleaner = notificationCleaner, + joinRoom = FakeJoinRoom(lambda = joinRoomLambda), + seenInvitesStore = seenInvitesStore + ) + + val result = acceptInvite(roomId) + + assertThat(result.isSuccess).isTrue() + + assert(joinRoomLambda) + .isCalledOnce() + .with(value(roomId.toRoomIdOrAlias()), any(), any()) + + assert(clearMembershipNotificationForRoomLambda) + .isCalledOnce() + .with(value(client.sessionId), value(roomId)) + + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } + + @Test + fun `accept invite failure scenario`() = runTest { + val joinRoomLambda = + lambdaRecorder, JoinedRoom.Trigger, Result> { _, _, _ -> + Result.failure(RuntimeException("Join room failed")) + } + + val acceptInvite = DefaultAcceptInvite( + client = client, + notificationCleaner = notificationCleaner, + joinRoom = FakeJoinRoom(lambda = joinRoomLambda), + seenInvitesStore = seenInvitesStore + ) + + val result = acceptInvite(roomId) + + assertThat(result.isFailure).isTrue() + + assert(joinRoomLambda) + .isCalledOnce() + .with(value(roomId.toRoomIdOrAlias()), any(), any()) + + assert(clearMembershipNotificationForRoomLambda).isNeverCalled() + + assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomId) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt new file mode 100644 index 00000000000..cb07bd5191f --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/DefaultDeclineInviteTest.kt @@ -0,0 +1,178 @@ +/* + * 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.invite.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.test.InMemorySeenInvitesStore +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner +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 kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultDeclineInviteTest { + private val roomId = A_ROOM_ID + private val inviter = aRoomMember() + private val seenInvitesStore = InMemorySeenInvitesStore(initialRoomIds = setOf(roomId)) + private val clearMembershipNotificationForRoomLambda = + lambdaRecorder { _, _ -> } + private val notificationCleaner = + FakeNotificationCleaner(clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda) + + private val successLeaveRoomLambda = lambdaRecorder> { -> Result.success(Unit) } + private val successIgnoreUserLambda = + lambdaRecorder> { _ -> Result.success(Unit) } + private val successReportRoomLambda = + lambdaRecorder> { _ -> Result.success(Unit) } + + private val failureLeaveRoomLambda = + lambdaRecorder> { -> Result.failure(Exception("Leave room error")) } + private val failureIgnoreUserLambda = + lambdaRecorder> { _ -> Result.failure(Exception("Ignore user error")) } + private val failureReportRoomLambda = + lambdaRecorder> { _ -> Result.failure(Exception("Report room error")) } + + @Test + fun `decline invite, block=false, report=false, all success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + + val result = + declineInvite(roomId, blockUser = false, reportRoom = false, reportReason = null) + + assertThat(result.isSuccess).isTrue() + + assert(clearMembershipNotificationForRoomLambda) + .isCalledOnce() + .with(value(client.sessionId), value(roomId)) + + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } + + @Test + fun `decline invite, block=true, report=true, all success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda, + initialRoomInfo = aRoomInfo(inviter = inviter) + ) + val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null) + + assertThat(result.isSuccess).isTrue() + + assert(clearMembershipNotificationForRoomLambda) + .isCalledOnce() + .with(value(client.sessionId), value(roomId)) + + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } + + @Test + fun `decline invite, block=true, report=true, decline invite failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = failureLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null) + + assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.DeclineInviteFailed) + + assert(clearMembershipNotificationForRoomLambda) + .isNeverCalled() + + assertThat(seenInvitesStore.seenRoomIds().first()).isNotEmpty() + } + + @Test + fun `decline invite, block=true, report=true, ignore user failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda, + initialRoomInfo = aRoomInfo(inviter = inviter) + ) + val client = FakeMatrixClient(ignoreUserResult = failureIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null) + + assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.BlockUserFailed) + + assert(clearMembershipNotificationForRoomLambda).isCalledOnce() + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } + + @Test + fun `decline invite, block=true, report=true, report room failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = failureReportRoomLambda, + initialRoomInfo = aRoomInfo(inviter = inviter) + ) + val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply { + givenGetRoomResult(roomId, room) + } + val declineInvite = DefaultDeclineInvite( + client = client, + notificationCleaner = notificationCleaner, + seenInvitesStore = seenInvitesStore + ) + val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null) + + assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.ReportRoomFailed) + + assert(clearMembershipNotificationForRoomLambda).isCalledOnce() + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt new file mode 100644 index 00000000000..c8a427bea6a --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt @@ -0,0 +1,217 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.invite.impl.acceptdecline + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite +import io.element.android.features.invite.impl.AcceptInvite +import io.element.android.features.invite.impl.DeclineInvite +import io.element.android.features.invite.impl.fake.FakeAcceptInvite +import io.element.android.features.invite.impl.fake.FakeDeclineInvite +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +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.test.runTest +import org.junit.Rule +import org.junit.Test + +class AcceptDeclineInvitePresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createAcceptDeclineInvitePresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + } + } + + @Test + fun `present - declining invite cancel flow`() = runTest { + val presenter = createAcceptDeclineInvitePresenter() + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true) + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false)) + state.eventSink( + InternalAcceptDeclineInviteEvents.CancelDeclineInvite + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + } + } + + @Test + fun `present - declining invite error flow`() = runTest { + val declineInviteFailure = lambdaRecorder> { _, _, _, _ -> + Result.failure(DeclineInvite.Exception.DeclineInviteFailed) + } + val presenter = createAcceptDeclineInvitePresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteFailure) + ) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true) + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false)) + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false) + ) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink( + InternalAcceptDeclineInviteEvents.DismissDeclineError + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteFailure) + .isCalledOnce() + .with(value(A_ROOM_ID), value(false), value(false), value(null)) + } + + @Test + fun `present - declining invite success flow`() = runTest { + val declineInviteSuccess = lambdaRecorder> { roomId, _, _, _ -> Result.success(roomId) } + val presenter = createAcceptDeclineInvitePresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteSuccess) + ) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true) + ) + } + awaitItem().also { state -> + assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, blockUser = false)) + state.eventSink( + AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false) + ) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteSuccess) + .isCalledOnce() + .with(value(A_ROOM_ID), value(false), value(false), value(null)) + } + + @Test + fun `present - accepting invite error flow`() = runTest { + val acceptInviteFailure = lambdaRecorder> { roomId: RoomId -> + Result.failure(RuntimeException("Failed to accept invite")) + } + val presenter = createAcceptDeclineInvitePresenter( + acceptInvite = FakeAcceptInvite(lambda = acceptInviteFailure), + ) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.AcceptInvite(inviteData) + ) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink( + InternalAcceptDeclineInviteEvents.DismissAcceptError + ) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(acceptInviteFailure) + .isCalledOnce() + .with(value(A_ROOM_ID)) + } + + @Test + fun `present - accepting invite success flow`() = runTest { + val acceptInviteSuccess = lambdaRecorder> { roomId: RoomId -> Result.success(roomId) } + val presenter = createAcceptDeclineInvitePresenter( + acceptInvite = FakeAcceptInvite(lambda = acceptInviteSuccess) + ) + presenter.test { + val inviteData = anInviteData() + awaitItem().also { state -> + state.eventSink( + AcceptDeclineInviteEvents.AcceptInvite(inviteData) + ) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> + assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java) + } + cancelAndConsumeRemainingEvents() + } + acceptInviteSuccess.assertions() + .isCalledOnce() + .with(value(A_ROOM_ID)) + } + + private fun anInviteData( + roomId: RoomId = A_ROOM_ID, + name: String = A_ROOM_NAME, + isDm: Boolean = false, + ): InviteData { + return InviteData( + roomId = roomId, + roomName = name, + isDm = isDm, + ) + } + + private fun createAcceptDeclineInvitePresenter( + acceptInvite: AcceptInvite = FakeAcceptInvite(), + declineInvite: DeclineInvite = FakeDeclineInvite(), + ): AcceptDeclineInvitePresenter { + return AcceptDeclineInvitePresenter( + acceptInvite = acceptInvite, + declineInvite = declineInvite, + ) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt new file mode 100644 index 00000000000..c5d2f53c92c --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt @@ -0,0 +1,175 @@ +/* + * 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.invite.impl.declineandblock + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.impl.DeclineInvite +import io.element.android.features.invite.impl.fake.FakeDeclineInvite +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +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.test.runTest +import org.junit.Rule +import org.junit.Test + +class DeclineAndBlockPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createDeclineAndBlockPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.blockUser).isTrue() + assertThat(state.reportRoom).isFalse() + assertThat(state.reportReason).isEmpty() + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.canDecline).isTrue() + } + } + } + + @Test + fun `present - update form values`() = runTest { + val presenter = createDeclineAndBlockPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.reportRoom).isFalse() + assertThat(state.blockUser).isTrue() + assertThat(state.reportReason).isEmpty() + assertThat(state.canDecline).isTrue() + state.eventSink(DeclineAndBlockEvents.ToggleBlockUser) + } + awaitItem().also { state -> + assertThat(state.reportRoom).isFalse() + assertThat(state.blockUser).isFalse() + assertThat(state.reportReason).isEmpty() + assertThat(state.canDecline).isFalse() + state.eventSink(DeclineAndBlockEvents.ToggleReportRoom) + } + awaitItem().also { state -> + assertThat(state.reportRoom).isTrue() + assertThat(state.blockUser).isFalse() + assertThat(state.reportReason).isEmpty() + assertThat(state.canDecline).isFalse() + state.eventSink(DeclineAndBlockEvents.UpdateReportReason("Spam")) + } + awaitItem().also { state -> + assertThat(state.reportRoom).isTrue() + assertThat(state.blockUser).isFalse() + assertThat(state.reportReason).isEqualTo("Spam") + assertThat(state.canDecline).isTrue() + } + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun `present - declining invite success flow`() = runTest { + val declineInviteSuccess = lambdaRecorder> { roomId, _, _, _ -> Result.success(roomId) } + val presenter = createDeclineAndBlockPresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteSuccess) + ) + presenter.test { + awaitItem().also { state -> + state.eventSink(DeclineAndBlockEvents.Decline) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteSuccess) + .isCalledOnce() + .with(value(A_ROOM_ID), value(true), value(false), value("")) + } + + @Test + fun `present - declining invite error flow`() = runTest { + val declineInviteFailure = lambdaRecorder> { _, _, _, _ -> + Result.failure(DeclineInvite.Exception.DeclineInviteFailed) + } + val presenter = createDeclineAndBlockPresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteFailure) + ) + presenter.test { + awaitItem().also { state -> + state.eventSink(DeclineAndBlockEvents.Decline) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) + } + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteFailure) + .isCalledOnce() + .with(value(A_ROOM_ID), value(true), value(false), value("")) + } + + @Test + fun `present - block user error flow`() = runTest { + val declineInviteFailure = lambdaRecorder> { _, _, _, _ -> + Result.failure(DeclineInvite.Exception.BlockUserFailed) + } + val presenter = createDeclineAndBlockPresenter( + declineInvite = FakeDeclineInvite(lambda = declineInviteFailure) + ) + presenter.test { + awaitItem().also { state -> + state.eventSink(DeclineAndBlockEvents.Decline) + } + assertThat(awaitItem().declineAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + cancelAndConsumeRemainingEvents() + } + assert(declineInviteFailure) + .isCalledOnce() + .with(value(A_ROOM_ID), value(true), value(false), value("")) + } + + private fun anInviteData( + roomId: RoomId = A_ROOM_ID, + name: String = A_ROOM_NAME, + isDm: Boolean = false, + ): InviteData { + return InviteData( + roomId = roomId, + roomName = name, + isDm = isDm, + ) + } + + private fun createDeclineAndBlockPresenter( + inviteData: InviteData = anInviteData(), + declineInvite: DeclineInvite = FakeDeclineInvite(), + snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), + ): DeclineAndBlockPresenter { + return DeclineAndBlockPresenter( + inviteData = inviteData, + declineInvite = declineInvite, + snackbarDispatcher = snackbarDispatcher, + ) + } +} 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 new file mode 100644 index 00000000000..60e00f8e35b --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt @@ -0,0 +1,124 @@ +/* + * 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.invite.impl.declineandblock + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.invite.impl.R +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DeclineAndBlockViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invoke the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + eventSink = eventsRecorder, + ), + onBackClick = it + ) + rule.pressBack() + } + } + + @Test + fun `clicking on decline when enabled emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + blockUser = true, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_decline) + eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline) + } + + @Test + fun `clicking on decline when disabled does not emit event`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + blockUser = false, + reportRoom = false, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_decline) + eventsRecorder.assertEmpty() + } + + @Test + fun `clicking on block option emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + blockUser = true, + eventSink = eventsRecorder, + ), + ) + 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`() { + val eventsRecorder = EventsRecorder() + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + reportRoom = true, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_report_room) + eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom) + } + + @Test + fun `typing text in the reason field emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setDeclineAndBlockView( + aDeclineAndBlockState( + reportRoom = true, + reportReason = "", + eventSink = eventsRecorder, + ), + ) + rule.onNodeWithText("").performTextInput("Spam!") + eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!")) + } +} + +private fun AndroidComposeTestRule.setDeclineAndBlockView( + state: DeclineAndBlockState, + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + DeclineAndBlockView( + state = state, + onBackClick = onBackClick, + ) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt new file mode 100644 index 00000000000..1ba95bc76ed --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeAcceptInvite.kt @@ -0,0 +1,21 @@ +/* + * 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.invite.impl.fake + +import io.element.android.features.invite.impl.AcceptInvite +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +class FakeAcceptInvite( + private val lambda: (RoomId) -> Result = { lambdaError() }, +) : AcceptInvite { + override suspend fun invoke(roomId: RoomId) = simulateLongTask { + lambda(roomId) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt new file mode 100644 index 00000000000..18b46dbd5f2 --- /dev/null +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/fake/FakeDeclineInvite.kt @@ -0,0 +1,21 @@ +/* + * 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.invite.impl.fake + +import io.element.android.features.invite.impl.DeclineInvite +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +class FakeDeclineInvite( + private val lambda: (RoomId, Boolean, Boolean, String?) -> Result = { _, _, _, _ -> lambdaError() }, +) : DeclineInvite { + override suspend fun invoke(roomId: RoomId, blockUser: Boolean, reportRoom: Boolean, reportReason: String?): Result = simulateLongTask { + lambda(roomId, blockUser, reportRoom, reportReason) + } +} diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt deleted file mode 100644 index 4b064e01084..00000000000 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.invite.impl.response - -import com.google.common.truth.Truth.assertThat -import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.ConfirmingDeclineInvite -import io.element.android.features.invite.api.response.InviteData -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.RoomIdOrAlias -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakePendingRoom -import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom -import io.element.android.libraries.push.api.notifications.NotificationCleaner -import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner -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.test.runTest -import org.junit.Rule -import org.junit.Test - -class AcceptDeclineInvitePresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state`() = runTest { - val presenter = createAcceptDeclineInvitePresenter() - presenter.test { - awaitItem().also { state -> - assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - } - } - } - - @Test - fun `present - declining invite cancel flow`() = runTest { - val presenter = createAcceptDeclineInvitePresenter() - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData)) - state.eventSink( - InternalAcceptDeclineInviteEvents.CancelDeclineInvite - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - } - } - } - - @Test - fun `present - declining invite error flow`() = runTest { - val declineInviteFailure = lambdaRecorder { -> - Result.failure(RuntimeException("Failed to leave room")) - } - val client = FakeMatrixClient().apply { - getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteFailure) - } - val presenter = createAcceptDeclineInvitePresenter(client = client) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData)) - state.eventSink( - InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId) - ) - } - assertThat(awaitItem().declineAction.isLoading()).isTrue() - awaitItem().also { state -> - assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) - state.eventSink( - InternalAcceptDeclineInviteEvents.DismissDeclineError - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - } - cancelAndConsumeRemainingEvents() - } - assert(declineInviteFailure).isCalledOnce() - } - - @Test - fun `present - declining invite success flow`() = runTest { - val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> - Result.success(Unit) - } - val fakeNotificationCleaner = FakeNotificationCleaner( - clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda - ) - val declineInviteSuccess = lambdaRecorder { -> - Result.success(Unit) - } - val client = FakeMatrixClient().apply { - getPendingRoomResults[A_ROOM_ID] = FakePendingRoom(declineInviteResult = declineInviteSuccess) - } - val presenter = createAcceptDeclineInvitePresenter( - client = client, - notificationCleaner = fakeNotificationCleaner, - ) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData)) - state.eventSink( - InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId) - ) - } - assertThat(awaitItem().declineAction.isLoading()).isTrue() - awaitItem().also { state -> - assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) - } - cancelAndConsumeRemainingEvents() - } - declineInviteSuccess.assertions().isCalledOnce() - clearMembershipNotificationForRoomLambda.assertions() - .isCalledOnce() - .with(value(A_SESSION_ID), value(A_ROOM_ID)) - } - - @Test - fun `present - accepting invite error flow`() = runTest { - val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List, _: JoinedRoom.Trigger -> - Result.failure(RuntimeException("Failed to join room $roomIdOrAlias")) - } - val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomFailure) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java) - state.eventSink( - InternalAcceptDeclineInviteEvents.DismissAcceptError - ) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - } - cancelAndConsumeRemainingEvents() - } - assert(joinRoomFailure) - .isCalledOnce() - .with( - value(A_ROOM_ID.toRoomIdOrAlias()), - value(emptyList()), - value(JoinedRoom.Trigger.Invite) - ) - } - - @Test - fun `present - accepting invite success flow`() = runTest { - val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> - Result.success(Unit) - } - val fakeNotificationCleaner = FakeNotificationCleaner( - clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda - ) - val joinRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: List, _: JoinedRoom.Trigger -> - Result.success(Unit) - } - val presenter = createAcceptDeclineInvitePresenter( - joinRoomLambda = joinRoomSuccess, - notificationCleaner = fakeNotificationCleaner, - ) - presenter.test { - val inviteData = anInviteData() - awaitItem().also { state -> - state.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(inviteData) - ) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) - } - awaitItem().also { state -> - assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java) - } - cancelAndConsumeRemainingEvents() - } - assert(joinRoomSuccess) - .isCalledOnce() - .with( - value(A_ROOM_ID.toRoomIdOrAlias()), - value(emptyList()), - value(JoinedRoom.Trigger.Invite) - ) - clearMembershipNotificationForRoomLambda.assertions() - .isCalledOnce() - .with(value(A_SESSION_ID), value(A_ROOM_ID)) - } - - private fun anInviteData( - roomId: RoomId = A_ROOM_ID, - name: String = A_ROOM_NAME, - isDm: Boolean = false - ): InviteData { - return InviteData( - roomId = roomId, - roomName = name, - isDm = isDm - ) - } - - private fun createAcceptDeclineInvitePresenter( - client: MatrixClient = FakeMatrixClient(), - joinRoomLambda: (RoomIdOrAlias, List, JoinedRoom.Trigger) -> Result = { _, _, _ -> - Result.success(Unit) - }, - notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), - ): AcceptDeclineInvitePresenter { - return AcceptDeclineInvitePresenter( - client = client, - joinRoom = FakeJoinRoom(joinRoomLambda), - notificationCleaner = notificationCleaner, - ) - } -} diff --git a/features/invite/test/build.gradle.kts b/features/invite/test/build.gradle.kts new file mode 100644 index 00000000000..e504873c3b8 --- /dev/null +++ b/features/invite/test/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.invite.test" +} + +dependencies { + implementation(libs.coroutines.core) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrix.test) + api(projects.features.invite.api) +} diff --git a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InMemorySeenInvitesStore.kt b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InMemorySeenInvitesStore.kt new file mode 100644 index 00000000000..25db72532e7 --- /dev/null +++ b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InMemorySeenInvitesStore.kt @@ -0,0 +1,33 @@ +/* + * 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.invite.test + +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class InMemorySeenInvitesStore( + initialRoomIds: Set = emptySet(), +) : SeenInvitesStore { + private val roomIds = MutableStateFlow(initialRoomIds) + + override fun seenRoomIds(): Flow> = roomIds + + override suspend fun markAsSeen(roomId: RoomId) { + roomIds.value += roomId + } + + override suspend fun markAsUnSeen(roomId: RoomId) { + roomIds.value -= roomId + } + + override suspend fun clear() { + roomIds.value = emptySet() + } +} diff --git a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt new file mode 100644 index 00000000000..62e6d8b3222 --- /dev/null +++ b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/InviteData.kt @@ -0,0 +1,23 @@ +/* + * 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.invite.test + +import io.element.android.features.invite.api.InviteData +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME + +fun anInviteData( + roomId: RoomId = A_ROOM_ID, + roomName: String = A_ROOM_NAME, + isDm: Boolean = false, +) = InviteData( + roomId = roomId, + roomName = roomName, + isDm = isDm, +) diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index 65635f1907b..e195efd1bca 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation(projects.features.invite.api) implementation(projects.features.roomdirectory.api) implementation(projects.services.analytics.api) + implementation(projects.libraries.preferences.api) + implementation(projects.appconfig) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -42,8 +44,10 @@ dependencies { testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(projects.features.invite.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.tests.testutils) testImplementation(libs.androidx.compose.ui.test.junit) + testImplementation(projects.libraries.preferences.test) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt index 224ea19291d..e28b2affe86 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/DefaultJoinRoomEntryPoint.kt @@ -18,7 +18,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultJoinRoomEntryPoint @Inject constructor() : JoinRoomEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: JoinRoomEntryPoint.Inputs): Node { - return parentNode.createNode( + return parentNode.createNode( buildContext = buildContext, plugins = listOf(inputs) ) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt index 5550d06bac9..323b556c38d 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt @@ -7,13 +7,17 @@ package io.element.android.features.joinroom.impl +import io.element.android.features.invite.api.InviteData + sealed interface JoinRoomEvents { data object RetryFetchingContent : JoinRoomEvents + data object DismissErrorAndHideContent : JoinRoomEvents data object JoinRoom : JoinRoomEvents data object KnockRoom : JoinRoomEvents + data object ForgetRoom : JoinRoomEvents data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents data class UpdateKnockMessage(val message: String) : JoinRoomEvents data object ClearActionStates : JoinRoomEvents - data object AcceptInvite : JoinRoomEvents - data object DeclineInvite : JoinRoomEvents + data class AcceptInvite(val inviteData: InviteData) : JoinRoomEvents + data class DeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : JoinRoomEvents } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt new file mode 100644 index 00000000000..ecd4c920c8d --- /dev/null +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt @@ -0,0 +1,101 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.joinroom.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.node.node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView +import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint +import io.element.android.features.joinroom.api.JoinRoomEntryPoint +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class JoinRoomFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: JoinRoomPresenter.Factory, + private val acceptDeclineInviteView: AcceptDeclineInviteView, + private val declineAndBlockEntryPoint: DeclineInviteAndBlockEntryPoint +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + private val inputs: JoinRoomEntryPoint.Inputs = inputs() + private val presenter = presenterFactory.create( + inputs.roomId, + inputs.roomIdOrAlias, + inputs.roomDescription, + inputs.serverNames, + inputs.trigger, + ) + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data class DeclineInviteAndBlockUser(val inviteData: InviteData) : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.DeclineInviteAndBlockUser -> declineAndBlockEntryPoint.createNode(this, buildContext, navTarget.inviteData) + NavTarget.Root -> rootNode(buildContext) + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView(modifier) + } + + private fun rootNode(buildContext: BuildContext): Node { + return node(buildContext) { modifier -> + val state = presenter.present() + JoinRoomView( + state = state, + onBackClick = ::navigateUp, + onJoinSuccess = {}, + onForgetSuccess = ::navigateUp, + onCancelKnockSuccess = {}, + onKnockSuccess = {}, + onDeclineInviteAndBlockUser = { + backstack.push( + NavTarget.DeclineInviteAndBlockUser(it) + ) + }, + modifier = modifier + ) + acceptDeclineInviteView.Render( + state = state.acceptDeclineInviteState, + onAcceptInviteSuccess = {}, + onDeclineInviteSuccess = {}, + modifier = Modifier + ) + } + } +} diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt deleted file mode 100644 index ec910ee29d4..00000000000 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.joinroom.impl - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.invite.api.response.AcceptDeclineInviteView -import io.element.android.features.joinroom.api.JoinRoomEntryPoint -import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.SessionScope - -@ContributesNode(SessionScope::class) -class JoinRoomNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - presenterFactory: JoinRoomPresenter.Factory, - private val acceptDeclineInviteView: AcceptDeclineInviteView, -) : Node(buildContext, plugins = plugins) { - private val inputs: JoinRoomEntryPoint.Inputs = inputs() - private val presenter = presenterFactory.create( - inputs.roomId, - inputs.roomIdOrAlias, - inputs.roomDescription, - inputs.serverNames, - inputs.trigger, - ) - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - JoinRoomView( - state = state, - onBackClick = ::navigateUp, - onJoinSuccess = ::navigateUp, - onCancelKnockSuccess = {}, - onKnockSuccess = {}, - modifier = modifier - ) - acceptDeclineInviteView.Render( - state = state.acceptDeclineInviteState, - onAcceptInvite = {}, - onDeclineInvite = {}, - modifier = Modifier - ) - } -} 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 3cb7b5f82a1..6206c2426a3 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 @@ -9,6 +9,7 @@ package io.element.android.features.joinroom.impl import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -22,26 +23,31 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.InviteData +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.toInviteData import io.element.android.features.joinroom.impl.di.CancelKnockRoom +import io.element.android.features.joinroom.impl.di.ForgetRoom import io.element.android.features.joinroom.impl.di.KnockRoom import io.element.android.features.roomdirectory.api.RoomDescription 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.coroutine.mapState import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias -import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias -import io.element.android.libraries.matrix.api.getRoomInfoFlow +import io.element.android.libraries.matrix.api.exception.ClientException +import io.element.android.libraries.matrix.api.exception.ErrorKind import io.element.android.libraries.matrix.api.room.CurrentUserMembership -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.RoomInfo +import io.element.android.libraries.matrix.api.room.RoomMember 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 import io.element.android.libraries.matrix.ui.model.toInviteSender import kotlinx.coroutines.CoroutineScope @@ -58,8 +64,10 @@ class JoinRoomPresenter @AssistedInject constructor( private val joinRoom: JoinRoom, private val knockRoom: KnockRoom, private val cancelKnockRoom: CancelKnockRoom, + private val forgetRoom: ForgetRoom, private val acceptDeclineInvitePresenter: Presenter, private val buildMeta: BuildMeta, + private val seenInvitesStore: SeenInvitesStore, ) : Presenter { interface Factory { fun create( @@ -75,35 +83,78 @@ class JoinRoomPresenter @AssistedInject constructor( override fun present(): JoinRoomState { val coroutineScope = rememberCoroutineScope() var retryCount by remember { mutableIntStateOf(0) } - val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty()) + val roomInfo by remember { + matrixClient.getRoomInfoFlow(roomId) + }.collectAsState(initial = Optional.empty()) val joinAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val knockAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val cancelKnockAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + val forgetRoomAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } var knockMessage by rememberSaveable { mutableStateOf("") } + var isDismissingContent by remember { mutableStateOf(false) } + val hideInviteAvatars by remember { + matrixClient + .mediaPreviewService() + .mediaPreviewConfigFlow + .mapState { config -> config.hideInviteAvatar } + }.collectAsState() + val canReportRoom by produceState(false) { value = matrixClient.canReportRoom() } + val contentState by produceState( - initialValue = ContentState.Loading(roomIdOrAlias), + initialValue = ContentState.Loading, key1 = roomInfo, key2 = retryCount, + key3 = isDismissingContent, ) { when { + isDismissingContent -> value = ContentState.Dismissing roomInfo.isPresent -> { - value = roomInfo.get().toContentState() + val notJoinedRoom = matrixClient.getRoomPreview(roomIdOrAlias, serverNames).getOrNull() + val (sender, reason) = when (roomInfo.get().currentUserMembership) { + CurrentUserMembership.BANNED -> { + // Workaround to get info about the sender for banned rooms + // TODO re-do this once we have a better API in the SDK + val membershipDetails = notJoinedRoom?.membershipDetails()?.getOrNull() + membershipDetails?.senderMember to membershipDetails?.currentUserMember?.membershipChangeReason + } + CurrentUserMembership.INVITED -> { + roomInfo.get().inviter to null + } + else -> null to null + } + val joinedMembersCountOverride = notJoinedRoom?.previewInfo?.numberOfJoinedMembers + value = roomInfo.get().toContentState( + membershipSender = sender, + joinedMembersCountOverride = joinedMembersCountOverride, + reason = reason, + ) } roomDescription.isPresent -> { value = roomDescription.get().toContentState() } else -> { - value = ContentState.Loading(roomIdOrAlias) - val result = matrixClient.getRoomPreviewInfo(roomIdOrAlias, serverNames) + value = ContentState.Loading + val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames) value = result.fold( - onSuccess = { previewInfo -> - previewInfo.toContentState() + onSuccess = { preview -> + val membershipInfo = when (preview.previewInfo.membership) { + CurrentUserMembership.INVITED, + CurrentUserMembership.BANNED, + CurrentUserMembership.KNOCKED -> { + preview.membershipDetails().getOrNull() + } + else -> null + } + preview.previewInfo.toContentState( + senderMember = membershipInfo?.senderMember, + reason = membershipInfo?.currentUserMember?.membershipChangeReason, + ) }, onFailure = { throwable -> - if (throwable.message?.contains("403") == true) { - ContentState.UnknownRoom(roomIdOrAlias) + if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) { + ContentState.UnknownRoom } else { - ContentState.Failure(roomIdOrAlias, throwable) + ContentState.Failure(throwable) } } ) @@ -112,20 +163,22 @@ class JoinRoomPresenter @AssistedInject constructor( } val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() + LaunchedEffect(contentState) { + contentState.markRoomInviteAsSeen() + } + fun handleEvents(event: JoinRoomEvents) { when (event) { JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction) is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage) - JoinRoomEvents.AcceptInvite -> { - val inviteData = contentState.toInviteData() ?: return + is JoinRoomEvents.AcceptInvite -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(inviteData) + AcceptDeclineInviteEvents.AcceptInvite(event.inviteData) ) } - JoinRoomEvents.DeclineInvite -> { - val inviteData = contentState.toInviteData() ?: return + is JoinRoomEvents.DeclineInvite -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(inviteData) + AcceptDeclineInviteEvents.DeclineInvite(invite = event.inviteData, blockUser = event.blockUser, shouldConfirm = true) ) } is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction) @@ -136,21 +189,30 @@ class JoinRoomPresenter @AssistedInject constructor( knockAction.value = AsyncAction.Uninitialized joinAction.value = AsyncAction.Uninitialized cancelKnockAction.value = AsyncAction.Uninitialized + forgetRoomAction.value = AsyncAction.Uninitialized } is JoinRoomEvents.UpdateKnockMessage -> { knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH) } + JoinRoomEvents.DismissErrorAndHideContent -> { + isDismissingContent = true + } + JoinRoomEvents.ForgetRoom -> coroutineScope.forgetRoom(forgetRoomAction) } } return JoinRoomState( + roomIdOrAlias = roomIdOrAlias, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, joinAction = joinAction.value, knockAction = knockAction.value, + forgetAction = forgetRoomAction.value, cancelKnockAction = cancelKnockAction.value, applicationName = buildMeta.applicationName, knockMessage = knockMessage, + hideInviteAvatars = hideInviteAvatars, + canReportRoom = canReportRoom, eventSink = ::handleEvents ) } @@ -180,9 +242,21 @@ class JoinRoomPresenter @AssistedInject constructor( } } } + + private fun CoroutineScope.forgetRoom(forgetAction: MutableState>) = launch { + forgetAction.runUpdatingState { + forgetRoom.invoke(roomId) + } + } + + private suspend fun ContentState.markRoomInviteAsSeen() { + if ((this as? ContentState.Loaded)?.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited != null) { + seenInvitesStore.markAsSeen(roomId) + } + } } -private fun RoomPreviewInfo.toContentState(): ContentState { +private fun RoomPreviewInfo.toContentState(senderMember: RoomMember?, reason: String?): ContentState { return ContentState.Loaded( roomId = roomId, name = name, @@ -192,12 +266,16 @@ private fun RoomPreviewInfo.toContentState(): ContentState { isDm = false, roomType = roomType, roomAvatarUrl = avatarUrl, - joinAuthorisationStatus = when { - // Note when isInvited, roomInfo will be used, so if this happen, it will be temporary. - isInvited -> JoinAuthorisationStatus.IsInvited(null) - canKnock -> JoinAuthorisationStatus.CanKnock - isPublic -> JoinAuthorisationStatus.CanJoin - else -> JoinAuthorisationStatus.Unknown + joinAuthorisationStatus = when (membership) { + CurrentUserMembership.INVITED -> { + JoinAuthorisationStatus.IsInvited( + inviteData = toInviteData(), + inviteSender = senderMember?.toInviteSender() + ) + } + CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason) + CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked + else -> joinRule.toJoinAuthorisationStatus() } ) } @@ -222,36 +300,43 @@ internal fun RoomDescription.toContentState(): ContentState { } @VisibleForTesting -internal fun MatrixRoomInfo.toContentState(): ContentState { +internal fun RoomInfo.toContentState( + membershipSender: RoomMember?, + joinedMembersCountOverride: Long?, + reason: String?, +): ContentState { return ContentState.Loaded( roomId = id, name = name, topic = topic, alias = canonicalAlias, - numberOfMembers = activeMembersCount, + numberOfMembers = joinedMembersCountOverride ?: joinedMembersCount, isDm = isDm, roomType = if (isSpace) RoomType.Space else RoomType.Room, roomAvatarUrl = avatarUrl, - joinAuthorisationStatus = when { - currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited( - inviteSender = inviter?.toInviteSender() + joinAuthorisationStatus = when (currentUserMembership) { + CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited( + inviteData = toInviteData(), + inviteSender = membershipSender?.toInviteSender(), ) - currentUserMembership == CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked - isPublic -> JoinAuthorisationStatus.CanJoin - else -> JoinAuthorisationStatus.Unknown + CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned( + banSender = membershipSender?.toInviteSender(), + reason = reason, + ) + CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked + else -> joinRule.toJoinAuthorisationStatus() } ) } -@VisibleForTesting -internal fun ContentState.toInviteData(): InviteData? { +private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus { return when (this) { - is ContentState.Loaded -> InviteData( - roomId = roomId, - // Note: name should not be null at this point, but use Id just in case... - roomName = name ?: roomId.value, - isDm = isDm - ) - else -> null + JoinRule.Knock, + is JoinRule.KnockRestricted -> JoinAuthorisationStatus.CanKnock + JoinRule.Invite, + JoinRule.Private -> JoinAuthorisationStatus.NeedInvite + is JoinRule.Restricted -> JoinAuthorisationStatus.Restricted + JoinRule.Public -> JoinAuthorisationStatus.CanJoin + else -> JoinAuthorisationStatus.Unknown } } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index 85538d6d88e..5b9f8007a3a 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -8,7 +8,8 @@ package io.element.android.features.joinroom.impl import androidx.compose.runtime.Immutable -import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState 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 @@ -16,36 +17,60 @@ 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 import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.ui.model.InviteSender internal const val MAX_KNOCK_MESSAGE_LENGTH = 500 @Immutable data class JoinRoomState( + val roomIdOrAlias: RoomIdOrAlias, val contentState: ContentState, val acceptDeclineInviteState: AcceptDeclineInviteState, val joinAction: AsyncAction, val knockAction: AsyncAction, + val forgetAction: AsyncAction, val cancelKnockAction: AsyncAction, - val applicationName: String, + private val applicationName: String, val knockMessage: String, + val hideInviteAvatars: Boolean, + val canReportRoom: Boolean, val eventSink: (JoinRoomEvents) -> Unit ) { + val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoom.Failures.UnauthorizedJoin val joinAuthorisationStatus = when (contentState) { - // Use the join authorisation status from the loaded content state - is ContentState.Loaded -> contentState.joinAuthorisationStatus - // Assume that if the room is unknown, the user can join it - is ContentState.UnknownRoom -> JoinAuthorisationStatus.CanJoin - // Otherwise assume that the user can't join the room - else -> JoinAuthorisationStatus.Unknown + is ContentState.Loaded -> { + when { + contentState.roomType == RoomType.Space -> { + JoinAuthorisationStatus.IsSpace(applicationName) + } + isJoinActionUnauthorized -> { + JoinAuthorisationStatus.Unauthorized + } + else -> { + contentState.joinAuthorisationStatus + } + } + } + is ContentState.UnknownRoom -> { + if (isJoinActionUnauthorized) { + JoinAuthorisationStatus.Unauthorized + } else { + JoinAuthorisationStatus.Unknown + } + } + else -> JoinAuthorisationStatus.None } + + val hideAvatarsImages = hideInviteAvatars && joinAuthorisationStatus is JoinAuthorisationStatus.IsInvited } @Immutable sealed interface ContentState { - data class Loading(val roomIdOrAlias: RoomIdOrAlias) : ContentState - data class Failure(val roomIdOrAlias: RoomIdOrAlias, val error: Throwable) : ContentState - data class UnknownRoom(val roomIdOrAlias: RoomIdOrAlias) : ContentState + data object Dismissing : ContentState + data object Loading : ContentState + data class Failure(val error: Throwable) : ContentState + data object UnknownRoom : ContentState data class Loaded( val roomId: RoomId, val name: String?, @@ -71,9 +96,15 @@ sealed interface ContentState { } sealed interface JoinAuthorisationStatus { - data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus + data object None : JoinAuthorisationStatus + data class IsSpace(val applicationName: String) : JoinAuthorisationStatus + data class IsInvited(val inviteData: InviteData, val inviteSender: InviteSender?) : JoinAuthorisationStatus + data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus data object IsKnocked : JoinAuthorisationStatus data object CanKnock : JoinAuthorisationStatus data object CanJoin : JoinAuthorisationStatus + data object NeedInvite : JoinAuthorisationStatus + data object Restricted : JoinAuthorisationStatus data object Unknown : JoinAuthorisationStatus + data object Unauthorized : JoinAuthorisationStatus } 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 d34d3773944..8f891fe6453 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 @@ -8,8 +8,9 @@ package io.element.android.features.joinroom.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.anAcceptDeclineInviteState +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState 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 @@ -18,17 +19,19 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.ui.model.InviteSender open class JoinRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aJoinRoomState( - contentState = aLoadingContentState() + contentState = ContentState.Loading ), aJoinRoomState( - contentState = anUnknownContentState() + contentState = ContentState.UnknownRoom ), aJoinRoomState( contentState = aLoadedContentState( @@ -41,19 +44,32 @@ open class JoinRoomStateProvider : PreviewParameterProvider { contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin) ), aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)) + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin), + joinAction = AsyncAction.Failure(JoinRoom.Failures.UnauthorizedJoin) + ), + aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin), + joinAction = AsyncAction.Failure(ClientException.Generic("Something went wrong", null)) ), aJoinRoomState( contentState = aLoadedContentState( - numberOfMembers = 123, - joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(anInviteSender()), + joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited( + inviteData = anInviteData(), + inviteSender = null, + ) ) ), aJoinRoomState( - contentState = aFailureContentState() + contentState = aLoadedContentState( + numberOfMembers = 123, + joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited( + inviteData = anInviteData(), + inviteSender = anInviteSender(), + ), + ) ), aJoinRoomState( - contentState = aFailureContentState(roomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias()) + contentState = aFailureContentState() ), aJoinRoomState( contentState = aLoadedContentState( @@ -98,23 +114,42 @@ open class JoinRoomStateProvider : PreviewParameterProvider { name = "A knocked Room", joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked ) - ) + ), + aJoinRoomState( + contentState = aLoadedContentState( + name = "A private room", + joinAuthorisationStatus = JoinAuthorisationStatus.NeedInvite + ) + ), + aJoinRoomState( + contentState = aLoadedContentState( + name = "A banned room", + joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned( + banSender = InviteSender( + userId = UserId("@alice:domain"), + displayName = "Alice", + avatarData = AvatarData("alice", "Alice", size = AvatarSize.InviteSender), + membershipChangeReason = "spamming" + ), + reason = "spamming", + ), + ) + ), + aJoinRoomState( + contentState = aLoadedContentState( + name = "A restricted room", + joinAuthorisationStatus = JoinAuthorisationStatus.Restricted, + ) + ), ) } -fun aFailureContentState( - roomIdOrAlias: RoomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias() -): ContentState { +fun aFailureContentState(): ContentState { return ContentState.Failure( - roomIdOrAlias = roomIdOrAlias, error = Exception("Error"), ) } -fun anUnknownContentState(roomId: RoomId = A_ROOM_ID) = ContentState.UnknownRoom(roomId.toRoomIdOrAlias()) - -fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(roomId.toRoomIdOrAlias()) - fun aLoadedContentState( roomId: RoomId = A_ROOM_ID, name: String? = "Element X android", @@ -124,7 +159,7 @@ fun aLoadedContentState( isDm: Boolean = false, roomType: RoomType = RoomType.Room, roomAvatarUrl: String? = null, - joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown + joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown, ) = ContentState.Loaded( roomId = roomId, name = name, @@ -138,32 +173,62 @@ fun aLoadedContentState( ) fun aJoinRoomState( + roomIdOrAlias: RoomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias(), contentState: ContentState = aLoadedContentState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), joinAction: AsyncAction = AsyncAction.Uninitialized, knockAction: AsyncAction = AsyncAction.Uninitialized, + forgetAction: AsyncAction = AsyncAction.Uninitialized, cancelKnockAction: AsyncAction = AsyncAction.Uninitialized, knockMessage: String = "", + hideInviteAvatars: Boolean = false, + canReportRoom: Boolean = true, eventSink: (JoinRoomEvents) -> Unit = {} ) = JoinRoomState( + roomIdOrAlias = roomIdOrAlias, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, joinAction = joinAction, knockAction = knockAction, cancelKnockAction = cancelKnockAction, + forgetAction = forgetAction, applicationName = "AppName", knockMessage = knockMessage, + hideInviteAvatars = hideInviteAvatars, + canReportRoom = canReportRoom, eventSink = eventSink ) +internal fun anAcceptDeclineInviteState( + acceptAction: AsyncAction = AsyncAction.Uninitialized, + declineAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (AcceptDeclineInviteEvents) -> Unit = {} +) = AcceptDeclineInviteState( + acceptAction = acceptAction, + declineAction = declineAction, + eventSink = eventSink, +) + internal fun anInviteSender( userId: UserId = UserId("@bob:domain"), displayName: String = "Bob", avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender), + membershipChangeReason: String? = null, ) = InviteSender( userId = userId, displayName = displayName, avatarData = avatarData, + membershipChangeReason = membershipChangeReason, +) + +internal fun anInviteData( + roomId: RoomId = A_ROOM_ID, + roomName: String = "Room name", + isDm: Boolean = false, +) = InviteData( + roomId = roomId, + roomName = roomName, + isDm = isDm, ) private val A_ROOM_ID = RoomId("!exa:matrix.org") 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 49c7b5742b8..cdeac0afe94 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 @@ -19,24 +19,26 @@ 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.sizeIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme 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.res.stringResource +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.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.features.invite.api.InviteData import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom @@ -47,24 +49,28 @@ import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubti import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage -import io.element.android.libraries.designsystem.background.LightGradientBackground +import io.element.android.libraries.designsystem.components.Announcement +import io.element.android.libraries.designsystem.components.AnnouncementType import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.SuperButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.RetryDialog 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.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.OutlinedButton 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.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.RoomIdOrAlias -import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.ui.components.InviteSenderView import io.element.android.libraries.ui.strings.CommonStrings @@ -74,35 +80,48 @@ fun JoinRoomView( onBackClick: () -> Unit, onJoinSuccess: () -> Unit, onKnockSuccess: () -> Unit, + onForgetSuccess: () -> Unit, onCancelKnockSuccess: () -> Unit, + onDeclineInviteAndBlockUser: (InviteData) -> Unit, modifier: Modifier = Modifier, ) { Box( modifier = modifier.fillMaxSize(), ) { - LightGradientBackground() HeaderFooterPage( containerColor = Color.Transparent, - paddingValues = PaddingValues(16.dp), + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 32.dp + ), topBar = { - JoinRoomTopBar(contentState = state.contentState, onBackClick = onBackClick) + JoinRoomTopBar( + contentState = state.contentState, + hideAvatarImage = state.hideAvatarsImages, + onBackClick = onBackClick, + ) }, content = { JoinRoomContent( + roomIdOrAlias = state.roomIdOrAlias, contentState = state.contentState, - applicationName = state.applicationName, knockMessage = state.knockMessage, + hideAvatarsImages = state.hideAvatarsImages, onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) }, ) }, footer = { JoinRoomFooter( - state = state, - onAcceptInvite = { - state.eventSink(JoinRoomEvents.AcceptInvite) + joinAuthorisationStatus = state.joinAuthorisationStatus, + onAcceptInvite = { inviteData -> + state.eventSink(JoinRoomEvents.AcceptInvite(inviteData)) }, - onDeclineInvite = { - state.eventSink(JoinRoomEvents.DeclineInvite) + onDeclineInvite = { inviteData, blockUser -> + if (state.canReportRoom && blockUser) { + onDeclineInviteAndBlockUser(inviteData) + } else { + state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, blockUser = blockUser)) + } }, onJoinRoom = { state.eventSink(JoinRoomEvents.JoinRoom) @@ -113,31 +132,55 @@ fun JoinRoomView( onCancelKnock = { state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true)) }, - onRetry = { - state.eventSink(JoinRoomEvents.RetryFetchingContent) + onForgetRoom = { + state.eventSink(JoinRoomEvents.ForgetRoom) }, onGoBack = onBackClick, ) } ) } + if (state.contentState is ContentState.Failure) { + RetryDialog( + title = stringResource(R.string.screen_join_room_loading_alert_title), + content = stringResource(CommonStrings.error_network_or_server_issue), + onRetry = { state.eventSink(JoinRoomEvents.RetryFetchingContent) }, + onDismiss = { + state.eventSink(JoinRoomEvents.DismissErrorAndHideContent) + onBackClick() + } + ) + } + // This particular error is shown directly in the footer + if (!state.isJoinActionUnauthorized) { + AsyncActionView( + async = state.joinAction, + errorTitle = { stringResource(CommonStrings.common_something_went_wrong) }, + errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) }, + onSuccess = { onJoinSuccess() }, + onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) }, + ) + } AsyncActionView( - async = state.joinAction, - onSuccess = { onJoinSuccess() }, + async = state.knockAction, + errorTitle = { stringResource(CommonStrings.common_something_went_wrong) }, + errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) }, + onSuccess = { onKnockSuccess() }, onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) }, ) AsyncActionView( - async = state.knockAction, - onSuccess = { onKnockSuccess() }, + async = state.forgetAction, + errorTitle = { stringResource(CommonStrings.common_something_went_wrong) }, + errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) }, + onSuccess = { onForgetSuccess() }, onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) }, ) AsyncActionView( async = state.cancelKnockAction, onSuccess = { onCancelKnockSuccess() }, onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) }, - errorMessage = { - stringResource(CommonStrings.error_unknown) - }, + errorTitle = { stringResource(CommonStrings.common_something_went_wrong) }, + errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) }, confirmationDialog = { ConfirmationDialog( content = stringResource(R.string.screen_join_room_cancel_knock_alert_description), @@ -153,95 +196,196 @@ fun JoinRoomView( @Composable private fun JoinRoomFooter( - state: JoinRoomState, - onAcceptInvite: () -> Unit, - onDeclineInvite: () -> Unit, + joinAuthorisationStatus: JoinAuthorisationStatus, + onAcceptInvite: (InviteData) -> Unit, + onDeclineInvite: (InviteData, Boolean) -> Unit, onJoinRoom: () -> Unit, onKnockRoom: () -> Unit, onCancelKnock: () -> Unit, - onRetry: () -> Unit, + onForgetRoom: () -> Unit, onGoBack: () -> Unit, modifier: Modifier = Modifier, ) { Box( modifier = modifier - .fillMaxWidth() - .padding(top = 8.dp) + .fillMaxWidth() + .padding(top = 8.dp) ) { - if (state.contentState is ContentState.Failure) { - Button( - text = stringResource(CommonStrings.action_retry), - onClick = onRetry, - modifier = Modifier.fillMaxWidth(), - size = ButtonSize.Large, - ) - } else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) { - Button( - text = stringResource(CommonStrings.action_go_back), - onClick = onGoBack, - modifier = Modifier.fillMaxWidth(), - size = ButtonSize.Large, - ) - } else { - val joinAuthorisationStatus = state.joinAuthorisationStatus - when (joinAuthorisationStatus) { - is JoinAuthorisationStatus.IsInvited -> { + when (joinAuthorisationStatus) { + is JoinAuthorisationStatus.IsInvited -> { + Column { ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) { OutlinedButton( text = stringResource(CommonStrings.action_decline), - onClick = onDeclineInvite, + onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, false) }, modifier = Modifier.weight(1f), size = ButtonSize.LargeLowPadding, ) Button( text = stringResource(CommonStrings.action_accept), - onClick = onAcceptInvite, + onClick = { onAcceptInvite(joinAuthorisationStatus.inviteData) }, modifier = Modifier.weight(1f), size = ButtonSize.LargeLowPadding, ) } - } - JoinAuthorisationStatus.CanJoin -> { - SuperButton( - onClick = onJoinRoom, + Spacer(modifier = Modifier.height(24.dp)) + TextButton( + text = stringResource(R.string.screen_join_room_decline_and_block_button_title), + onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, true) }, modifier = Modifier.fillMaxWidth(), - buttonSize = ButtonSize.Large, - ) { - Text( - text = stringResource(R.string.screen_join_room_join_action), - ) - } + destructive = true + ) } - JoinAuthorisationStatus.CanKnock -> { - SuperButton( - onClick = onKnockRoom, - modifier = Modifier.fillMaxWidth(), - buttonSize = ButtonSize.Large, - ) { - Text( - text = stringResource(R.string.screen_join_room_knock_action), - ) - } + } + JoinAuthorisationStatus.CanJoin -> { + SuperButton( + onClick = onJoinRoom, + modifier = Modifier.fillMaxWidth(), + buttonSize = ButtonSize.Large, + ) { + Text( + text = stringResource(R.string.screen_join_room_join_action), + ) } - JoinAuthorisationStatus.IsKnocked -> { - OutlinedButton( - text = stringResource(R.string.screen_join_room_cancel_knock_action), - onClick = onCancelKnock, - modifier = Modifier.fillMaxWidth(), - size = ButtonSize.Large, + } + JoinAuthorisationStatus.CanKnock -> { + SuperButton( + onClick = onKnockRoom, + modifier = Modifier.fillMaxWidth(), + buttonSize = ButtonSize.Large, + ) { + Text( + text = stringResource(R.string.screen_join_room_knock_action), ) } - JoinAuthorisationStatus.Unknown -> Unit } + JoinAuthorisationStatus.IsKnocked -> { + OutlinedButton( + text = stringResource(R.string.screen_join_room_cancel_knock_action), + onClick = onCancelKnock, + modifier = Modifier.fillMaxWidth(), + size = ButtonSize.Large, + ) + } + JoinAuthorisationStatus.NeedInvite -> { + Announcement( + title = stringResource(R.string.screen_join_room_invite_required_message), + description = null, + type = AnnouncementType.Informative(isCritical = false), + ) + } + is JoinAuthorisationStatus.IsBanned -> JoinBannedFooter(joinAuthorisationStatus, onForgetRoom) + JoinAuthorisationStatus.Unknown -> JoinRestrictedFooter(onJoinRoom) + JoinAuthorisationStatus.Restricted -> JoinRestrictedFooter(onJoinRoom) + JoinAuthorisationStatus.Unauthorized -> JoinUnauthorizedFooter(onGoBack) + is JoinAuthorisationStatus.IsSpace -> UnsupportedSpaceFooter(joinAuthorisationStatus.applicationName, onGoBack) + JoinAuthorisationStatus.None -> Unit } } } +@Composable +private fun JoinUnauthorizedFooter( + onOkClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Announcement( + title = stringResource(R.string.screen_join_room_fail_message), + description = stringResource(R.string.screen_join_room_fail_reason), + type = AnnouncementType.Informative(isCritical = true), + ) + Spacer(Modifier.height(24.dp)) + Button( + text = stringResource(CommonStrings.action_ok), + onClick = onOkClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun JoinBannedFooter( + status: JoinAuthorisationStatus.IsBanned, + onForgetRoom: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + val banReason = status.reason?.let { + stringResource(R.string.screen_join_room_ban_reason, it.removeSuffix(".")) + } + val title = if (status.banSender != null) { + stringResource(R.string.screen_join_room_ban_by_message, status.banSender.displayName) + } else { + stringResource(R.string.screen_join_room_ban_message) + } + Announcement( + title = title, + description = banReason, + type = AnnouncementType.Informative(isCritical = true), + ) + Spacer(Modifier.height(24.dp)) + Button( + text = stringResource(R.string.screen_join_room_forget_action), + onClick = onForgetRoom, + modifier = Modifier.fillMaxWidth(), + size = ButtonSize.Large, + ) + } +} + +@Composable +private fun JoinRestrictedFooter( + onJoinRoom: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Announcement( + title = stringResource(R.string.screen_join_room_join_restricted_message), + description = null, + type = AnnouncementType.Informative(), + ) + Spacer(Modifier.height(24.dp)) + SuperButton( + onClick = onJoinRoom, + modifier = Modifier.fillMaxWidth(), + buttonSize = ButtonSize.Large, + ) { + Text( + text = stringResource(R.string.screen_join_room_join_action), + ) + } + } +} + +@Composable +private fun UnsupportedSpaceFooter( + applicationName: String, + onGoBack: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Announcement( + title = stringResource(R.string.screen_join_room_space_not_supported_title), + description = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName), + type = AnnouncementType.Informative(), + ) + Spacer(Modifier.height(24.dp)) + Button( + text = stringResource(CommonStrings.action_ok), + onClick = onGoBack, + modifier = Modifier.fillMaxWidth(), + size = ButtonSize.Large, + ) + } +} + @Composable private fun JoinRoomContent( + roomIdOrAlias: RoomIdOrAlias, contentState: ContentState, - applicationName: String, knockMessage: String, + hideAvatarsImages: Boolean, onKnockMessageUpdate: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -253,76 +397,84 @@ private fun JoinRoomContent( IsKnockedLoadedContent() } else -> { - DefaultLoadedContent( - modifier = Modifier.verticalScroll(rememberScrollState()), - contentState = contentState, - applicationName = applicationName, - knockMessage = knockMessage, - onKnockMessageUpdate = onKnockMessageUpdate - ) - } - } - } - is ContentState.UnknownRoom -> { - RoomPreviewOrganism( - avatar = { - PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) - }, - title = { - RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview)) - }, - subtitle = { - RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview)) - }, - ) - } - is ContentState.Loading -> { - RoomPreviewOrganism( - avatar = { - PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) - }, - title = { - PlaceholderAtom(width = 200.dp, height = 22.dp) - }, - subtitle = { - PlaceholderAtom(width = 140.dp, height = 20.dp) - }, - ) - } - is ContentState.Failure -> { - RoomPreviewOrganism( - avatar = { - PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) - }, - title = { - when (contentState.roomIdOrAlias) { - is RoomIdOrAlias.Alias -> { - RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier) - } - is RoomIdOrAlias.Id -> { - PlaceholderAtom(width = 200.dp, height = 22.dp) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender + if (inviteSender != null) { + InviteSenderView(inviteSender = inviteSender, hideAvatarImage = hideAvatarsImages) + Spacer(modifier = Modifier.height(32.dp)) } + DefaultLoadedContent( + modifier = Modifier.verticalScroll(rememberScrollState()), + contentState = contentState, + knockMessage = knockMessage, + hideAvatarImage = hideAvatarsImages, + onKnockMessageUpdate = onKnockMessageUpdate + ) } - }, - subtitle = { - Text( - text = stringResource(id = CommonStrings.error_unknown), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.error, - ) - }, - ) + } + } } + is ContentState.UnknownRoom -> UnknownRoomContent() + is ContentState.Loading -> IncompleteContent(roomIdOrAlias, isLoading = true) + is ContentState.Dismissing -> IncompleteContent(roomIdOrAlias, isLoading = false) + is ContentState.Failure -> IncompleteContent(roomIdOrAlias, isLoading = false) } } } +@Composable +private fun UnknownRoomContent( + modifier: Modifier = Modifier +) { + RoomPreviewOrganism( + modifier = modifier, + avatar = { + Spacer(modifier = Modifier.size(AvatarSize.RoomHeader.dp)) + }, + title = { + RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview)) + }, + subtitle = { + }, + ) +} + +@Composable +private fun IncompleteContent( + roomIdOrAlias: RoomIdOrAlias, + isLoading: Boolean, + modifier: Modifier = Modifier +) { + RoomPreviewOrganism( + modifier = modifier, + avatar = { + PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) + }, + title = { + when (roomIdOrAlias) { + is RoomIdOrAlias.Alias -> { + RoomPreviewSubtitleAtom(roomIdOrAlias.identifier) + } + is RoomIdOrAlias.Id -> { + PlaceholderAtom(width = 200.dp, height = 22.dp) + } + } + }, + subtitle = { + if (isLoading) { + Spacer(Modifier.height(8.dp)) + CircularProgressIndicator() + } + }, + ) +} + @Composable private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) { BoxWithConstraints( modifier = modifier - .fillMaxHeight() - .padding(horizontal = 16.dp), + .fillMaxHeight() + .padding(horizontal = 16.dp), contentAlignment = Alignment.Center, ) { IconTitleSubtitleMolecule( @@ -337,15 +489,19 @@ private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) { @Composable private fun DefaultLoadedContent( contentState: ContentState.Loaded, - applicationName: String, knockMessage: String, + hideAvatarImage: Boolean, onKnockMessageUpdate: (String) -> Unit, modifier: Modifier = Modifier, ) { RoomPreviewOrganism( modifier = modifier, avatar = { - Avatar(contentState.avatarData(AvatarSize.RoomHeader)) + Avatar( + contentState.avatarData(AvatarSize.RoomHeader), + hideImage = hideAvatarImage, + avatarType = AvatarType.Room(), + ) }, title = { if (contentState.name != null) { @@ -369,26 +525,8 @@ private fun DefaultLoadedContent( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { - val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender - if (inviteSender != null) { - InviteSenderView(inviteSender = inviteSender) - } RoomPreviewDescriptionAtom(contentState.topic ?: "") - if (contentState.roomType == RoomType.Space) { - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = stringResource(R.string.screen_join_room_space_not_supported_title), - textAlign = TextAlign.Center, - style = ElementTheme.typography.fontBodyLgMedium, - color = MaterialTheme.colorScheme.primary, - ) - Text( - text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName), - textAlign = TextAlign.Center, - style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, - ) - } else if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) { + if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) { Spacer(modifier = Modifier.height(24.dp)) val supportingText = if (knockMessage.isNotEmpty()) { "${knockMessage.length}/$MAX_KNOCK_MESSAGE_LENGTH" @@ -418,6 +556,7 @@ private fun DefaultLoadedContent( @Composable private fun JoinRoomTopBar( contentState: ContentState, + hideAvatarImage: Boolean, onBackClick: () -> Unit, ) { TopAppBar( @@ -434,9 +573,17 @@ private fun JoinRoomTopBar( modifier = titleModifier, verticalAlignment = Alignment.CenterVertically ) { - Avatar(avatarData = contentState.avatarData(AvatarSize.TimelineRoom)) + Avatar( + avatarData = contentState.avatarData(AvatarSize.TimelineRoom), + hideImage = hideAvatarImage, + avatarType = AvatarType.Room(), + ) Text( - modifier = Modifier.padding(horizontal = 8.dp), + modifier = Modifier + .padding(horizontal = 8.dp) + .semantics { + heading() + }, text = contentState.name, style = ElementTheme.typography.fontBodyLgMedium, maxLines = 1, @@ -462,6 +609,8 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) onBackClick = { }, onJoinSuccess = { }, onKnockSuccess = { }, + onForgetSuccess = { }, onCancelKnockSuccess = { }, + onDeclineInviteAndBlockUser = { }, ) } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt index 2d885229bd0..7826a819c68 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/CancelKnockRoom.kt @@ -21,8 +21,8 @@ interface CancelKnockRoom { class DefaultCancelKnockRoom @Inject constructor(private val client: MatrixClient) : CancelKnockRoom { override suspend fun invoke(roomId: RoomId): Result { return client - .getPendingRoom(roomId) - ?.leave() + .getRoom(roomId) + ?.use { it.leave() } ?: Result.failure(IllegalStateException("No pending room found")) } } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/ForgetRoom.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/ForgetRoom.kt new file mode 100644 index 00000000000..ac17e2c3e23 --- /dev/null +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/ForgetRoom.kt @@ -0,0 +1,26 @@ +/* + * 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.joinroom.impl.di + +import com.squareup.anvil.annotations.ContributesBinding +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 javax.inject.Inject + +interface ForgetRoom { + suspend operator fun invoke(roomId: RoomId): Result +} + +@ContributesBinding(SessionScope::class) +class DefaultForgetRoom @Inject constructor(private val client: MatrixClient) : ForgetRoom { + override suspend fun invoke(roomId: RoomId): Result { + return client.getRoom(roomId)?.use { it.forget() } + ?: Result.failure(IllegalStateException("Room not found")) + } +} diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index d906edbba94..5e85c0abbc2 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -11,7 +11,8 @@ import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.joinroom.impl.JoinRoomPresenter import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.Presenter @@ -32,8 +33,10 @@ object JoinRoomModule { joinRoom: JoinRoom, knockRoom: KnockRoom, cancelKnockRoom: CancelKnockRoom, + forgetRoom: ForgetRoom, acceptDeclineInvitePresenter: Presenter, buildMeta: BuildMeta, + seenInvitesStore: SeenInvitesStore, ): JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory { override fun create( @@ -52,9 +55,11 @@ object JoinRoomModule { matrixClient = client, joinRoom = joinRoom, knockRoom = knockRoom, + forgetRoom = forgetRoom, cancelKnockRoom = cancelKnockRoom, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, buildMeta = buildMeta, + seenInvitesStore = seenInvitesStore, ) } } diff --git a/features/joinroom/impl/src/main/res/values-bg/translations.xml b/features/joinroom/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 00000000000..e9f7c38fcf8 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,4 @@ + + + "Присъединяване към стаята" + diff --git a/features/joinroom/impl/src/main/res/values-cs/translations.xml b/features/joinroom/impl/src/main/res/values-cs/translations.xml index 37c537bdce5..bab8e1f01e3 100644 --- a/features/joinroom/impl/src/main/res/values-cs/translations.xml +++ b/features/joinroom/impl/src/main/res/values-cs/translations.xml @@ -7,6 +7,10 @@ "Ano, zrušit" "Opravdu chcete zrušit svou žádost o vstup do této místnosti?" "Zrušit žádost o vstup" + "Ano, odmítnout a zablokovat" + "Opravdu chcete odmítnout pozvánku do této místnosti? Tím také zabráníte tomu, aby vás %1$s kontaktoval(a) nebo pozval(a) do místností." + "Odmítnout pozvání a zablokovat" + "Odmítnout a zablokovat" "Vstup do místnosti se nezdařil." "Tato místnost je buď určena pouze pro zvané, nebo do ní může být omezen přístup na úrovni prostoru." "Zapomenout na tuto místnost" @@ -17,6 +21,8 @@ "Zpráva (nepovinné)" "Pokud bude váš požadavek přijat, obdržíte pozvánku na vstup do místnosti." "Žádost o vstup odeslána" + "Náhled místnosti se nám nepodařilo zobrazit. To může být způsobeno problémy se sítí nebo serverem." + "Náhled této místnosti jsme nemohli zobrazit" "%1$s zatím nepodporuje prostory. Prostory můžete používat na webu." "Prostory zatím nejsou podporovány" "Klikněte na tlačítko níže a správce místnosti bude informován. Po schválení se budete moci připojit ke konverzaci." diff --git a/features/joinroom/impl/src/main/res/values-cy/translations.xml b/features/joinroom/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..b093d942205 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,32 @@ + + + "Cawsoch eich gwahardd o\'r ystafell hon gan %1$s." + "Cawsoch eich gwahardd o\'r ystafell hon" + "Rheswm: %1$s." + "Diddymu cais" + "Iawn, diddymu" + "Ydych chi\'n siŵr eich bod am ddiddymu\'ch cais i ymuno â\'r ystafell hon?" + "Diddymu cais i ymuno" + "Iawn, gwrthod a rhwystro" + "Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â\'r ystafell hon? Bydd hyn hefyd yn atal %1$s rhag cysylltu â chi neu eich gwahodd i ystafelloedd." + "Gwrthod gwahoddiad a rhwystro" + "Gwrthod a rhwystro" + "Methodd yr ymuno â\'r ystafell." + "Mae\'r ystafell hon naill ai drwy wahoddiad yn unig neu efallai y bydd cyfyngiadau ar fynediad ar lefel y gofod." + "Anghofiwch yr ystafell hon" + "Mae angen gwahoddiad arnoch chi er mwyn ymuno â\'r ystafell hon" + "Ymuno â\'r ystafell" + "Efallai y bydd angen i chi gael eich gwahodd neu fod yn aelod o ofod er mwyn ymuno." + "Anfon cais i ymuno" + "Neges (dewisol)" + "Byddwch yn derbyn gwahoddiad i ymuno â\'r ystafell os caiff eich cais ei dderbyn." + "Anfonwyd y cais i ymuno" + "Doedd dim modd dangos rhagolwg yr ystafell. Gall hyn fod oherwydd problemau rhwydwaith neu weinydd." + "Doedd dim modd dangos rhagolwg yr ystafell hon" + "Nid yw %1$s yn cefnogi gofodau eto. Gallwch gael mynediad i ofodau ar y we." + "Nid yw gofodau\'n cael eu cefnogi eto" + "Cliciwch ar y botwm isod a bydd gweinyddwr ystafell yn cael ei hysbysu. Byddwch yn gallu ymuno â\'r sgwrs ar ôl ei chymeradwyo." + "Rhaid i chi fod yn aelod o\'r ystafell hon i weld hanes y neges." + "Eisiau ymuno â\'r ystafell hon?" + "Nid yw rhagolwg ar gael" + diff --git a/features/joinroom/impl/src/main/res/values-da/translations.xml b/features/joinroom/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..c27ff741455 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,32 @@ + + + "Du blev spærret fra dette rum af %1$s." + "Du blev spærret fra dette rum" + "Årsag: %1$s." + "Annuller anmodning" + "Ja, annullér" + "Er du sikker på, at du vil annullere din anmodning om at deltage i dette rum?" + "Annullér anmodning om at deltage" + "Ja, afvis og blokér" + "Er du sikker på, at du vil afvise invitationen til at deltage i dette rum? Dette forhindrer også %1$s i at kontakte dig eller invitere dig til andre rum." + "Afvis invitation og blokér" + "Afvis og blokér" + "Deltagelse i rummet fejlede." + "Dette rum er enten kun for gæster, eller der kan være sat begrænsninger for adgangen på klyngeniveau." + "Glem dette rum" + "Du har brug for en invitation for at deltage i dette rum" + "Deltag i rummet" + "Du skal muligvis være inviteret eller være medlem af en klynge for at deltage." + "Send anmodning om at deltage" + "Besked (valgfrit)" + "Du vil modtage en invitation til at deltage i rummet, hvis din anmodning accepteres." + "Anmodning om at deltage sendt" + "Vi kunne ikke forhåndsvise rummet. Dette kan skyldes netværks- eller serverproblemer." + "Vi kunne ikke forhåndsvise rummet" + "%1$s understøtter ikke klynger endnu. Du kan få adgang til klynger på nettet." + "Klynger er ikke understøttet endnu" + "Klik på knappen nedenfor, og en rumadministrator vil blive underrettet. Du kan deltage i samtalen, når din anmodning er godkendt." + "Du skal være medlem af dette rum for at kunne se meddelelseshistorikken." + "Vil du deltage i dette rum?" + "Forhåndsvisning er ikke tilgængelig" + diff --git a/features/joinroom/impl/src/main/res/values-de/translations.xml b/features/joinroom/impl/src/main/res/values-de/translations.xml index 5a6d901a3dd..0fa2bdce5ff 100644 --- a/features/joinroom/impl/src/main/res/values-de/translations.xml +++ b/features/joinroom/impl/src/main/res/values-de/translations.xml @@ -1,26 +1,32 @@ - "Sie wurden aus diesem Chatroom ausgeschlossen %1$s." - "Sie wurden aus diesem Chatroom verbannt" + "Sie wurden von %1$s für diesen Chatroom gesperrt." + "Sie wurden für diesen Chatroom gesperrt" "Grund:%1$s." "Anfrage abbrechen" "Ja, abbrechen" "Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?" "Beitrittsanfrage stornieren" + "Ja, ablehnen & blockieren" + "Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt." + "Einladung ablehnen & Nutzer blockieren" + "Ablehnen und blockieren" "Der Beitritt zum Chatroom schlug fehl." - "Dieser Chatroom ist entweder nur auf Einladung zugänglich oder es kann zu Zugangsbeschränkungen auf Spaceebene kommen." + "Dieser Chatroom ist entweder nur auf Einladung zugänglich oder es gibt andere Zugangsbeschränkungen durch Spaces." "Vergessen Sie diesen Raum" - "Sie benötigen eine Einladung, um diesem Raum beizutreten" + "Sie benötigen eine Einladung, um diesem Chatroom zu betreten" "Raum beitreten" "Möglicherweise müssen Sie eingeladen sein oder Mitglied eines Spaces sein, um beitreten zu können." "Anklopfen" "Nachricht (optional)" - "Falls Ihre Anfrage, dem Raum beizutreten, akzeptiert wird, werden Sie eine Einladung erhalten." + "Falls Ihre Anfrage, den Raum zu betreten, akzeptiert wird, erhalten Sie eine Einladung." "Beitrittsanfrage geschickt" - "%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen." + "Wir konnten die Chatroomvorschau nicht anzeigen. Dies kann an Netzwerk- oder Serverproblemen liegen." + "Wir konnten diese Chatroomvorschau nicht anzeigen" + "%1$s unterstützt noch keine Spaces. Sie können auf Spaces im Web zugreifen." "Spaces werden noch nicht unterstützt" - "Klopfe an um einen Administrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen." - "Du musst Mitglied in diesem Raum sein, um den Nachrichtenverlauf zu sehen." - "Willst du diesem Raum beitreten?" + "Klicken Sie auf die Schaltfläche unten und ein Chatroomadministrator wird benachrichtigt. Nach der Freigabe durch einen Chatroomadministrator können Sie sich an der Unterhaltung beteiligen." + "Sie müssen Mitglied in diesem Chatroom sein, um den Nachrichtenverlauf einsehen zu können." + "Möchten Sie diesem Chatroom betreten?" "Vorschau nicht verfügbar" diff --git a/features/joinroom/impl/src/main/res/values-el/translations.xml b/features/joinroom/impl/src/main/res/values-el/translations.xml index 0b114b54e6e..9a7296b93b3 100644 --- a/features/joinroom/impl/src/main/res/values-el/translations.xml +++ b/features/joinroom/impl/src/main/res/values-el/translations.xml @@ -1,18 +1,32 @@ + "Αποκλειστήκατε από αυτή την αίθουσα από το χρήστη %1$s." + "Σας απαγορεύτηκε η είσοδος σε αυτή την αίθουσα" + "Αιτία: %1$s." "Ακύρωση αιτήματος" "Ναι, ακύρωση" - "Σίγουρα θες να ακυρώσεις το αίτημά σου για συμμετοχή σε αυτό το δωμάτιο;" + "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το αίτημά σας να συμμετάσχετε σε αυτή την αίθουσα;" "Ακύρωση αίτησης συμμετοχής" - "Συμμετοχή στο δωμάτιο" + "Ναι, απόρριψη και αποκλεισμός" + "Είστε βέβαιοι ότι θέλετε να απορρίψετε την πρόσκληση συμμετοχής σε αυτήν την αίθουσα; Αυτό θα εμποδίσει επίσης τον χρήστη %1$s να επικοινωνήσει μαζί σας ή να σας προσκαλέσει σε αίθουσες." + "Απόρριψη πρόσκλησης και αποκλεισμός" + "Απόρριψη και αποκλεισμός" + "Η συμμετοχή στην αίθουσα απέτυχε." + "Αυτή η αίθουσα είναι είτε μόνο για προσκεκλημένους είτε ενδέχεται να υπάρχουν περιορισμοί πρόσβασης σε επίπεδο χώρου." + "Ξεχάστε αυτή την αίθουσα" + "Χρειάζεστε πρόσκληση για να συμμετάσχετε σε αυτή την αίθουσα" + "Συμμετοχή στην αίθουσα" + "Ενδέχεται να χρειαστεί να προσκληθείτε ή να είστε μέλος ενός χώρου για να συμμετάσχετε." "Χτύπα για συμμετοχή" "Μήνυμα (προαιρετικό)" - "Θα λάβεις πρόσκληση για συμμετοχή στο δωμάτιο εάν το αίτημά σου γίνει αποδεκτό." + "Θα λάβετε πρόσκληση για να συμμετάσχετε στην αίθουσα, εάν το αίτημά σας γίνει αποδεκτό." "Το αίτημα συμμετοχής στάλθηκε" + "Δεν μπορέσαμε να εμφανίσουμε την προεπισκόπηση της αίθουσας. Αυτό μπορεί να οφείλεται σε προβλήματα δικτύου ή διακομιστή." + "Δεν μπορέσαμε να εμφανίσουμε αυτή την προεπισκόπηση αίθουσας" "Το %1$s δεν υποστηρίζει ακόμα χώρους. Μπορείς να έχεις πρόσβαση σε χώρους στον ιστό." "Οι Χώροι δεν υποστηρίζονται ακόμα" - "Κάνε κλικ στο παρακάτω κουμπί και ένας διαχειριστής δωματίου θα ειδοποιηθεί. Θα μπορείς να συμμετάσχεις στη συνομιλία μόλις εγκριθεί." - "Πρέπει να είσαι μέλος αυτού του δωματίου για να δεις το ιστορικό μηνυμάτων." - "Θες να συμμετάσχεις σε αυτό το δωμάτιο;" + "Κάντε κλικ στο παρακάτω κουμπί και θα ειδοποιηθεί ένας διαχειριστής της αίθουσας. Μόλις εγκριθείτε, θα μπορείτε να συμμετάσχετε στη συζήτηση." + "Πρέπει να είστε μέλος αυτής της αίθουσας για να δείτε το ιστορικό των μηνυμάτων." + "Θέλετε να συμμετάσχετε σε αυτή την αίθουσα;" "Η προεπισκόπηση δεν είναι διαθέσιμη" diff --git a/features/joinroom/impl/src/main/res/values-es/translations.xml b/features/joinroom/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..b98536b1be4 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,32 @@ + + + "Has sido vetado de esta sala por %1$s." + "Has sido vetado de esta sala" + "Motivo: %1$s." + "Cancelar solicitud" + "Sí, cancelar" + "¿Estás seguro de que deseas cancelar tu solicitud de unión a esta sala?" + "Cancelar solicitud de unión" + "Sí, rechazar y bloquear" + "¿Estás seguro de que deseas rechazar la invitación para unirte a esta sala? Esto también impedirá que %1$s pueda contactar contigo o invitarte a salas." + "Rechazar invitación y bloquear" + "Rechazar y bloquear" + "No se pudo unir a la sala." + "O bien solo se puede acceder a esta sala con invitación, o puede que haya restricciones de acceso a nivel de espacio." + "Olvidar esta sala" + "Necesitas una invitación para unirte a esta sala" + "Unirse a la sala" + "Es posible que necesites ser invitado o ser miembro de un espacio para poder unirte." + "Enviar solicitud de unión" + "Mensaje (opcional)" + "Recibirás una invitación para unirte a la sala si se acepta tu solicitud." + "Solicitud de unión enviada" + "No hemos podido mostrar la vista previa de la sala. Esto puede deberse a problemas de red o del servidor." + "No hemos podido mostrar la vista previa de esta sala" + "%1$s aún no admite los espacios. Puedes acceder a los espacios en la web." + "Todavía no se admiten los espacios" + "Haz clic en el botón que aparece a continuación y se notificará a un administrador de la sala. Podrás unirte a la conversación una vez que hayas sido aprobado." + "Debes ser miembro de esta sala para ver el historial de mensajes." + "¿Quieres unirte a esta sala?" + "La vista previa no está disponible" + diff --git a/features/joinroom/impl/src/main/res/values-et/translations.xml b/features/joinroom/impl/src/main/res/values-et/translations.xml index 5efaa23a1d9..00ae5f18bbc 100644 --- a/features/joinroom/impl/src/main/res/values-et/translations.xml +++ b/features/joinroom/impl/src/main/res/values-et/translations.xml @@ -7,6 +7,10 @@ "Jah, tühista" "Kas sa oled kindel, et soovid tühistada oma palve jututoaga liitumiseks?" "Tühista liitumispalve" + "Jah, keeldu ja blokeeri" + "Kas sa oled kindel, et soovid keelduda kutsest sellesse jututuppa? Samaga kaob kasutajal %1$s võimalus sinuga suhelda ja saata sulle jututubade kutseid." + "Keeldu kutsest ja blokeeri" + "Keeldu ja blokeeri" "Jututoaga liitumine ei õnnestunud." "Ligipääs siia jututuppa on võimalik vaid kutse alusel või kehtivad siin kogukonnakohased piirangud." "Unusta see jututuba" @@ -17,6 +21,8 @@ "Selgitus (kui soovid lisada)" "Kui sinu liitumispalvega ollakse nõus, siis saad kutse jututoaga liitumiseks." "Liitumispalve on saadetud" + "Me ei saanud jututoa eelvaadet näidata. See võib olla põhjustatud võrguühenduse või serveri vigadest." + "Meil ei õnnestunud selle jututoa eelvaadet kuvada" "%1$s veel ei toeta kogukondadega liitumise ja kasutamise võimalust. Vajadusel saad seda teha veebiliidese vahendusel." "Kogukonnad pole veel toetatud" "Klõpsi allolevat nuppu ja jututoa haldaja saab asjakohase teate. Sa saad liituda, kui haldaja sinu soovi heaks kiidab." diff --git a/features/joinroom/impl/src/main/res/values-eu/translations.xml b/features/joinroom/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..4d6ddd9dbd0 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,18 @@ + + + "Arrazoia: %1$s." + "Utzi eskaera bertan behera" + "Bai, utzi bertan behera" + "Gelara sartzeak huts egin du." + "Ahaztu gela hau" + "Sartu gelan" + "Bidali batzeko eskaera" + "Mezua (aukerakoa)" + "Sartzeko eskaera bidali da" + "%1$s ez da oraindik guneekin bateragarria. Webgunean sar zaitezke guneetara." + "Oraindik ez da guneekin bateragarria" + "Klikatu beheko botoia eta gelako administratzaileari jakinaraziko zaio. Elkarrizketara batu ahal izango zara onartutakoan." + "Gela honetako kide izan behar zara mezuen historia ikusteko." + "Gela honetan sartu nahi?" + "Aurrebista ez dago erabilgarri" + diff --git a/features/joinroom/impl/src/main/res/values-fa/translations.xml b/features/joinroom/impl/src/main/res/values-fa/translations.xml index 85151ddf648..529177cedbf 100644 --- a/features/joinroom/impl/src/main/res/values-fa/translations.xml +++ b/features/joinroom/impl/src/main/res/values-fa/translations.xml @@ -1,7 +1,21 @@ + "به دست %1$s از این اتاق محروم شدید." + "از این اتاق محروم شدید" + "دلیل: %1$s." + "لغو درخواست" + "بله. لغو" + "لغو درخواست پیوستن" + "بله. رد و انسداد" + "رد دعوت و انسداد" + "رد و انسداد" + "پیوستن به اتاق شکست خورد." + "فراموشی این اتاق" + "برای پیوستن به این اتاق نیاز به دعوت دارید" "پیوستن به اتاق" "در زدن برای پیوستن" + "پیام (اختیاری)" + "درخواست پیوستن فرستاده شد" "%1$s هنوز از فضاها پشتیبانی نمی‌کند. می‌توانید روی وب به فضاها دسترسی داشته باشید." "فضاها هنوز پشتیبانی نمی‌شوند" "زدن روی این دکمه برای آگاه شدن مدیر اتاق. پس از تأیید می‌توانید به گفت‌وگو بپیوندید." diff --git a/features/joinroom/impl/src/main/res/values-fi/translations.xml b/features/joinroom/impl/src/main/res/values-fi/translations.xml index 721628b60da..2a25587398a 100644 --- a/features/joinroom/impl/src/main/res/values-fi/translations.xml +++ b/features/joinroom/impl/src/main/res/values-fi/translations.xml @@ -7,6 +7,10 @@ "Kyllä, peruuta" "Haluatko varmasti peruuttaa pyyntösi liittyä tähän huoneeseen?" "Peruuta liittymispyyntö" + "Kyllä, hylkää ja estä" + "Oletko varma, että haluat kieltäytyä kutsusta liittyä tähän huoneeseen? Tämä estää myös käyttäjää %1$s ottamasta sinuun yhteyttä tai kutsumasta sinua huoneisiin." + "Hylkää kutsu ja estä" + "Hylkää ja estä" "Huoneeseen liittyminen epäonnistui." "Tämä huone on tarkoitettu vain kutsutuille, tai siihen saattaa liittyä rajoituksia tilatasolla." "Unohda tämä huone" @@ -17,6 +21,8 @@ "Viesti (valinnainen)" "Saat kutsun liittyä huoneeseen, jos pyyntösi hyväksytään." "Liittymispyyntö lähetetty" + "Emme voineet näyttää huoneen esikatselua. Tämä voi johtua verkko- tai palvelinongelmista." + "Emme voineet näyttää tämän huoneen esikatselua" "%1$s ei tue vielä tiloja. Voit käyttää tiloja selainversiolla." "Tiloja ei vielä tueta" "Paina alla olevaa nappia ja huoneen ylläpitäjä saa ilmoituksen. Voit liittyä keskusteluun kun pyyntösi on hyväksytty." diff --git a/features/joinroom/impl/src/main/res/values-fr/translations.xml b/features/joinroom/impl/src/main/res/values-fr/translations.xml index e8ca43e13b2..0652b0c16d4 100644 --- a/features/joinroom/impl/src/main/res/values-fr/translations.xml +++ b/features/joinroom/impl/src/main/res/values-fr/translations.xml @@ -7,6 +7,10 @@ "Oui, annuler" "Êtes-vous sûr de vouloir annuler votre demande d’accès à ce salon ?" "Annuler la demande d’adhésion" + "Oui, refuser et bloquer" + "Êtes-vous sûr de vouloir refuser l’invitation à rejoindre ce salon ? Cela empêchera également %1$s de vous contacter ou de vous inviter dans les salons." + "Refuser l’invitation et bloquer" + "Refuser et bloquer" "Rejoindre le salon a échoué." "Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions d’accès au niveau du Space." "Oublier ce salon" @@ -17,7 +21,9 @@ "Message (facultatif)" "Vous recevrez une invitation à rejoindre le salon si votre demande est acceptée." "Demande de rejoindre le salon envoyée" - "Les Spaces ne sont pas encore pris en charge par %1$s . Vous pouvez voir les Spaces sur le Web." + "Impossible d’afficher l’aperçu du salon. Cela peut être dû à des problèmes de réseau ou de serveur." + "Impossible d’afficher l’aperçu de ce salon" + "Les Spaces ne sont pas encore pris en charge par %1$s. Vous pouvez voir les Spaces sur le Web." "Les Spaces ne sont pas encore pris en charge" "Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion." "Vous devez être un membre du salon pour pouvoir lire l’historique des messages." diff --git a/features/joinroom/impl/src/main/res/values-hu/translations.xml b/features/joinroom/impl/src/main/res/values-hu/translations.xml index 5073b034fce..e6b461abee6 100644 --- a/features/joinroom/impl/src/main/res/values-hu/translations.xml +++ b/features/joinroom/impl/src/main/res/values-hu/translations.xml @@ -7,6 +7,10 @@ "Igen, visszavonás" "Biztos, hogy visszavonja a szobához való csatlakozási kérését?" "Csatlakozási kérés visszavonása" + "Igen, elutasítás és blokkolás" + "Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez a szobához? Ez azt is megakadályozza, hogy %1$s kapcsolatba lépjen Önnel, vagy szobákba hívja." + "Meghívó elutasítása és blokkolás" + "Elutasítás és blokkolás" "A szobához való csatlakozás sikertelen." "Ebbe a szobába csak meghívóval vagy tértagsággal lehet belépni." "Szoba elfelejtése" @@ -17,6 +21,8 @@ "Üzenet (nem kötelező)" "Ha a kérését elfogadják, meghívót kap a szobához való csatlakozáshoz." "Csatlakozási kérés elküldve" + "Nem tudtuk megjeleníteni a szoba előnézetét. Ennek az oka hálózati vagy kiszolgálóprobléma is lehet." + "Nem tudtuk megjeleníteni a szoba előnézetét" "Az %1$s még nem támogatja a tereket. A tereket a weben érheti el." "A terek még nem támogatottak" "Kattintson az alábbi gombra, és a szoba adminisztrátora értesítést kap. A jóváhagyást követően csatlakozhat a beszélgetéshez." diff --git a/features/joinroom/impl/src/main/res/values-in/translations.xml b/features/joinroom/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..16c078c6ff8 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,32 @@ + + + "Anda dicekal dari ruangan ini oleh %1$s." + "Anda dicekal dari ruangan ini" + "Alasan: %1$s." + "Batalkan permintaan" + "Ya, batalkan" + "Apakah Anda yakin ingin membatalkan permintaan Anda untuk bergabung dengan ruangan ini?" + "Batalkan permintaan untuk bergabung" + "Ya, tolak & blokir" + "Apakah Anda yakin ingin menolak undangan untuk bergabung dengan ruangan ini? Ini juga akan mencegah %1$s menghubungi Anda atau mengundang Anda ke ruangan." + "Tolak undangan & blokir" + "Tolak dan blokir" + "Bergabung dalam ruangan gagal." + "Ruangan ini hanya untuk undangan atau mungkin ada pembatasan akses pada tingkat space." + "Lupakan ruangan ini" + "Anda memerlukan undangan untuk bergabung dalam ruangan ini" + "Gabung dengan ruangan" + "Anda mungkin perlu diundang atau menjadi anggota space untuk bergabung." + "Ketuk untuk bergabung" + "Pesan (opsional)" + "Anda akan menerima undangan untuk bergabung dengan ruangan jika permintaan Anda diterima." + "Permintaan untuk bergabung dikirim" + "Kami tidak dapat menampilkan pratinjau ruangan. Ini mungkin karena masalah jaringan atau server." + "Kami tidak dapat menampilkan pratinjau ruangan ini" + "%1$s belum mendukung space. Anda dapat mengakses space di web." + "Space belum didukung" + "Klik tombol di bawah ini dan administrator kamar akan diberi tahu. Anda akan dapat bergabung dengan percakapan setelah disetujui." + "Anda harus menjadi anggota ruangan ini untuk melihat riwayat pesan." + "Ingin bergabung dengan ruangan ini?" + "Pratinjau tidak tersedia" + diff --git a/features/joinroom/impl/src/main/res/values-it/translations.xml b/features/joinroom/impl/src/main/res/values-it/translations.xml index 0c80a574e7d..753b54159a0 100644 --- a/features/joinroom/impl/src/main/res/values-it/translations.xml +++ b/features/joinroom/impl/src/main/res/values-it/translations.xml @@ -7,6 +7,10 @@ "Sì, annulla" "Sei sicuro di voler annullare la tua richiesta di accesso a questa stanza?" "Annulla la richiesta di accesso" + "Sì, rifiuta e blocca" + "Sei sicuro di voler rifiutare l\'invito a entrare in questa stanza? Ciò impedirà a %1$s di contattarti o invitarti nuovamente in una stanza." + "Rifiuta invito e blocca" + "Rifiuta e blocca" "L\'accesso alla stanza non è riuscito." "Questa stanza è solo su invito o potrebbero esserci delle restrizioni all\'accesso al livello dello spazio." "Dimentica questa stanza" @@ -17,6 +21,8 @@ "Messaggio (opzionale)" "Riceverai un invito a entrare nella stanza se la tua richiesta viene accettata." "Richiesta di accesso inviata" + "Non è stato possibile visualizzare l\'anteprima della stanza. Ciò può essere dovuto a problemi di rete o del server." + "Non è stato possibile visualizzare l\'anteprima di questa stanza" "%1$s non supporta ancora gli spazi. Puoi accedere agli spazi sul web." "Gli spazi non sono ancora supportati" "Clicca sul pulsante qui sotto e un amministratore della stanza riceverà una notifica. Potrai partecipare alla conversazione una volta approvato." diff --git a/features/joinroom/impl/src/main/res/values-nb/translations.xml b/features/joinroom/impl/src/main/res/values-nb/translations.xml index 103b10201d1..f1f4d7afda7 100644 --- a/features/joinroom/impl/src/main/res/values-nb/translations.xml +++ b/features/joinroom/impl/src/main/res/values-nb/translations.xml @@ -1,6 +1,32 @@ + "Du ble utestengt fra dette rommet av %1$s." + "Du ble utestengt fra dette rommet" + "Årsak: %1$s." + "Avbryt forespørsel" + "Ja, avbryt" + "Er du sikker på at du vil kansellere forespørselen din om å bli med i dette rommet?" + "Avbryt forespørsel om å bli med" + "Ja, avslå og blokker" + "Er du sikker på at du vil avslå invitasjonen til å bli med i dette rommet? Dette vil også forhindre %1$s fra å kontakte deg eller invitere deg til rom." + "Avslå invitasjon og blokker" + "Avslå og blokker" + "Å bli med i rommet mislyktes." + "Dette rommet er enten kun for inviterte, eller det kan være begrensninger for tilgang på områdenivå." + "Glem dette rommet" + "Du trenger en invitasjon for å bli med i dette rommet" "Bli med i rommet" + "Du må kanskje bli invitert eller være medlem av et område for å bli med." + "Send forespørsel om å bli med" + "Melding (valgfritt)" + "Du vil motta en invitasjon til å bli med i rommet hvis forespørselen din blir akseptert." + "Forespørsel om å bli med sendt" + "Vi kunne ikke vise forhåndsvisningen av rommet. Dette kan skyldes nettverks- eller serverproblemer." + "Vi kunne ikke vise forhåndsvisning av dette rommet" "%1$s støtter ikke områder ennå. Du kan få tilgang til områder på nett." "Områder støttes ikke ennå" + "Klikk på knappen nedenfor, så vil en romadministrator bli varslet. Du vil kunne delta i samtalen når den er godkjent." + "Du må være medlem av dette rommet for å se meldingshistorikken." + "Vil du bli med i dette rommet?" + "Forhåndsvisning er ikke tilgjengelig" diff --git a/features/joinroom/impl/src/main/res/values-nl/translations.xml b/features/joinroom/impl/src/main/res/values-nl/translations.xml index c64b8917d3f..f7d19d9fd1a 100644 --- a/features/joinroom/impl/src/main/res/values-nl/translations.xml +++ b/features/joinroom/impl/src/main/res/values-nl/translations.xml @@ -1,8 +1,10 @@ + "Verzoek annuleren" "Toetreden tot de kamer" "Klop om deel te nemen" "Bericht (optioneel)" + "Je ontvangt een uitnodiging om deel te nemen aan de kamer als je aanvraag wordt geaccepteerd." "Verzoek om toe te treden verzonden" "%1$s ondersteunt nog geen spaces. Je kunt spaces benaderen via de webbrowser." "Spaces worden nog niet ondersteund" diff --git a/features/joinroom/impl/src/main/res/values-pl/translations.xml b/features/joinroom/impl/src/main/res/values-pl/translations.xml index 758d8d56153..56f16fc4757 100644 --- a/features/joinroom/impl/src/main/res/values-pl/translations.xml +++ b/features/joinroom/impl/src/main/res/values-pl/translations.xml @@ -1,14 +1,28 @@ + "Zostałeś zbanowany z tego pokoju przez %1$s." + "Zostałeś zbanowany z tego pokoju" + "Powód: %1$s." "Anuluj prośbę" "Tak, anuluj" "Czy na pewno chcesz anulować prośbę o dołączenie do tego pokoju?" "Anuluj prośbę o dołączenie" + "Tak, odrzuć i zablokuj" + "Czy na pewno chcesz odrzucić zaproszenie dołączenia do tego pokoju? %1$s nie będzie mógł się również z Tobą skontaktować, ani zaprosić Cię do pokoju." + "Odrzuć zaproszenie i zablokuj" + "Odrzuć i zablokuj" + "Nie udało się dołączyć do pokoju." + "Ten pokój wymaga zaproszenia lub jest ograniczony z poziomu przestrzeni." + "Zapomnij o tym pokoju" + "Potrzebujesz zaproszenia, aby dołączyć do tego pokoju" "Dołącz do pokoju" + "Aby dołączyć, musisz uzyskać zaproszenie lub być członkiem danej przestrzeni." "Wyślij prośbę o dołączenie" "Wiadomość (opcjonalne)" "Otrzymasz zaproszenie dołączenia do pokoju, jeśli prośba zostanie zaakceptowana." "Wysłano prośbę o dołączenie" + "Nie udało się wyświetlić podglądu pokoju. Może to być spowodowane problemami z siecią lub serwerem." + "Nie udało nam się wyświetlić podglądu tego pokoju" "%1$s jeszcze nie obsługuje przestrzeni. Uzyskaj dostęp do przestrzeni w wersji web." "Przestrzenie nie są jeszcze obsługiwane" "Kliknij przycisk poniżej, aby powiadomić administratora pokoju. Po zatwierdzeniu będziesz mógł dołączyć do rozmowy." diff --git a/features/joinroom/impl/src/main/res/values-pt-rBR/translations.xml b/features/joinroom/impl/src/main/res/values-pt-rBR/translations.xml index 75b75b80a4d..015e235c0ea 100644 --- a/features/joinroom/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/joinroom/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,4 +1,32 @@ + "Você foi banido desta sala por %1$s." + "Você foi banido desta sala" + "Motivo: %1$s." + "Cancelar pedido" + "Sim, cancele" + "Tem a certeza de que pretende cancelar o seu pedido de adesão a esta sala?" + "Cancelar pedido de adesão" + "Sim, recusar e bloquear" + "Você tem certeza de que deseja recusar o convite para participar desta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para as salas." + "Recusar convite e bloquear" + "Recusar e bloquear" + "A entrada na sala falhou." + "Esta sala é apenas para convidados ou pode haver restrições de acesso no nível do espaço." + "Esquecer esta sala" + "Você precisa de um convite para entrar nesta sala" + "Entrar na sala" + "Talvez você precise ser convidado ou ser membro de um espaço para participar." + "Enviar solicitação para participar" + "Mensagem (opcional)" + "Você receberá um convite para participar da sala se seu pedido for aceito." + "Pedido de adesão enviado" + "Não foi possível exibir a visualização da sala. Isso pode ser devido a problemas de rede ou de servidor." + "Não foi possível exibir a visualização desta sala" "%1$s não suporta espaços ainda. Você pode acessar os espaços na web." + "Os espaços ainda não são compatíveis" + "Clique no botão abaixo e um administrador da sala será notificado. Você poderá participar da conversa assim que for aprovado." + "Você deve ser um membro desta sala para visualizar o histórico de mensagens." + "Quer entrar nesta sala?" + "A pré-visualização não está disponível" diff --git a/features/joinroom/impl/src/main/res/values-pt/translations.xml b/features/joinroom/impl/src/main/res/values-pt/translations.xml index 93eeaf3bca2..ffca14b009d 100644 --- a/features/joinroom/impl/src/main/res/values-pt/translations.xml +++ b/features/joinroom/impl/src/main/res/values-pt/translations.xml @@ -1,14 +1,28 @@ + "Foste banido desta sala por %1$s." + "Foste banido desta sala." + "Razão: %1$s." "Cancelar pedido" "Sim, cancelar" "Tens a certeza de que queres cancelar o teu pedido de entrada nesta sala?" "Cancela o pedido de adesão" + "Sim, recusar & bloquear" + "Tens a certeza de que queres recusar o convite para entrar nesta sala? Isto também evitará que %1$s te contacte ou te convide para salas." + "Recusar convite & bloquear" + "Recusar e bloquear" + "Falha ao entrar na sala." + "A entrada nesta sala ou está limitada a convites ou a alguma configuração de espaço." + "Esquecer esta sala" + "Precisas de um convite para entrares nesta sala" "Entrar na sala" + "Podes ter que ser convidado ou pertenceres a um espaço para poderes entrar." "Bater à porta" "Mensagem (opcional)" "Irá receber um convite para participar na sala se seu pedido for aceite." "Pedido de adesão enviado" + "Não conseguimos exibir a pré-visualização da sala. Isso pode ser devido a problemas de rede ou servidor." + "Não foi possível exibir a pré-visualização desta sala" "A %1$s ainda não funciona com espaços. Podes usá-los na aplicação web." "Os espaços ainda não estão implementados" "Carrega no botão abaixo para notificar um administrador da sala. Poderás entrar quando te aprovarem." diff --git a/features/joinroom/impl/src/main/res/values-ru/translations.xml b/features/joinroom/impl/src/main/res/values-ru/translations.xml index b26afac1338..58a94019c15 100644 --- a/features/joinroom/impl/src/main/res/values-ru/translations.xml +++ b/features/joinroom/impl/src/main/res/values-ru/translations.xml @@ -1,12 +1,16 @@ - "Вам был запрещен доступ в комнату %1$s ." + "Вам был запрещен доступ в комнату %1$s." "Вам был запрещен доступ в эту комнату" "Причина: %1$s." "Отменить запрос" "Да, отменить" "Вы действительно хотите отменить заявку на вступление в эту комнату?" "Отменить запрос на присоединение" + "Да, отклонить и заблокировать" + "Вы действительно хотите отклонить приглашение в эту комнате? Это также предотвратит %1$s возможность связываться с вами или приглашать вас в комнаты." + "Отклонить приглашение и заблокировать" + "Отклонить и заблокировать" "Не удалось присоединиться к комнате." "Доступ в эту комнату возможен только по приглашениям или может быть ограничен на уровне пространства." "Забыть эту комнату" @@ -17,6 +21,8 @@ "Сообщение (опционально)" "Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят." "Запрос на присоединение отправлен" + "Не удалось отобразить предварительный просмотр комнаты. Это может быть связано с проблемами сети или сервера." + "Мы не смогли отобразить предварительный просмотр этой комнаты" "%1$s еще не поддерживает пространства. Вы можете получить к ним доступ в веб-версии." "Пространства пока не поддерживаются" "Нажмите кнопку ниже и администратор комнаты получит уведомление. После одобрения вы сможете присоединиться к обсуждению." diff --git a/features/joinroom/impl/src/main/res/values-sk/translations.xml b/features/joinroom/impl/src/main/res/values-sk/translations.xml index 4eeab053dd7..645c523c618 100644 --- a/features/joinroom/impl/src/main/res/values-sk/translations.xml +++ b/features/joinroom/impl/src/main/res/values-sk/translations.xml @@ -7,6 +7,10 @@ "Áno, zrušiť" "Ste si istí, že chcete zrušiť svoju žiadosť o vstup do tejto miestnosti?" "Zrušiť žiadosť o pripojenie" + "Áno, odmietnuť a zablokovať" + "Ste si istí, že chcete odmietnuť pozvanie na vstup do tejto miestnosti? To tiež zabráni tomu, aby vás %1$s kontaktoval/a alebo vás pozval/a do miestností." + "Odmietnuť pozvánku a zablokovať" + "Odmietnuť a zablokovať" "Pripojenie do miestnosti zlyhalo." "Táto miestnosť je buď len pre pozvaných, alebo môžu existovať obmedzenia na prístup na úrovni priestoru." "Zabudnúť túto miestnosť" @@ -17,6 +21,8 @@ "Správa (voliteľné)" "Ak bude vaša žiadosť prijatá, dostanete pozvánku na vstup do miestnosti." "Žiadosť o pripojenie bola odoslaná" + "Nepodarilo sa zobraziť ukážku miestnosti. Môže to byť spôsobené problémami so sieťou alebo serverom." + "Ukážku tejto miestnosti sa nepodarilo zobraziť" "%1$s zatiaľ nepodporuje priestory. K priestorom môžete pristupovať na webe." "Priestory zatiaľ nie sú podporované" "Kliknite na tlačidlo nižšie a správca miestnosti bude informovaný. Po schválení sa budete môcť pripojiť ku konverzácii." diff --git a/features/joinroom/impl/src/main/res/values-sv/translations.xml b/features/joinroom/impl/src/main/res/values-sv/translations.xml index a61bce02a4c..a6aa79ebd96 100644 --- a/features/joinroom/impl/src/main/res/values-sv/translations.xml +++ b/features/joinroom/impl/src/main/res/values-sv/translations.xml @@ -1,11 +1,28 @@ + "Du bannades från det här rummet av %1$s." + "Du bannades från det här rummet" + "Anledning: %1$s." "Avbryt begäran" + "Ja, avbryt" + "Är du säker på att du vill avbryta din begäran om att gå med i det här rummet?" + "Avbryt begäran om att gå med" + "Ja, avvisa och blockera" + "Är du säker på att du vill avvisa inbjudan att gå med i det här rummet? Detta kommer också att hindra %1$s från att kontakta dig eller bjuda in dig till rum." + "Avvisa inbjudan och blockera" + "Avvisa och blockera" + "Misslyckades att gå med i rummet." + "Detta rum är antingen endast för inbjudna eller så kan det finnas begränsningar för åtkomst på utrymmesnivå." + "Glöm det här rummet" + "Du behöver en inbjudan för att gå med i detta rum" "Gå med i rummet" + "Du kan behöva bli inbjuden eller vara medlem i ett utrymme för att gå med." "Knacka för att gå med" "Meddelande (valfritt)" "Du kommer att få en inbjudan att gå med i rummet om din begäran accepteras." "Begäran om att gå med skickad" + "Vi kunde inte visa förhandsgranskningen av rummet. Detta kan bero på nätverks- eller serverproblem." + "Vi kunde inte visa förhandsgranskningen av rummet" "%1$s stöder inte utrymmen än. Du kan komma åt utrymmen på webben." "Utrymmen stöds inte ännu" "Klicka på knappen nedan så kommer en rumsadministratör att meddelas. Du kommer att kunna gå med i konversationen när den har godkänts." diff --git a/features/joinroom/impl/src/main/res/values-tr/translations.xml b/features/joinroom/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..fb3edc84b2c --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,32 @@ + + + "%1$s tarafından bu odadan yasaklandınız." + "Bu odadan yasaklandın" + "Neden: %1$s." + "İsteği iptal et" + "Evet, iptal et" + "Bu odaya katılma isteğinizi iptal etmek istediğinizden emin misiniz?" + "Katılma isteğini iptal et" + "Evet, reddet ve engelle" + "Bu odaya katılma davetini reddetmek istediğinizden emin misiniz? Bu aynı zamanda %1$s sizinle iletişim kurmasını veya sizi odalara davet etmesini de engeller." + "Daveti reddet ve engelle" + "Reddet ve engelle" + "Odaya katılım başarısız oldu." + "Bu odaya yalnızca davetle girilebilir veya alan düzeyinde erişim kısıtlamaları olabilir." + "Bu odayı unut" + "Bu odaya katılmak için bir davete ihtiyacınız var" + "Odaya katıl" + "Katılmak için davet edilmeniz veya bir alana üye olmanız gerekebilir." + "Katılma isteği gönder" + "Mesaj (isteğe bağlı)" + "Talebiniz kabul edilirse odaya katılmanız için bir davet alacaksınız." + "Katılma isteği gönderildi" + "Oda önizlemesini görüntüleyemedik. Bunun nedeni ağ veya sunucu sorunları olabilir." + "Bu oda önizlemesini görüntüleyemedik" + "%1$s henüz alanları desteklemiyor. Alanlara web üzerinden erişebilirsiniz." + "Alanlar henüz desteklenmiyor" + "Aşağıdaki düğmeyi tıkladığınızda bir oda yöneticisi bilgilendirilecektir. Onaylandıktan sonra görüşmeye katılabilirsiniz." + "Mesaj geçmişini görüntülemek için bu odaya üye olmanız gerekmektedir." + "Bu odaya katılmak ister misiniz?" + "Önizleme mevcut değil" + 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 371ed60c94a..72a085042f0 100644 --- a/features/joinroom/impl/src/main/res/values-uk/translations.xml +++ b/features/joinroom/impl/src/main/res/values-uk/translations.xml @@ -1,14 +1,28 @@ + "%1$s забороняє вам відвідувати цю кімнату." + "Вам заборонили відвідувати цю кімнату" + "Причина: %1$s." "Скасувати запит" "Так, скасувати" "Ви впевнені, що бажаєте скасувати свій запит на приєднання до цієї кімнати?" "Скасувати запит на приєднання" + "Так, відхилити та заблокувати" + "Ви впевнені, що хочете відхилити запрошення приєднатися до цієї кімнати? Це також завадить %1$s зв\'язатися з вами або запрошувати вас в кімнати." + "Відхилити запрошення та заблокувати" + "Відхилити та заблокувати" + "Не вдалося приєднатися до кімнати." + "Ця кімната доступна лише за запрошенням або на рівні простору можуть бути обмеження доступу." + "Забути цю кімнату" + "Вам потрібне запрошення, щоб приєднатися до цієї кімнати" "Приєднатися до кімнати" + "Можливо, вам знадобиться отримати запрошення або стати учасником простору, щоб приєднатися." "Постукати, щоб приєднатися" "Повідомлення (необов\'язково)" "Ви отримаєте запрошення приєднатися до кімнати, якщо ваш запит буде прийнятий." "Запит на приєднання надіслано" + "Ми не змогли показати попередній перегляд кімнати. Це може бути пов\'язано з проблемами мережі або сервера." + "Ми не можемо показати попередній перегляд цієї кімнати" "%1$s ще не підтримує простори. Ви можете отримати доступ до них у вебверсії." "Простори поки що не підтримуються" "Натисніть кнопку нижче, і адміністратор кімнати отримає сповіщення. Ви зможете приєднатися до розмови після схвалення." diff --git a/features/joinroom/impl/src/main/res/values-ur/translations.xml b/features/joinroom/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..af34c737107 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,11 @@ + + + "کمرے میں شامل ہوں" + "شامل ہونے کی درخواست بھیجیں" + "%1$s ابھی تک خالی جگہوں کی حمایت نہیں کرتا۔ آپ جال پر خالی جگہوں تک رسائی حاصل کرسکتے ہیں۔" + "ابھی تک جگہیں تعاون یافتہ نہیں" + "نیچے دیئے گئے کلید پر دبائیں اور کمرے کے منتظم کو مطلع کیا جائے گا۔ منظور ہونے کے بعد آپ گفتگو میں شامل ہو سکیں گے۔" + "پیغام کی سرگزشت دیکھنے کے لیے آپ کا اس کمرے کا رکن ہونا ضروری ہے۔" + "اس کمرے میں شامل ہونا چاہتے ہیں؟" + "پیش منظر دستیاب نہیں ہے" + diff --git a/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml index 30c2956a6b1..b176a76e0f2 100644 --- a/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/joinroom/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,14 +1,28 @@ + "您被 %1$s 禁止進入此聊天室。" + "您被禁止進入此聊天室" + "理由:%1$s。" "取消請求" "是的,取消" "您確定您想要取消加入此聊天室的請求嗎?" "取消加入請求" + "是的,拒絕並封鎖" + "您確定要拒絕加入此聊天室的邀請嗎?這也會防止 %1$s 聯絡您或邀請您加入聊天室。" + "拒絕邀請並封鎖" + "拒絕並封鎖" + "加入聊天室失敗。" + "此聊天室僅有受邀者才能進入,或是在空間層級有存取限制。" + "忘記此聊天室" + "您需要獲得邀請才能加入此聊天室" "加入聊天室" + "您可能需要被邀請成為空間的成員才能加入。" "傳送加入請求" "訊息(選擇性)" "若接受了您的請求,您將會收到加入聊天是的邀請。" "已傳送加入請求" + "我們無法顯示聊天室預覽。這可能是因為網路或伺服器問題所致。" + "我們無法顯示此聊天室的預覽" "%1$s 尚未支援空間。您可以在網頁上存取空間。" "尚未支援空間" "點選下方按鈕將會通知聊天試管里員。一旦核准,您就可以加入對話。" diff --git a/features/joinroom/impl/src/main/res/values/localazy.xml b/features/joinroom/impl/src/main/res/values/localazy.xml index 391d63eb2fd..a3e91c542c6 100644 --- a/features/joinroom/impl/src/main/res/values/localazy.xml +++ b/features/joinroom/impl/src/main/res/values/localazy.xml @@ -7,6 +7,10 @@ "Yes, cancel" "Are you sure that you want to cancel your request to join this room?" "Cancel request to join" + "Yes, decline & block" + "Are you sure you want to decline the invite to join this room? This will also prevent %1$s from contacting you or inviting you to rooms." + "Decline invite & block" + "Decline and block" "Joining the room failed." "This room is either invite-only or there might be restrictions to access at space level." "Forget this room" @@ -17,6 +21,8 @@ "Message (optional)" "You will receive an invite to join the room if your request is accepted." "Request to join sent" + "We could not display the room preview. This may be due to network or server issues." + "We couldn’t display this room preview" "%1$s does not support spaces yet. You can access spaces on web." "Spaces are not supported yet" "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved." diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeForgetRoom.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeForgetRoom.kt new file mode 100644 index 00000000000..48d3eec0776 --- /dev/null +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/FakeForgetRoom.kt @@ -0,0 +1,20 @@ +/* + * 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.joinroom.impl + +import io.element.android.features.joinroom.impl.di.ForgetRoom +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.simulateLongTask + +class FakeForgetRoom( + var lambda: (RoomId) -> Result = { Result.success(Unit) } +) : ForgetRoom { + override suspend fun invoke(roomId: RoomId) = simulateLongTask { + lambda(roomId) + } +} diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index 82c4d4f637f..8a475668ae1 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -9,33 +9,48 @@ package io.element.android.features.joinroom.impl import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.anAcceptDeclineInviteState +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState +import io.element.android.features.invite.api.toInviteData +import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.joinroom.impl.di.CancelKnockRoom +import io.element.android.features.joinroom.impl.di.ForgetRoom import io.element.android.features.joinroom.impl.di.KnockRoom import io.element.android.features.roomdirectory.api.RoomDescription 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.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.exception.ClientException +import io.element.android.libraries.matrix.api.exception.ErrorKind import io.element.android.libraries.matrix.api.room.CurrentUserMembership +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.preview.RoomPreviewInfo +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.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SERVER_LIST +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember -import io.element.android.libraries.matrix.test.room.aRoomSummary +import io.element.android.libraries.matrix.test.room.aRoomPreview +import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom +import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.any @@ -43,12 +58,14 @@ 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.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import java.util.Optional +@Suppress("LargeClass") class JoinRoomPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -58,12 +75,10 @@ class JoinRoomPresenterTest { val presenter = createJoinRoomPresenter() presenter.test { awaitItem().also { state -> - assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias())) - assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) + assertThat(state.contentState).isEqualTo(ContentState.Loading) assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Uninitialized) assertThat(state.knockAction).isEqualTo(AsyncAction.Uninitialized) - assertThat(state.applicationName).isEqualTo("AppName") cancelAndIgnoreRemainingEvents() } } @@ -71,46 +86,56 @@ class JoinRoomPresenterTest { @Test fun `present - when room is joined then content state is filled with his data`() = runTest { - val roomSummary = aRoomSummary() - val matrixClient = FakeMatrixClient().apply { - getRoomSummaryFlowLambda = { _ -> - flowOf(Optional.of(roomSummary)) + val roomInfo = aRoomInfo() + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) } } val presenter = createJoinRoomPresenter( matrixClient = matrixClient ) presenter.test { - skipItems(1) + skipItems(2) awaitItem().also { state -> val contentState = state.contentState as ContentState.Loaded assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) - assertThat(contentState.name).isEqualTo(roomSummary.info.name) - assertThat(contentState.topic).isEqualTo(roomSummary.info.topic) - assertThat(contentState.alias).isEqualTo(roomSummary.info.canonicalAlias) - assertThat(contentState.numberOfMembers).isEqualTo(roomSummary.info.activeMembersCount) - assertThat(contentState.isDm).isEqualTo(roomSummary.info.isDirect) - assertThat(contentState.roomAvatarUrl).isEqualTo(roomSummary.info.avatarUrl) + assertThat(contentState.name).isEqualTo(roomInfo.name) + assertThat(contentState.topic).isEqualTo(roomInfo.topic) + assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias) + assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.joinedMembersCount) + assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect) + assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) } } } @Test fun `present - when room is invited then join authorization is equal to invited`() = runTest { - val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED) - val matrixClient = FakeMatrixClient().apply { - getRoomSummaryFlowLambda = { _ -> - flowOf(Optional.of(roomSummary)) + val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) } } + val seenInvitesStore = InMemorySeenInvitesStore() val presenter = createJoinRoomPresenter( - matrixClient = matrixClient + matrixClient = matrixClient, + seenInvitesStore = seenInvitesStore, ) + val inviteData = roomInfo.toInviteData() + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() presenter.test { - skipItems(1) + skipItems(2) awaitItem().also { state -> - assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(null)) + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, null)) } + // Check that the roomId is stored in the seen invites store + assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomInfo.id) } } @@ -118,22 +143,60 @@ class JoinRoomPresenterTest { fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest { val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob") val expectedInviteSender = inviter.toInviteSender() - val roomSummary = aRoomSummary( + val roomInfo = aRoomInfo( currentUserMembership = CurrentUserMembership.INVITED, + joinedMembersCount = 5, inviter = inviter, ) - val matrixClient = FakeMatrixClient().apply { - getRoomSummaryFlowLambda = { _ -> - flowOf(Optional.of(roomSummary)) + val inviteData = roomInfo.toInviteData() + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) } } val presenter = createJoinRoomPresenter( matrixClient = matrixClient ) presenter.test { - skipItems(1) + skipItems(2) awaitItem().also { state -> - assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(expectedInviteSender)) + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, expectedInviteSender)) + assertThat((state.contentState as ContentState.Loaded).numberOfMembers).isEqualTo(5) + } + } + } + + @Test + fun `present - when room is invited read the number of member from the room preview`() = runTest { + val roomInfo = aRoomInfo( + currentUserMembership = CurrentUserMembership.INVITED, + // It seems that the SDK does not provide this value. + joinedMembersCount = 0, + ) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + numberOfJoinedMembers = 10, + ) + ) + ) + }, + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + skipItems(2) + awaitItem().also { state -> + assertThat((state.contentState as ContentState.Loaded).numberOfMembers).isEqualTo(10) } } } @@ -144,12 +207,13 @@ class JoinRoomPresenterTest { val acceptDeclinePresenter = Presenter { anAcceptDeclineInviteState(eventSink = eventSinkRecorder) } - val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED) + val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) val matrixClient = FakeMatrixClient().apply { - getRoomSummaryFlowLambda = { _ -> - flowOf(Optional.of(roomSummary)) + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) } } + val inviteData = roomInfo.toInviteData() val presenter = createJoinRoomPresenter( matrixClient = matrixClient, acceptDeclineInvitePresenter = acceptDeclinePresenter @@ -158,16 +222,14 @@ class JoinRoomPresenterTest { skipItems(1) awaitItem().also { state -> - state.eventSink(JoinRoomEvents.AcceptInvite) - state.eventSink(JoinRoomEvents.DeclineInvite) - - val inviteData = state.contentState.toInviteData()!! + state.eventSink(JoinRoomEvents.AcceptInvite(inviteData)) + state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, false)) assert(eventSinkRecorder) .isCalledExactly(2) .withSequence( listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))), - listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))), + listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true))), ) } } @@ -179,7 +241,11 @@ class JoinRoomPresenterTest { val joinRoomLambda = lambdaRecorder { _: RoomIdOrAlias, _: List, _: JoinedRoom.Trigger -> Result.success(Unit) } + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ) val presenter = createJoinRoomPresenter( + matrixClient = matrixClient, trigger = aTrigger, serverNames = A_SERVER_LIST, joinRoomLambda = joinRoomLambda, @@ -203,7 +269,11 @@ class JoinRoomPresenterTest { @Test fun `present - when room is joined with error, it is possible to clear the error`() = runTest { + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ) val presenter = createJoinRoomPresenter( + matrixClient = matrixClient, joinRoomLambda = { _, _, _ -> Result.failure(AN_EXCEPTION) }, @@ -227,18 +297,89 @@ class JoinRoomPresenterTest { } @Test - fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest { - val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true) - val matrixClient = FakeMatrixClient().apply { - getRoomSummaryFlowLambda = { _ -> - flowOf(Optional.of(roomSummary)) + fun `present - when room is joined with unauthorized error, then the authorisation status is unauthorized`() = runTest { + val roomDescription = aRoomDescription() + val presenter = createJoinRoomPresenter( + roomDescription = Optional.of(roomDescription), + joinRoomLambda = { _, _, _ -> + Result.failure(JoinRoom.Failures.UnauthorizedJoin) + }, + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + state.eventSink(JoinRoomEvents.JoinRoom) + } + awaitItem().also { state -> + assertThat(state.joinAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> + assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(JoinRoom.Failures.UnauthorizedJoin)) + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unauthorized) + } + } + } + + @Test + fun `present - when room is banned, then join authorization is equal to IsBanned`() = runTest { + val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.BANNED, joinRule = JoinRule.Public) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + roomId = A_ROOM_ID, + joinRule = JoinRule.Public, + currentUserMembership = CurrentUserMembership.BANNED, + ), + roomMembershipDetails = { + Result.success( + RoomMembershipDetails( + currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), + senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), + ) + ) + } + ) + ) + } + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) } } val presenter = createJoinRoomPresenter( matrixClient = matrixClient ) presenter.test { + // Skip initial state skipItems(1) + + // Advance until the room info is loaded and the presenter recomposes. The room preview info still needs to be loaded async. + skipItems(1) + + // Now we should have the room info + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isInstanceOf(JoinAuthorisationStatus.IsBanned::class.java) + } + } + } + + @Test + fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest { + val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = JoinRule.Public) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) + } + } + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient + ) + presenter.test { + skipItems(2) awaitItem().also { state -> assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.CanJoin) } @@ -246,18 +387,20 @@ class JoinRoomPresenterTest { } @Test - fun `present - when room is left and not public then join authorization is equal to unknown`() = runTest { - val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false) - val matrixClient = FakeMatrixClient().apply { - getRoomSummaryFlowLambda = { _ -> - flowOf(Optional.of(roomSummary)) + fun `present - when room is left and join rule null then join authorization is equal to Unknown`() = runTest { + val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, joinRule = null) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ).apply { + getRoomInfoFlowLambda = { _ -> + flowOf(Optional.of(roomInfo)) } } val presenter = createJoinRoomPresenter( matrixClient = matrixClient ) presenter.test { - skipItems(1) + skipItems(2) awaitItem().also { state -> assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) } @@ -327,6 +470,20 @@ class JoinRoomPresenterTest { } } + @Test + fun `present - when room preview join rule is Private then join authorization is equal to NeedInvite`() = runTest { + val roomDescription = aRoomDescription(joinRule = RoomDescription.JoinRule.UNKNOWN) + val presenter = createJoinRoomPresenter( + roomDescription = Optional.of(roomDescription) + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) + } + } + } + @Test fun `present - emit knock room event`() = runTest { val knockMessage = "Knock message" @@ -337,7 +494,13 @@ class JoinRoomPresenterTest { Result.failure(RuntimeException("Failed to knock room $roomIdOrAlias")) } val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess) - val presenter = createJoinRoomPresenter(knockRoom = fakeKnockRoom) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ) + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient, + knockRoom = fakeKnockRoom, + ) presenter.test { skipItems(1) awaitItem().also { state -> @@ -376,7 +539,13 @@ class JoinRoomPresenterTest { Result.failure(RuntimeException("Failed to knock room $roomId")) } val cancelKnockRoom = FakeCancelKnockRoom(cancelKnockRoomSuccess) - val presenter = createJoinRoomPresenter(cancelKnockRoom = cancelKnockRoom) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ) + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient, + cancelKnockRoom = cancelKnockRoom, + ) presenter.test { skipItems(1) awaitItem().also { state -> @@ -406,23 +575,118 @@ class JoinRoomPresenterTest { } @Test - fun `present - when room is not known RoomPreview is loaded`() = runTest { + fun `present - emit forget room event`() = runTest { + val forgetRoomSuccess = lambdaRecorder { _: RoomId -> + Result.success(Unit) + } + val forgetRoomFailure = lambdaRecorder { _: RoomId -> + Result.failure(RuntimeException("Failed to forget room")) + } + val fakeForgetRoom = FakeForgetRoom(forgetRoomSuccess) + val matrixClient = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ) + val presenter = createJoinRoomPresenter( + matrixClient = matrixClient, + forgetRoom = fakeForgetRoom, + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + state.eventSink(JoinRoomEvents.ForgetRoom) + } + + assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading) + awaitItem().also { state -> + assertThat(state.forgetAction).isEqualTo(AsyncAction.Success(Unit)) + fakeForgetRoom.lambda = forgetRoomFailure + state.eventSink(JoinRoomEvents.ForgetRoom) + } + + assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading) + awaitItem().also { state -> + assertThat(state.forgetAction).isInstanceOf(AsyncAction.Failure::class.java) + } + } + assert(forgetRoomFailure) + .isCalledOnce() + .with(value(A_ROOM_ID)) + assert(forgetRoomSuccess) + .isCalledOnce() + .with(value(A_ROOM_ID)) + } + + @Test + fun `present - when room is not known RoomPreview is loaded - membership null`() = runTest { val client = FakeMatrixClient( - getRoomPreviewInfoResult = { _, _ -> + getNotJoinedRoomResult = { _, _ -> Result.success( - RoomPreviewInfo( + aRoomPreview( + info = aRoomPreviewInfo( + roomId = A_ROOM_ID, + canonicalAlias = RoomAlias("#alias:matrix.org"), + name = "Room name", + topic = "Room topic", + avatarUrl = "avatarUrl", + numberOfJoinedMembers = 2, + isSpace = false, + isHistoryWorldReadable = false, + joinRule = JoinRule.Public, + currentUserMembership = null, + ) + ) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo( + ContentState.Loaded( roomId = A_ROOM_ID, - canonicalAlias = RoomAlias("#alias:matrix.org"), name = "Room name", topic = "Room topic", - avatarUrl = "avatarUrl", - numberOfJoinedMembers = 2, + alias = RoomAlias("#alias:matrix.org"), + numberOfMembers = 2, + isDm = false, roomType = RoomType.Room, - isHistoryWorldReadable = false, - isJoined = false, - isInvited = false, - isPublic = true, - canKnock = false, + roomAvatarUrl = "avatarUrl", + joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin + ) + ) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded - membership INVITED`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + roomId = A_ROOM_ID, + canonicalAlias = RoomAlias("#alias:matrix.org"), + name = "Room name", + topic = "Room topic", + avatarUrl = "avatarUrl", + numberOfJoinedMembers = 2, + isSpace = false, + isHistoryWorldReadable = false, + joinRule = JoinRule.Public, + currentUserMembership = CurrentUserMembership.INVITED, + ), + roomMembershipDetails = { + Result.success( + RoomMembershipDetails( + currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), + senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), + ) + ) + } ) ) } @@ -443,7 +707,23 @@ class JoinRoomPresenterTest { isDm = false, roomType = RoomType.Room, roomAvatarUrl = "avatarUrl", - joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin + joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited( + inviteData = InviteData( + roomId = A_ROOM_ID, + roomName = "Room name", + isDm = false, + ), + inviteSender = InviteSender( + userId = A_USER_ID_2, + displayName = "Bob", + avatarData = AvatarData( + id = A_USER_ID_2.value, + name = "Bob", + size = AvatarSize.InviteSender, + ), + membershipChangeReason = null, + ), + ) ) ) } @@ -451,10 +731,33 @@ class JoinRoomPresenterTest { } @Test - fun `present - when room is not known RoomPreview is loaded with error`() = runTest { + fun `present - when room is not known RoomPreview is loaded - membership BANNED`() = runTest { val client = FakeMatrixClient( - getRoomPreviewInfoResult = { _, _ -> - Result.failure(AN_EXCEPTION) + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + roomId = A_ROOM_ID, + canonicalAlias = RoomAlias("#alias:matrix.org"), + name = null, + topic = "Room topic", + avatarUrl = "avatarUrl", + numberOfJoinedMembers = 2, + isSpace = false, + isHistoryWorldReadable = false, + joinRule = JoinRule.Public, + currentUserMembership = CurrentUserMembership.BANNED, + ), + roomMembershipDetails = { + Result.success( + RoomMembershipDetails( + currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), + senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), + ) + ) + } + ) + ) } ) val presenter = createJoinRoomPresenter( @@ -464,35 +767,242 @@ class JoinRoomPresenterTest { skipItems(1) awaitItem().also { state -> assertThat(state.contentState).isEqualTo( - ContentState.Failure( - roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(), - error = AN_EXCEPTION + ContentState.Loaded( + roomId = A_ROOM_ID, + name = null, + topic = "Room topic", + alias = RoomAlias("#alias:matrix.org"), + numberOfMembers = 2, + isDm = false, + roomType = RoomType.Room, + roomAvatarUrl = "avatarUrl", + joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned( + banSender = InviteSender( + userId = A_USER_ID_2, + displayName = "Bob", + avatarData = AvatarData( + id = A_USER_ID_2.value, + name = "Bob", + size = AvatarSize.InviteSender, + ), + membershipChangeReason = null, + ), + reason = null, + ) ) ) - state.eventSink(JoinRoomEvents.RetryFetchingContent) } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded - membership KNOCKED`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview( + info = aRoomPreviewInfo( + roomId = A_ROOM_ID, + canonicalAlias = RoomAlias("#alias:matrix.org"), + name = "Room name", + topic = "Room topic", + avatarUrl = "avatarUrl", + numberOfJoinedMembers = 2, + isSpace = false, + isHistoryWorldReadable = false, + joinRule = JoinRule.Public, + currentUserMembership = CurrentUserMembership.KNOCKED, + ), + roomMembershipDetails = { + Result.success( + RoomMembershipDetails( + currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"), + senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"), + ) + ) + } + ) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { skipItems(1) awaitItem().also { state -> assertThat(state.contentState).isEqualTo( - ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()) + ContentState.Loaded( + roomId = A_ROOM_ID, + name = "Room name", + topic = "Room topic", + alias = RoomAlias("#alias:matrix.org"), + numberOfMembers = 2, + isDm = false, + roomType = RoomType.Room, + roomAvatarUrl = "avatarUrl", + joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked + ) + ) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded as Private`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Private)) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.NeedInvite) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded as Custom`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Custom("custom"))) ) } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded as Invite`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Invite)) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.NeedInvite) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded as KnockRestricted`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(emptyList()))) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.CanKnock) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded as Restricted`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(emptyList()))) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Restricted) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded as Space`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.success( + aRoomPreview(info = aRoomPreviewInfo(isSpace = true)) + ) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsSpace("AppName")) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded with error`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.failure(AN_EXCEPTION) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) awaitItem().also { state -> assertThat(state.contentState).isEqualTo( - ContentState.Failure( - roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(), - error = AN_EXCEPTION - ) + ContentState.Failure(error = AN_EXCEPTION) + ) + state.eventSink(JoinRoomEvents.RetryFetchingContent) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo(ContentState.Loading) + } + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo( + ContentState.Failure(error = AN_EXCEPTION) ) } } } @Test - fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest { + fun `present - when room is not known RoomPreview is loaded with error - dismiss`() = runTest { val client = FakeMatrixClient( - getRoomPreviewInfoResult = { _, _ -> - Result.failure(Exception("403")) + getNotJoinedRoomResult = { _, _ -> + Result.failure(AN_EXCEPTION) } ) val presenter = createJoinRoomPresenter( @@ -502,10 +1012,31 @@ class JoinRoomPresenterTest { skipItems(1) awaitItem().also { state -> assertThat(state.contentState).isEqualTo( - ContentState.UnknownRoom( - roomIdOrAlias = A_ROOM_ID.toRoomIdOrAlias(), - ) + ContentState.Failure(error = AN_EXCEPTION) ) + state.eventSink(JoinRoomEvents.DismissErrorAndHideContent) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo(ContentState.Dismissing) + } + } + } + + @Test + fun `present - when room is not known RoomPreview is loaded with error Forbidden`() = runTest { + val client = FakeMatrixClient( + getNotJoinedRoomResult = { _, _ -> + Result.failure(ClientException.MatrixApi(ErrorKind.Forbidden, "403", "Forbidden", null)) + } + ) + val presenter = createJoinRoomPresenter( + matrixClient = client + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.contentState).isEqualTo(ContentState.UnknownRoom) } } } @@ -521,8 +1052,10 @@ class JoinRoomPresenterTest { }, knockRoom: KnockRoom = FakeKnockRoom(), cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(), + forgetRoom: ForgetRoom = FakeForgetRoom(), buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"), - acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() } + acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, + seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), ): JoinRoomPresenter { return JoinRoomPresenter( roomId = roomId, @@ -534,8 +1067,10 @@ class JoinRoomPresenterTest { joinRoom = FakeJoinRoom(joinRoomLambda), knockRoom = knockRoom, cancelKnockRoom = cancelKnockRoom, + forgetRoom = forgetRoom, buildMeta = buildMeta, - acceptDeclineInvitePresenter = acceptDeclineInvitePresenter + acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, + seenInvitesStore = seenInvitesStore, ) } 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 d756cdbb8cb..0205c1cac0f 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 @@ -11,13 +11,20 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.invite.api.InviteData +import io.element.android.features.invite.test.anInviteData import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.ui.model.toInviteSender 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.Rule import org.junit.Test @@ -139,29 +146,63 @@ class JoinRoomViewTest { } @Test - fun `clicking on Accept invitation IsInvited room emits the expected Event`() { + fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() { val eventsRecorder = EventsRecorder() + val inviteData = anInviteData() rule.setJoinRoomView( aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)), + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) rule.clickOn(CommonStrings.action_accept) - eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite) + eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData)) } @Test - fun `clicking on Decline invitation on IsInvited room emits the expected Event`() { + fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() { val eventsRecorder = EventsRecorder() + val inviteData = anInviteData() rule.setJoinRoomView( aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)), + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) rule.clickOn(CommonStrings.action_decline) - eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite) + eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false)) + } + + @Test + fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val inviteData = anInviteData() + val joinRoomState = aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), + canReportRoom = true, + eventSink = eventsRecorder, + ) + ensureCalledOnceWithParam(inviteData) { + rule.setJoinRoomView( + state = joinRoomState, + onDeclineInviteAndBlockUser = it, + ) + 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`() { + val eventsRecorder = EventsRecorder() + val inviteData = anInviteData() + val joinRoomState = aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), + canReportRoom = false, + eventSink = eventsRecorder, + ) + rule.setJoinRoomView(state = joinRoomState) + rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) + eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true)) } @Test @@ -178,7 +219,7 @@ class JoinRoomViewTest { } @Test - fun `clicking on Go back when a space is displayed invokes the expected callback`() { + fun `clicking on ok when a space is displayed invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { rule.setJoinRoomView( @@ -188,9 +229,38 @@ class JoinRoomViewTest { ), onBackClick = it ) - rule.clickOn(CommonStrings.action_go_back) + rule.clickOn(CommonStrings.action_ok) + } + } + + @Test + fun `clicking on ok when user is unauthorized the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setJoinRoomView( + aJoinRoomState( + contentState = aLoadedContentState(), + joinAction = AsyncAction.Failure(JoinRoom.Failures.UnauthorizedJoin), + eventSink = eventsRecorder, + ), + onBackClick = it + ) + rule.clickOn(CommonStrings.action_ok) } } + + @Test + fun `clicking on forget when user is banned invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + rule.setJoinRoomView( + aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(R.string.screen_join_room_forget_action) + eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom) + } } private fun AndroidComposeTestRule.setJoinRoomView( @@ -199,6 +269,8 @@ private fun AndroidComposeTestRule.setJoinR onJoinSuccess: () -> Unit = EnsureNeverCalled(), onKnockSuccess: () -> Unit = EnsureNeverCalled(), onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(), + onForgetSuccess: () -> Unit = EnsureNeverCalled(), + onDeclineInviteAndBlockUser: (InviteData) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { JoinRoomView( @@ -206,7 +278,9 @@ private fun AndroidComposeTestRule.setJoinR onBackClick = onBackClick, onJoinSuccess = onJoinSuccess, onKnockSuccess = onKnockSuccess, - onCancelKnockSuccess = onCancelKnockSuccess + onForgetSuccess = onForgetSuccess, + onCancelKnockSuccess = onCancelKnockSuccess, + onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser, ) } } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt index 186a578945e..992098dc91a 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/AvatarRow.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp 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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toPx @@ -36,6 +37,7 @@ import kotlinx.collections.immutable.toImmutableList * Draw a row of avatars (they must all have the same size), from start to end. * @param avatarDataList the avatars to render. Note: they will all be rendered, the caller may * want to limit the list size + * @param avatarType the type of avatars to render * @param modifier Jetpack Compose modifier * @param overlapRatio the overlap ration. When 0f, avatars will render without overlap, when 1f * only the first avatar will be visible @@ -43,6 +45,7 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun AvatarRow( avatarDataList: ImmutableList, + avatarType: AvatarType, modifier: Modifier = Modifier, overlapRatio: Float = 0.5f, ) { @@ -58,35 +61,36 @@ fun AvatarRow( .forEachIndexed { index, avatarData -> Avatar( modifier = Modifier - .padding(start = avatarSize * (1 - overlapRatio) * (lastItemIndex - index)) - .graphicsLayer { - compositingStrategy = CompositingStrategy.Offscreen + .padding(start = avatarSize * (1 - overlapRatio) * (lastItemIndex - index)) + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen + } + .drawWithContent { + // Draw content and clear the pixels for the avatar on the left (right in RTL). + drawContent() + val xOffset = if (isRtl) { + size.width - avatarSizePx * (overlapRatio - 0.5f) + } else { + 0f + avatarSizePx * (overlapRatio - 0.5f) } - .drawWithContent { - // Draw content and clear the pixels for the avatar on the left (right in RTL). - drawContent() - val xOffset = if (isRtl) { - size.width - avatarSizePx * (overlapRatio - 0.5f) - } else { - 0f + avatarSizePx * (overlapRatio - 0.5f) - } - if (index < lastItemIndex) { - drawCircle( - color = Color.Black, - center = Offset( - x = xOffset, - y = size.height / 2, - ), - radius = avatarSizePx / 2, - blendMode = BlendMode.Clear, - ) - } + if (index < lastItemIndex) { + drawCircle( + color = Color.Black, + center = Offset( + x = xOffset, + y = size.height / 2, + ), + radius = avatarSizePx / 2, + blendMode = BlendMode.Clear, + ) } - .size(size = avatarSize) - // Keep internal padding, it has the advantage to not reduce the size of the Avatar image, - // which is already small in our use case. - .padding(2.dp), + } + .size(size = avatarSize) + // Keep internal padding, it has the advantage to not reduce the size of the Avatar image, + // which is already small in our use case. + .padding(2.dp), avatarData = avatarData, + avatarType = avatarType, ) } } @@ -122,6 +126,7 @@ private fun ContentToPreview(overlapRatio: Float) { size = AvatarSize.RoomListItem, ) }.toImmutableList(), + avatarType = AvatarType.User, overlapRatio = overlapRatio, ) } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt index 204a9dc4d61..737a3b0562c 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt @@ -18,6 +18,7 @@ import io.element.android.features.knockrequests.impl.data.KnockRequestPresentab import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState +import io.element.android.libraries.di.annotations.SessionCoroutineScope import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -28,7 +29,8 @@ private const val ACCEPT_ERROR_DISPLAY_DURATION = 1500L class KnockRequestsBannerPresenter @Inject constructor( private val knockRequestsService: KnockRequestsService, - private val appCoroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, ) : Presenter { @Composable override fun present(): KnockRequestsBannerState { @@ -52,13 +54,13 @@ class KnockRequestsBannerPresenter @Inject constructor( fun handleEvents(event: KnockRequestsBannerEvents) { when (event) { is KnockRequestsBannerEvents.AcceptSingleRequest -> { - appCoroutineScope.acceptSingleKnockRequest( + sessionCoroutineScope.acceptSingleKnockRequest( knockRequests = knockRequests, displayAcceptError = showAcceptError, ) } is KnockRequestsBannerEvents.Dismiss -> { - appCoroutineScope.launch { + sessionCoroutineScope.launch { knockRequestsService.markAllKnockRequestsAsSeen() } } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt index b62b886afa6..334bb531ae9 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.designsystem.components.async.AsyncIndicator import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType 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 @@ -128,14 +129,14 @@ private fun KnockRequestsBannerContent( Text( text = state.formattedTitle(), style = ElementTheme.typography.fontBodyMdMedium, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, textAlign = TextAlign.Start, ) if (state.subtitle != null) { Text( text = state.subtitle, style = ElementTheme.typography.fontBodySmRegular, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, textAlign = TextAlign.Start, ) } @@ -195,7 +196,10 @@ private fun KnockRequestAvatarView( Box(modifier) { when (knockRequests.size) { 0 -> Unit - 1 -> Avatar(knockRequests.first().getAvatarData(AvatarSize.KnockRequestBanner)) + 1 -> Avatar( + avatarData = knockRequests.first().getAvatarData(AvatarSize.KnockRequestBanner), + avatarType = AvatarType.User, + ) else -> KnockRequestAvatarListView(knockRequests) } } @@ -214,6 +218,7 @@ private fun KnockRequestAvatarListView( .toImmutableList() AvatarRow( avatarDataList = avatars, + avatarType = AvatarType.User, modifier = modifier, ) } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt index 901d4968369..4de596a623e 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt @@ -7,7 +7,7 @@ package io.element.android.features.knockrequests.impl.data -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.canBan import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canKick @@ -22,7 +22,7 @@ data class KnockRequestPermissions( val canHandle = canAccept || canDecline || canBan } -fun MatrixRoom.knockRequestPermissionsFlow(): Flow { +fun JoinedRoom.knockRequestPermissionsFlow(): Flow { return syncUpdateFlow.map { val canAccept = canInvite().getOrDefault(false) val canDecline = canKick().getOrDefault(false) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt index dc3e177d7c7..671208e97b3 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt @@ -14,14 +14,14 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom @Module @ContributesTo(RoomScope::class) object KnockRequestsModule { @Provides @SingleIn(RoomScope::class) - fun knockRequestsService(room: MatrixRoom, featureFlagService: FeatureFlagService): KnockRequestsService { + fun knockRequestsService(room: JoinedRoom, featureFlagService: FeatureFlagService): KnockRequestsService { return KnockRequestsService( knockRequestsFlow = room.knockRequestsFlow, permissionsFlow = room.knockRequestPermissionsFlow(), diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt index b56ed8af4ec..a83450e9bde 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt @@ -89,7 +89,7 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider Unit) { TopAppBar( - title = { - Text( - text = stringResource(R.string.screen_knock_requests_list_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(R.string.screen_knock_requests_list_title), navigationIcon = { BackButton(onClick = onBackClick) }, ) } diff --git a/features/knockrequests/impl/src/main/res/values-be/translations.xml b/features/knockrequests/impl/src/main/res/values-be/translations.xml new file mode 100644 index 00000000000..e6f08bc3203 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-be/translations.xml @@ -0,0 +1,4 @@ + + + "Прыняць" + diff --git a/features/knockrequests/impl/src/main/res/values-bg/translations.xml b/features/knockrequests/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 00000000000..108ffb122ea --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,4 @@ + + + "Приемане" + diff --git a/features/knockrequests/impl/src/main/res/values-cy/translations.xml b/features/knockrequests/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..8257ae857b9 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,40 @@ + + + "Iawn, derbyn y cyfan" + "Ydych chi\'n siŵr eich bod am dderbyn pob cais i ymuno?" + "Derbyn pob cais" + "Derbyn y cyfan" + "Doedd dim modd derbyn pob cais. Hoffech chi roi cynnig arall arni?" + "Wedi methu derbyn pob cais" + "Yn derbyn pob cais i ymuno" + "Doedd dim modd derbyn y cais hwn. Hoffech chi roi cynnig arall arni?" + "Wedi methu â derbyn y cais" + "Yn derbyn cais i ymuno" + "Iawn, gwrthod a gwahardd" + "Ydych chi\'n siŵr eich bod am wrthod a gwahardd %1$s? Bydd y defnyddiwr hwn ddim yn gallu gofyn am fynediad i ymuno â\'r ystafell hon eto." + "Gwrthod a gwahardd mynediad" + "Yn gwrthod a gwahardd mynediad" + "Iawn, gwrthod" + "Ydych chi\'n siŵr eich bod am wrthod %1$s cais i ymuno â\'r ystafell hon?" + "Gwrthod mynediad" + "Gwrthod a gwahardd" + "Doedd dim modd i ni wrthod y cais hwn. Hoffech chi roi cynnig arall arni?" + "Wedi methu â gwrthod y cais" + "Yn gwrthod cais i ymuno" + "Pan fydd rhywun yn gofyn am gael ymuno â\'r ystafell, byddwch yn gallu gweld eu cais yma." + "Dim cais i ymuno yn disgwyl" + "Yn llwytho ceisiadau i ymuno…" + "Ceisiadau i ymuno" + + "Dyw %1$s na +%2$d arall eisiau ymuno â\'r ystafell hon" + "Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon" + "Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon" + "Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon" + "Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon" + "Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon" + + "Gweld y cyfan" + "Derbyn" + "Mae %1$s eisiau ymuno â\'r ystafell hon" + "Golwg" + diff --git a/features/knockrequests/impl/src/main/res/values-da/translations.xml b/features/knockrequests/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..30ac6a8d306 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,36 @@ + + + "Ja, acceptér alle" + "Er du sikker på, at du vil acceptere alle anmodninger om at deltage?" + "Acceptér alle anmodninger" + "Acceptér alle" + "Vi kunne ikke acceptere alle anmodninger. Vil du prøve igen?" + "Kunne ikke acceptere alle anmodninger" + "Accepterer alle anmodninger om at deltage" + "Vi kunne ikke acceptere denne anmodning. Vil du prøve igen?" + "Kunne ikke acceptere anmodningen" + "Accepterer anmodning om at deltage" + "Ja, afvis og spær" + "Er du sikker på, at du vil afvise og spærre %1$s? Denne bruger vil ikke kunne anmode om adgang til at deltage i dette rum igen." + "Afvis og spær for deres adgang til rummet" + "Afviser og spærrer for adgang" + "Ja, afvis" + "Er du sikker på, at du vil afvise %1$ss anmodning om at deltage i dette rum?" + "Afvis adgang" + "Afvis og spær" + "Vi kunne ikke afvise denne anmodning. Vil du prøve igen?" + "Anmodningen kunne ikke afvises" + "Afviser anmodning om at deltage" + "Når nogen beder om at deltage i rummet, kan du se deres anmodning her." + "Ingen ventende anmodning om at deltage" + "Indlæser anmodninger om at deltage…" + "Anmodninger om at deltage" + + "%1$s + %2$d anden ønsker at deltage i dette rum" + "%1$s + %2$d andre ønsker at deltage i dette rum" + + "Se alle" + "Accepter" + "%1$s ønsker at deltage i dette rum" + "Se" + diff --git a/features/knockrequests/impl/src/main/res/values-de/translations.xml b/features/knockrequests/impl/src/main/res/values-de/translations.xml index d9047019c05..45df2ecc8f3 100644 --- a/features/knockrequests/impl/src/main/res/values-de/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-de/translations.xml @@ -11,7 +11,7 @@ "Die Anfrage konnte nicht akzeptiert werden" "Beitrittsanfrage annehmen" "Ja, ablehnen und sperren" - "Sind Sie sicher, dass Sie %1$s ablehnen und sperren möchten ? Dieser Benutzer kann keinen erneuten Zugriff auf diesen Raum anfordern." + "Sind Sie sicher, dass Sie %1$s ablehnen und sperren möchten?Dieser Benutzer kann keine erneute Zulassung auf diesen Chatroom anfordern." "Ablehnen und Zugriff verbieten" "Ablehnung und Sperrung des Zugriffs" "Ja, ablehnen" diff --git a/features/knockrequests/impl/src/main/res/values-el/translations.xml b/features/knockrequests/impl/src/main/res/values-el/translations.xml index 326227df48f..2066b2e2930 100644 --- a/features/knockrequests/impl/src/main/res/values-el/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-el/translations.xml @@ -4,26 +4,33 @@ "Σίγουρα θες να αποδεχτείς όλα τα αιτήματα συμμετοχής;" "Αποδοχή όλων των αιτημάτων" "Αποδοχή όλων" + "Δεν μπορέσαμε να δεχτούμε όλα τα αιτήματα. Θες να προσπαθήσεις ξανά;" + "Αποτυχία αποδοχής όλων των αιτημάτων" "Αποδοχή όλων των αιτημάτων συμμετοχής" + "Δεν μπορέσαμε να δεχτούμε αυτό το αίτημα. Θα θέλατε να προσπαθήσετε ξανά;" + "Αποτυχία αποδοχής αιτήματος" "Γίνεται αποδοχή αιτήματος συμμετοχής" "Ναι, απόρριψη και αποκλεισμός" - "Σίγουρα θες να απορρίψειε και να αποκλείσεις τον χρήστη %1$s; Αυτός ο χρήστης δεν θα μπορεί να ζητήσει πρόσβαση για να συμμετάσχει ξανά σε αυτό το δωμάτιο." + "Είστε σίγουροι ότι θέλετε να απορρίψετε και να αποκλείσετε το χρήστη %1$s; Αυτός ο χρήστης δεν θα μπορέσει να ζητήσει ξανά πρόσβαση για να συμμετάσχει σε αυτή την αίθουσα." "Απόρριψη και αποκλεισμός πρόσβασης" "Γίνεται απόρριψη και αποκλεισμός πρόσβασης" "Ναι, απόρριψη" - "Σίγουρα θες να απορρίψεις το αίτημα του χρήστη %1$s να συμμετάσχει στο δωμάτιο;" + "Είστε σίγουροι ότι θέλετε να απορρίψετε το αίτημα του %1$s να συμμετάσχετε σε αυτήν την αίθουσα;" "Απόρριψη πρόσβασης" "Απόρριψη και αποκλεισμός" + "Δεν μπορέσαμε να απορρίψουμε αυτό το αίτημα. Θα θέλατε να προσπαθήσετε ξανά;" + "Απέτυχε η απόρριψη του αιτήματος" "Γίνεται απόρριψη αιτήματος συμμετοχής" - "Όταν κάποιος θα ζητήσει να συμμετάσχει στο δωμάτιο, θα μπορείς να δεις το αίτημά του εδώ." + "Όταν κάποιος θα ζητήσει να συμμετάσχει στην αίθουσα, θα μπορείτε να δείτε το αίτημά του εδώ." "Δεν υπάρχει εκκρεμές αίτημα συμμετοχής" + "Φόρτωση αιτημάτων συμμετοχής…" "Αιτήματα συμμετοχής" - "Οι χρήστες %1$s +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο" - "Οι χρήστες %1$s +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο" + "%1$s +%2$d άλλοι θέλουν να συμμετάσχουν σε αυτή την αίθουσα" + "%1$s +%2$d άλλοι θέλουν να συμμετάσχουν σε αυτή την αίθουσα" "Προβολή όλων" "Αποδοχή" - "Ο χρήστης %1$s θέλει να μπει σε αυτό το δωμάτιο" + "%1$s θέλει να συμμετάσχει σε αυτή την αίθουσα" "Προβολή" diff --git a/features/knockrequests/impl/src/main/res/values-es/translations.xml b/features/knockrequests/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..ea9f74544de --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,36 @@ + + + "Sí, aceptar todas" + "¿Estás seguro de que quieres aceptar todas las solicitudes de unión?" + "Aceptar todas las solicitudes" + "Aceptar todas" + "No pudimos aceptar todas las solicitudes. ¿Quieres volver a intentarlo?" + "No se pudieron aceptar todas las solicitudes" + "Aceptando todas las solicitudes de unión" + "No pudimos aceptar esta solicitud. ¿Quieres volver a intentarlo?" + "No se pudo aceptar la solicitud" + "Aceptando solicitud de unión" + "Sí, rechazar y vetar" + "¿Estás seguro de que quieres rechazar y vetar a %1$s? Este usuario no podrá volver a solicitar acceso para unirse a esta sala." + "Rechazar y vetar el acceso" + "Rechazando y vetando acceso" + "Sí, rechazar" + "¿Estás seguro de que deseas rechazar la solicitud de %1$s para unirse a esta sala?" + "Rechazar el acceso" + "Rechazar y vetar" + "No pudimos rechazar esta solicitud. ¿Quieres volver a intentarlo?" + "No se pudo rechazar la solicitud" + "Rechazando solicitud de unión" + "Cuando alguien pida unirse a la sala, podrás ver su solicitud aquí." + "No hay ninguna solicitud de unión pendiente" + "Cargando solicitudes de unión…" + "Solicitudes de unión" + + "%1$s y %2$d más quieren unirse a esta sala" + "%1$s y otros %2$d más quieren unirse a esta sala" + + "Ver todo" + "Aceptar" + "%1$s quiere unirse a esta sala" + "Ver" + diff --git a/features/knockrequests/impl/src/main/res/values-eu/translations.xml b/features/knockrequests/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..5f4585070cc --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,19 @@ + + + "Bai, onartu guztiak" + "Onartu eskaera guztiak" + "Onartu guztiak" + "Bai, ukatu eta ezarri debekua" + "Bai, ukatu" + "Ukatu sarbidea" + "Ukatu eta ezarri debekua" + "Sartzeko eskaerak" + + "%1$s + beste %2$d gelara batu nahi dute" + "%1$s + beste %2$d gelara batu nahi dute" + + "Ikusi guztiak" + "Onartu" + "%1$s(e)k gela honetara sartu nahi du" + "Ikusi" + diff --git a/features/knockrequests/impl/src/main/res/values-fa/translations.xml b/features/knockrequests/impl/src/main/res/values-fa/translations.xml new file mode 100644 index 00000000000..5b931e4f001 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,23 @@ + + + "بله. پذیرش همه" + "پذیرش همهٔ درخواست‌ها" + "پذیرش همه" + "شکست در پذیرش همهٔ درخواست‌ها" + "پذیرفتن همهٔ درخواست‌های پیوستن" + "شکست در پذیرش درخواست" + "پذیرفتن درخواست پیوستن" + "بله. رد و انسداد" + "رد و تحریم دسترسی" + "رد کردن و تحریم دسترسی" + "بله. رد شود" + "رد کردن دسترسی" + "رد و تحریم" + "شکتس در رد درخواست" + "در کردن درخواست پیوستن" + "درخواست‌های پیوستن" + "دیدن همه" + "پذیرش" + "%1$s می‌خواهد به این اتاق بپیوندد" + "نما" + diff --git a/features/knockrequests/impl/src/main/res/values-hu/translations.xml b/features/knockrequests/impl/src/main/res/values-hu/translations.xml index 2ae4a79857d..c01121ae83d 100644 --- a/features/knockrequests/impl/src/main/res/values-hu/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-hu/translations.xml @@ -24,7 +24,7 @@ "Ha valaki csatlakozni kíván a szobához, itt láthatja a kérését." "Nincs függőben lévő csatlakozási kérelem" "Csatlakozási kérések betöltése…" - "Csatlakozási kérelmek" + "Csatlakozási kérelem" "%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához" "%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához" diff --git a/features/knockrequests/impl/src/main/res/values-in/translations.xml b/features/knockrequests/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..08480082c5b --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,35 @@ + + + "Ya, terima semua" + "Apakah Anda yakin ingin menerima semua permintaan untuk bergabung?" + "Terima semua permintaan" + "Terima semua" + "Kami tidak dapat menerima semua permintaan. Apakah Anda ingin mencoba lagi?" + "Gagal menerima semua permintaan" + "Menerima semua permintaan untuk bergabung" + "Kami tidak dapat menerima permintaan ini. Apakah Anda ingin mencoba lagi?" + "Gagal menerima permintaan" + "Menerima permintaan untuk bergabung" + "Ya, tolak dan cekal" + "Apakah Anda yakin ingin menolak dan mencekal %1$s? Pengguna ini tidak akan dapat meminta akses untuk bergabung ke ruangan ini lagi." + "Tolak dan cekal akses" + "Menolak dan mencekal akses" + "Ya, tolak" + "Apakah Anda yakin ingin menolak permintaan %1$s untuk bergabung ke ruangan ini?" + "Tolak akses" + "Tolak dan cekal" + "Kami tidak dapat menolak permintaan ini. Apakah Anda ingin mencoba lagi?" + "Gagal menolak permintaan" + "Menolak permintaan untuk bergabung" + "Ketika seseorang akan meminta untuk bergabung dengan ruangan, Anda akan dapat melihat permintaan mereka di sini." + "Tidak ada permintaan yang tertunda untuk bergabung" + "Memuat permintaan untuk bergabung…" + "Permintaan untuk bergabung" + + "%1$s +%2$d lainnya ingin bergabung ke ruangan ini" + + "Lihat semua" + "Terima" + "%1$s ingin bergabung ke ruangan ini" + "Lihat" + diff --git a/features/knockrequests/impl/src/main/res/values-ka/translations.xml b/features/knockrequests/impl/src/main/res/values-ka/translations.xml new file mode 100644 index 00000000000..ce6628b264f --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-ka/translations.xml @@ -0,0 +1,4 @@ + + + "მიღება" + diff --git a/features/knockrequests/impl/src/main/res/values-lt/translations.xml b/features/knockrequests/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..df25b7a23a9 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,4 @@ + + + "Priimti" + diff --git a/features/knockrequests/impl/src/main/res/values-nb/translations.xml b/features/knockrequests/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..1616a4b5cae --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,36 @@ + + + "Ja, godta alle" + "Er du sikker på at du vil godta alle forespørsler om å bli med?" + "Godta alle forespørsler" + "Godta alle" + "Vi kunne ikke godta alle forespørsler. Vil du prøve igjen?" + "Kunne ikke godta alle forespørsler" + "Godtar alle forespørsler om å bli med" + "Vi kunne ikke godta denne forespørselen. Vil du prøve igjen?" + "Kunne ikke godta forespørselen" + "Godtar forespørsel om å bli med" + "Ja, avslå og utesteng" + "Er du sikker på at du vil avvise og utestenge %1$s? Denne brukeren vil ikke kunne be om tilgang til dette rommet igjen." + "Avslå og forby tilgang" + "Avslår og forbyr tilgang" + "Ja, avslå" + "Er du sikker på at du vil avslå %1$ss forespørsel om å bli med i dette rommet?" + "Avslå tilgang" + "Avslå og forby" + "Vi kunne ikke avslå denne forespørselen. Vil du prøve igjen?" + "Kunne ikke avslå forespørselen" + "Avslår forespørsel om å bli med" + "Når noen ber om å bli med i rommet, vil du kunne se forespørselen deres her." + "Ingen ventende forespørsel om å bli med" + "Laster inn forespørsler om å bli med…" + "Forespørsler om å bli med" + + "%1$s +%2$d andre ønsker å bli med i dette rommet" + "%1$s +%2$d andre ønsker å bli med i dette rommet" + + "Vis alle" + "Godta" + "%1$s ønsker å bli med i dette rommet" + "Vis" + diff --git a/features/knockrequests/impl/src/main/res/values-nl/translations.xml b/features/knockrequests/impl/src/main/res/values-nl/translations.xml new file mode 100644 index 00000000000..46787d05e0c --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,4 @@ + + + "Accepteren" + diff --git a/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml b/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..b602cedaf5e --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,36 @@ + + + "Sim, aceitar todos" + "Tem certeza de que deseja aceitar todos os pedidos de adesão?" + "Aceitar todos os pedidos" + "Aceitar todos" + "Não pudemos aceitar todas as solicitações. Você gostaria de tentar novamente?" + "Falha ao aceitar todas as solicitações" + "Aceitando todas as solicitações de adesão" + "Não pudemos aceitar essa solicitação. Você gostaria de tentar novamente?" + "Falha ao aceitar a solicitação" + "Aceitando solicitação de adesão" + "Sim, recusar e banir" + "Você tem certeza de que deseja recusar e banir %1$s? Este usuário não poderá solicitar acesso para entrar nesta sala novamente." + "Recusar e proibir o acesso" + "Recusando e proibindo o acesso" + "Sim, recusar" + "Você tem certeza de que deseja recusar a solicitação de %1$s para participar desta sala?" + "Recusar acesso" + "Recusar e banir" + "Não foi possível recusar esta solicitação. Você gostaria de tentar novamente?" + "Falha ao recusar a solicitação" + "Recusando a solicitação de adesão" + "Quando alguém pedir para entrar na sala, você poderá ver o pedido aqui." + "Nenhum pedido pendente de adesão" + "Carregando solicitações para participar…" + "Solicitações para entrar" + + "%1$s +%2$d outro desejam entrar desta sala" + "%1$s +%2$d outros desejam entrar desta sala" + + "Ver tudo" + "Aceitar" + "%1$s quer entrar nesta sala" + "Ver" + diff --git a/features/knockrequests/impl/src/main/res/values-pt/translations.xml b/features/knockrequests/impl/src/main/res/values-pt/translations.xml index 1ba0fe34d33..255c9d52020 100644 --- a/features/knockrequests/impl/src/main/res/values-pt/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-pt/translations.xml @@ -4,15 +4,26 @@ "Tens a certeza de que queres aceitar todos os pedidos de entrada?" "Aceitar todos os pedidos" "Aceitar todos" + "Não foi possível aceitar todos os pedidos. Queres tentar novamente?" + "Falha ao aceitar todos os pedidos" + "A aceitar todos os pedidos de entrada" + "Não foi possível aceitar este pedido. Queres tentar novamente?" + "Falha ao aceitar pedido" + "A aceitar pedido de entrada" "Sim, recusar e proibir" "Tens a certeza de que queres recusar e banir %1$s? Este utilizador não poderá voltar a pedir para entrar nesta sala." "Recusar e banir" - "Sim, recusar" + "A rejeitar pedido e a banir o utilizador" + "Sim, rejeitar" "Tens a certeza que queres recusar o pedido de entrada de %1$s?" - "Recusar entrada" + "Rejeitar entrada" "Recusar e banir" + "Não foi possível rejeitar este pedido. Queres tentar novamente?" + "Falha ao rejeitar pedido" + "A rejeitar pedido de entrada" "Quando alguém pedir para entrar na sala, irás poder rever o pedido aqui." "Sem pedidos de entrada" + "A carregar pedidos de entrada…" "Pedidos de entrada" "%1$s +%2$d outro querem entrar nesta sala" diff --git a/features/knockrequests/impl/src/main/res/values-ro/translations.xml b/features/knockrequests/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..2a75681c5d0 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,4 @@ + + + "Acceptați" + diff --git a/features/knockrequests/impl/src/main/res/values-ru/translations.xml b/features/knockrequests/impl/src/main/res/values-ru/translations.xml index 3447cd0bc73..f5e91a469a5 100644 --- a/features/knockrequests/impl/src/main/res/values-ru/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-ru/translations.xml @@ -24,14 +24,14 @@ "Вы сможете увидеть запрос, когда кто-то попросит присоединиться к комнате." "Нет ожидающих запросов на присоединение" "Загрузка запросов на присоединение…" - "Запросы на присоединение" + "Запросы на вступление" "%1$s +%2$d хочет присоединиться к этой комнате" "%1$s +%2$d хотят присоединиться к этой комнате" "%1$s +%2$d хотят присоединиться к этой комнате" "Показать все" - "Принять" + "Разрешить" "%1$s хочет присоединиться к этой комнате" "Просмотр" diff --git a/features/knockrequests/impl/src/main/res/values-sk/translations.xml b/features/knockrequests/impl/src/main/res/values-sk/translations.xml index e8df24e46b4..12cc86d327c 100644 --- a/features/knockrequests/impl/src/main/res/values-sk/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-sk/translations.xml @@ -24,7 +24,7 @@ "Keď niekto požiada, aby sa pripojil k miestnosti, jeho žiadosť si môžete pozrieť tu." "Žiadna čakajúca žiadosť o pripojenie" "Načítavajú sa žiadosti o pripojenie…" - "Žiadosti o pripojenie" + "Žiadosti o vstup" "%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti" "%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti" diff --git a/features/knockrequests/impl/src/main/res/values-sv/translations.xml b/features/knockrequests/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 00000000000..1b51a8e7077 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,36 @@ + + + "Ja, acceptera alla" + "Är du säker på att du vill acceptera alla förfrågningar om att gå med?" + "Acceptera alla förfrågningar" + "Acceptera alla" + "Vi kunde inte acceptera alla förfrågningar. Vill du försöka igen?" + "Misslyckades att acceptera alla förfrågningar" + "Accepterar alla förfrågningar om att gå med" + "Vi kunde inte acceptera denna begäran. Vill du försöka igen?" + "Misslyckades att acceptera förfrågan" + "Accepterar begäran om att gå med" + "Ja, avslå och förbjud" + "Är du säker på att du vill avvisa och förbjuda%1$s? Den här användaren kommer inte att kunna begära åtkomst för att gå med i det här rummet igen." + "Avvisa och förbjud åtkomst" + "Avvisar och bannar åtkomst" + "Ja, avböj" + "Är du säker på att du vill avslå %1$s begäran om att gå med i det här rummet?" + "Avvisa åtkomst" + "Avvisa och förbjud" + "Vi kunde inte avslå denna begäran. Vill du försöka igen?" + "Misslyckades att avvisa begäran" + "Avvisa begäran om att gå med" + "När någon begär om att gå med i rummet, kan du se deras förfrågan här." + "Ingen väntande begäran om att gå med" + "Laddar förfrågningar om att gå med …" + "Begäran om att gå med" + + "%1$s+ %2$d annan vill gå med i detta rum" + "%1$s+ %2$d andra vill gå med i detta rum" + + "Visa alla" + "Godkänn" + "%1$s vill gå med i det här rummet" + "Visa" + diff --git a/features/knockrequests/impl/src/main/res/values-tr/translations.xml b/features/knockrequests/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..b6a070ee08b --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,36 @@ + + + "Evet, tümünü kabul et" + "Tüm katılma isteklerini kabul etmek istediğinizden emin misiniz?" + "Tüm istekleri kabul et" + "Tümünü kabul et" + "Tüm istekleri kabul edemedik. Tekrar denemek ister misiniz?" + "Tüm istekler kabul edilemedi" + "Tüm katılım istekleri kabul ediliyor" + "Bu isteği kabul edemedik. Tekrar denemek ister misiniz?" + "İstek kabul edilemedi" + "Katılma isteği kabul ediliyor" + "Evet, reddet ve yasakla" + "%1$s reddetmek ve yasaklamak istediğinizden emin misiniz? Bu kullanıcı, bu odaya tekrar katılmak için erişim isteğinde bulunamaz." + "Reddet ve erişimi yasakla" + "Reddediliyor ve erişim yasaklanıyor" + "Evet, reddet" + "Bu odaya katılma isteğini reddetmek istediğinizden emin misiniz %1$s?" + "Erişimi reddet" + "Reddet ve yasakla" + "Bu isteği reddedemedik. Tekrar denemek ister misiniz?" + "İsteği reddetme başarısız oldu" + "Katılma isteği reddediliyor" + "Birisi odaya katılmak istediğinde, isteklerini burada görebileceksiniz." + "Bekleyen katılım isteği yok" + "Katılma istekleri yükleniyor…" + "Katılma istekleri" + + "%1$s +%2$d kişi daha bu odaya katılmak istiyor" + "%1$s +%2$d kişi daha bu odaya katılmak istiyor" + + "Tümünü görüntüle" + "Kabul et" + "%1$s bu odaya katılmak istiyor" + "Görüntüle" + diff --git a/features/knockrequests/impl/src/main/res/values-ur/translations.xml b/features/knockrequests/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..fba633f6fbc --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,4 @@ + + + "قبول کریں" + diff --git a/features/knockrequests/impl/src/main/res/values-uz/translations.xml b/features/knockrequests/impl/src/main/res/values-uz/translations.xml new file mode 100644 index 00000000000..d7e6705e67c --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-uz/translations.xml @@ -0,0 +1,4 @@ + + + "Qabul qiling" + 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 f30db6ad5b7..252a0ceaeb7 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 @@ -238,6 +238,6 @@ private fun TestScope.createKnockRequestsBannerPresenter( ) return KnockRequestsBannerPresenter( knockRequestsService = knockRequestsService, - appCoroutineScope = this, + sessionCoroutineScope = this, ) } 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 d326844206d..5b4b8fb7897 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 @@ -108,7 +108,7 @@ class KnockRequestsListViewTest { rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), - asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")), + asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), currentAction = KnockRequestsAction.AcceptAll, eventSink = eventsRecorder, ), @@ -124,7 +124,7 @@ class KnockRequestsListViewTest { rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), - asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")), + asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), currentAction = KnockRequestsAction.AcceptAll, eventSink = eventsRecorder, ), diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt index e1c5cde2232..28321d1b99e 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt @@ -40,9 +40,9 @@ private fun LeaveRoomConfirmationDialog( is LeaveRoomState.Confirmation.Hidden -> {} is LeaveRoomState.Confirmation.Dm -> LeaveRoomConfirmationDialog( - text = R.string.leave_conversation_alert_subtitle, + text = R.string.leave_room_alert_private_subtitle, roomId = state.confirmation.roomId, - isDm = true, + isDm = false, eventSink = state.eventSink, ) diff --git a/features/leaveroom/api/src/main/res/values-cy/translations.xml b/features/leaveroom/api/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..1ce4f2f3b48 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-cy/translations.xml @@ -0,0 +1,7 @@ + + + "Ydych chi\'n siŵr eich bod am adael y sgwrs hon? Dyw\'r sgwrs hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad." + "Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Chi yw\'r unig berson yma. Os byddwch yn gadael, fydd neb yn gallu ymuno yn y dyfodol, gan gynnwys chi." + "Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Dyw\'r ystafell hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad." + "Ydych chi\'n siŵr eich bod am adael yr ystafell?" + diff --git a/features/leaveroom/api/src/main/res/values-da/translations.xml b/features/leaveroom/api/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..5a1364be0c3 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-da/translations.xml @@ -0,0 +1,7 @@ + + + "Er du sikker på, at du vil forlade denne samtale? Denne samtale er ikke offentlig, og du kan ikke deltage igen uden en invitation." + "Er du sikker på, at du vil forlade dette rum? Du er den eneste person her. Hvis du går, vil ingen kunne tilslutte sig det i fremtiden, heller ikke dig." + "Er du sikker på, at du vil forlade dette rum? Rummet er ikke offentligt, så du vil ikke kunne deltage igen uden en invitation." + "Er du sikker på, at du ønsker at forlade rummet?" + diff --git a/features/leaveroom/api/src/main/res/values-de/translations.xml b/features/leaveroom/api/src/main/res/values-de/translations.xml index 5c4928257c9..9a9a53a769d 100644 --- a/features/leaveroom/api/src/main/res/values-de/translations.xml +++ b/features/leaveroom/api/src/main/res/values-de/translations.xml @@ -1,7 +1,7 @@ - "Bist du sicher, dass du diese Unterhaltung verlassen willst? Diese Unterhaltung ist nicht öffentlich und du kannst ihr ohne Einladung nicht wieder beitreten." - "Bist du sicher, dass du diesen Raum verlassen möchtest? Du bist die einzige Person hier. Wenn du gehst, kann in Zukunft niemand mehr eintreten, auch du nicht." - "Bist du sicher, dass du diesen Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten." - "Bist du sicher, dass du den Raum verlassen willst?" + "Sind Sie sicher, dass Sie diesen Chat verlassen wollen? Dieser Chat ist nicht öffentlich und Sie können ihn ohne Einladung nicht wieder betreten." + "Sind Sie sicher dass Sie diesen Chatroom verlassen möchten? Sie sind die einzige Person hier. Wenn Sie gehen, kann in Zukunft niemand mehr - auch Sie nicht - diesen Chatrooom betreten.." + "Sind Sie sicher dass Sie diesen Chatroom verlassen möchten? Dieser Chatroom ist nicht öffentlich und Sie können ihn ohne Einladung nicht wieder betreten." + "Sind Sie sicher, dass Sie den Raum verlassen möchten?" diff --git a/features/leaveroom/api/src/main/res/values-el/translations.xml b/features/leaveroom/api/src/main/res/values-el/translations.xml index 09295f805bd..b6ea1a6caf9 100644 --- a/features/leaveroom/api/src/main/res/values-el/translations.xml +++ b/features/leaveroom/api/src/main/res/values-el/translations.xml @@ -1,7 +1,7 @@ "Είσαι σίγουρος ότι θέλεις να αποχωρήσεις από αυτή τη συζήτηση; Αυτή η συνομιλία δεν είναι δημόσια και δεν θα μπορείς να συμμετάσχεις ξανά χωρίς πρόσκληση." - "Σίγουρα θες να φύγεις από αυτό το δωμάτιο; Είσαι το μόνο άτομο εδώ. Εάν φύγεις, κανείς δεν θα μπορεί να συμμετάσχει στο μέλλον, ούτε και \'σύ." - "Σίγουρα θες να φύγεις από αυτό το δωμάτιο; Αυτό το δωμάτιο δεν είναι δημόσιο και δεν θα μπορείς να συμμετάσχεις ξανά χωρίς πρόσκληση." - "Θες σίγουρα να φύγεις από το δωμάτιο;" + "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από αυτή την αίθουσα; Είστε το μόνο άτομο εδώ. Αν αποχωρήσετε, κανείς δεν θα μπορεί να ενταχθεί στο μέλλον, ούτε εσείς." + "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από αυτήν την αίθουσα; Αυτή η αίθουσα δεν είναι δημόσια και δεν θα μπορέσετε να επανενταχτείτε χωρίς πρόσκληση." + "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από την αίθουσα;" diff --git a/features/leaveroom/api/src/main/res/values-es/translations.xml b/features/leaveroom/api/src/main/res/values-es/translations.xml index 25c753201c1..81b169e5c96 100644 --- a/features/leaveroom/api/src/main/res/values-es/translations.xml +++ b/features/leaveroom/api/src/main/res/values-es/translations.xml @@ -2,6 +2,6 @@ "¿Estás seguro de que quieres salir de esta conversación? Esta conversación no es pública y no podrás volver a unirte sin una invitación." "¿Estás seguro de que quieres salir de esta sala? Eres la única persona aquí. Si te vas, nadie podrá unirse en el futuro, ni siquiera tú." - "¿Estás seguro de que quieres abandonar esta sala? Esta sala no es pública y no podrás volver a entrar sin una invitación." - "¿Seguro que quieres salir de la habitación?" + "¿Seguro que quieres salir de esta sala? Esta sala no es pública y no podrás volver a entrar sin una invitación." + "¿Seguro que quieres salir de la sala?" diff --git a/features/leaveroom/api/src/main/res/values-eu/translations.xml b/features/leaveroom/api/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..df735861b84 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-eu/translations.xml @@ -0,0 +1,7 @@ + + + "Ziur elkarrizketa utzi nahi duzula? Ez da publikoa eta ezingo zara berriro batu gonbidapenik gabe." + "Ziur gelatik irten nahi duzula? Dagoen pertsona bakarra zara. Ateratzen bazara ezingo da inor batu etorkizunean, ezta zeu ere." + "Ziur gelatik irten nahi duzula? Gela hau ez da publikoa eta ezingo zara berriro batu gonbidapenik gabe." + "Ziur gelatik atera nahi duzula?" + diff --git a/features/leaveroom/api/src/main/res/values-fa/translations.xml b/features/leaveroom/api/src/main/res/values-fa/translations.xml new file mode 100644 index 00000000000..3c5a4cddecd --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-fa/translations.xml @@ -0,0 +1,6 @@ + + + "آیا مطمئن هستید که می خواهید این اتاق را ترک کنید؟ شما تنها کسی هستید که اینجا هستید. اگر ترک کنید، هیچ کس نمی تواند در آینده به آن بپیوندد.از جمله خود شما." + "آیا مطمئن هستید که می خواهید از این اتاق خارج شوید؟ این اتاق عمومی نیست و نمی‌توانید بدون دعوت دوباره بپیوندید." + "آیا مطمئن هستید که می خواهید اتاق را ترک کنید؟" + diff --git a/features/leaveroom/api/src/main/res/values-ka/translations.xml b/features/leaveroom/api/src/main/res/values-ka/translations.xml index bb2c4e6ed04..f039365a381 100644 --- a/features/leaveroom/api/src/main/res/values-ka/translations.xml +++ b/features/leaveroom/api/src/main/res/values-ka/translations.xml @@ -1,5 +1,6 @@ + "დარწმუნებული ხართ ამ საუბრის დატოვებაში? იგი საჯარო არაა და მოწვევის გარეშე ხელახლა ვერ შეურთდებით." "დარწმუნებული ბრძანდებით, რომ ამ ოთახის დატოვება გსურთ? თქვენ აქ მარტო ხართ და ჩატის დატოვებისას აქ თქვენს ჩათვლით ვერავინ ვერ გაწევრიანდება." "დარწმუნებული ბრძანდებით, რომ ამ ოთახის დატოვება გსურთ? ეს ოთახი არ არის საჯარო და მოწვევის გარეშე ვერ შეძლებთ ხელახლა გაწევრიანებას." "დარწმუნებული ბრძანდებით, რომ ოთახის დატოვება გსურთ?" diff --git a/features/leaveroom/api/src/main/res/values-lt/translations.xml b/features/leaveroom/api/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..c204c280831 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-lt/translations.xml @@ -0,0 +1,6 @@ + + + "Ar tikrai norite išeiti iš šio kambario? Jūs esate vienintelis žmogus jame. Jei išeisite, niekas negalės prisijungti ateityje, įskaitant Jus." + "Ar tikrai norite išeiti iš šio kambario? Šis kambarys nėra viešas ir negalėsite vėl prisijungti be kvietimo." + "Ar tikrai norite išeiti iš kambario?" + diff --git a/features/leaveroom/api/src/main/res/values-nb/translations.xml b/features/leaveroom/api/src/main/res/values-nb/translations.xml index 5b1b4c74c75..1e1800e0d03 100644 --- a/features/leaveroom/api/src/main/res/values-nb/translations.xml +++ b/features/leaveroom/api/src/main/res/values-nb/translations.xml @@ -1,5 +1,6 @@ + "Er du sikker på at du vil forlate denne samtalen? Denne samtalen er ikke offentlig, og du vil ikke kunne bli med igjen uten en invitasjon." "Er du sikker på at du vil forlate dette rommet? Du er den eneste personen her. Hvis du forlater, vil ingen kunne bli med i fremtiden, inkludert deg." "Er du sikker på at du vil forlate dette rommet? Dette rommet er ikke offentlig, og du vil ikke kunne bli med igjen uten en invitasjon." "Er du sikker på at du vil forlate rommet?" diff --git a/features/leaveroom/api/src/main/res/values-pt-rBR/translations.xml b/features/leaveroom/api/src/main/res/values-pt-rBR/translations.xml index bb4f35d07f5..b55010ad807 100644 --- a/features/leaveroom/api/src/main/res/values-pt-rBR/translations.xml +++ b/features/leaveroom/api/src/main/res/values-pt-rBR/translations.xml @@ -1,5 +1,6 @@ + "Tem certeza de que deseja sair dessa conversa? Essa conversa não é pública e você não poderá entrar novamente sem um convite." "Tem certeza de que deseja sair desta sala? Você é a única pessoa aqui. Se você sair, ninguém poderá ingressar no futuro, inclusive você." "Tem certeza de que deseja sair desta sala? Esta sala não é pública e você não poderá entrar novamente sem um convite." "Tem certeza de que deseja sair da sala?" diff --git a/features/leaveroom/api/src/main/res/values-tr/translations.xml b/features/leaveroom/api/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..39ba596628f --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-tr/translations.xml @@ -0,0 +1,7 @@ + + + "Bu sohbetten ayrılmak istediğinizden emin misiniz? Bu sohbet herkese açık değil ve davet olmadan tekrar katılamayacaksınız." + "Bu odadan ayrılmak istediğinizden emin misiniz? Burada tek kişi sizsiniz. Ayrılırsanız, siz de dahil olmak üzere gelecekte kimse katılamayacak." + "Bu odadan ayrılmak istediğinizden emin misiniz? Bu oda herkese açık değildir ve davet olmadan tekrar katılamazsınız." + "Odadan çıkmak istediğinden emin misin?" + diff --git a/features/leaveroom/api/src/main/res/values-ur/translations.xml b/features/leaveroom/api/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..5a6cfeae7f0 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-ur/translations.xml @@ -0,0 +1,7 @@ + + + "کیا آپ واقعی اس گفتگو کو چھوڑنا چاہتے ہیں؟ یہ گفتگو عوامی نہیں ہے اور آپ دعوت نامے کے بغیر دوبارہ شامل نہیں ہو سکیں گے۔" + "کیا آپ واقعی یہ کمرہ چھوڑنا چاہتے ہیں؟ آپ یہاں واحد شخص ہیں۔ اگر آپ چھوڑتے ہیں، تو مستقبل میں کوئی بھی شامل نہیں ہو سکے گا، آپ سمیت۔" + "کیا آپ واقعی یہ کمرہ چھوڑنا چاہتے ہیں؟ یہ کمرہ عوامی نہیں اور آپ دعوت نامے کے بغیر پھر شامل نہیں ہو پائیں گے۔" + "کیا آپ واقعی کمرہ چھوڑنا چاہتے ہیں؟" + diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt index a567b2d3d75..915d65b71fc 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 @@ -23,6 +23,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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.isDm +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -74,10 +75,12 @@ private suspend fun showLeaveRoomAlert( confirmation: MutableState, ) { matrixClient.getRoom(roomId)?.use { room -> + val roomInfo = room.roomInfoFlow.first() confirmation.value = when { - room.isDm -> Dm(roomId) - !room.isPublic -> PrivateRoom(roomId) - room.joinedMemberCount == 1L -> LastUserInRoom(roomId) + roomInfo.isDm -> Dm(roomId) + // If unknown, assume the room is private + roomInfo.isPublic == null || roomInfo.isPublic == false -> PrivateRoom(roomId) + roomInfo.joinedMembersCount == 1L -> LastUserInRoom(roomId) else -> Generic(roomId) } } 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 new file mode 100644 index 00000000000..108c2ddb53b --- /dev/null +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt @@ -0,0 +1,245 @@ +/* + * 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. + */ + +package io.element.android.features.leaveroom.impl + +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.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +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.testCoroutineDispatchers +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 + +@OptIn(ExperimentalCoroutinesApi::class) +class LeaveBaseRoomPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state hides all dialogs`() = runTest { + val presenter = createLeaveRoomPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Hidden) + assertThat(initialState.progress).isEqualTo(LeaveRoomState.Progress.Hidden) + assertThat(initialState.error).isEqualTo(LeaveRoomState.Error.Hidden) + } + } + + @Test + fun `present - show generic confirmation`() = runTest { + val presenter = createLeaveRoomPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom().apply { + givenRoomInfo(aRoomInfo(isDirect = false, isPublic = true, joinedMembersCount = 10)) + } + ) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Generic(A_ROOM_ID)) + } + } + + @Test + fun `present - show private room confirmation`() = runTest { + val presenter = createLeaveRoomPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom().apply { + givenRoomInfo(aRoomInfo(isPublic = false)) + }, + ) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID)) + } + } + + @Test + fun `present - show last user in room confirmation`() = runTest { + val presenter = createLeaveRoomPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom().apply { + givenRoomInfo(aRoomInfo(joinedMembersCount = 1)) + }, + ) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID)) + } + } + + @Test + fun `present - show DM confirmation`() = runTest { + val presenter = createLeaveRoomPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom().apply { + givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 2)) + }, + ) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Dm(A_ROOM_ID)) + } + } + + @Test + fun `present - leaving a room leaves the room`() = runTest { + val leaveRoomLambda = lambdaRecorder> { Result.success(Unit) } + val presenter = createLeaveRoomPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom( + leaveRoomLambda = leaveRoomLambda + ), + ) + }, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + advanceUntilIdle() + cancelAndIgnoreRemainingEvents() + assert(leaveRoomLambda) + .isCalledOnce() + .withNoParameter() + } + } + + @Test + fun `present - show error if leave room fails`() = runTest { + val presenter = createLeaveRoomPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom( + leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) } + ), + ) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + skipItems(1) // Skip show progress state + val errorState = awaitItem() + assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - show progress indicator while leaving a room`() = runTest { + val presenter = createLeaveRoomPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom( + leaveRoomLambda = { Result.success(Unit) } + ), + ) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + val progressState = awaitItem() + assertThat(progressState.progress).isEqualTo(LeaveRoomState.Progress.Shown) + val finalState = awaitItem() + assertThat(finalState.progress).isEqualTo(LeaveRoomState.Progress.Hidden) + } + } + + @Test + fun `present - hide error hides the error`() = runTest { + val presenter = createLeaveRoomPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeBaseRoom( + leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) } + ), + ) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + skipItems(1) // Skip show progress state + val errorState = awaitItem() + assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) + skipItems(1) // Skip hide progress state + errorState.eventSink(LeaveRoomEvent.HideError) + val hiddenErrorState = awaitItem() + assertThat(hiddenErrorState.error).isEqualTo(LeaveRoomState.Error.Hidden) + } + } +} + +private fun TestScope.createLeaveRoomPresenter( + client: MatrixClient = FakeMatrixClient(), +): LeaveRoomPresenter = LeaveRoomPresenter( + client = client, + dispatchers = testCoroutineDispatchers(false), +) diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt deleted file mode 100644 index e9d229ad934..00000000000 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt +++ /dev/null @@ -1,236 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.leaveroom.impl - -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.leaveroom.api.LeaveRoomEvent -import io.element.android.features.leaveroom.api.LeaveRoomState -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -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.testCoroutineDispatchers -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 - -@OptIn(ExperimentalCoroutinesApi::class) -class LeaveRoomPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state hides all dialogs`() = runTest { - val presenter = createLeaveRoomPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Hidden) - assertThat(initialState.progress).isEqualTo(LeaveRoomState.Progress.Hidden) - assertThat(initialState.error).isEqualTo(LeaveRoomState.Error.Hidden) - } - } - - @Test - fun `present - show generic confirmation`() = runTest { - val presenter = createLeaveRoomPresenter( - client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom() - ) - } - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) - val confirmationState = awaitItem() - assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Generic(A_ROOM_ID)) - } - } - - @Test - fun `present - show private room confirmation`() = runTest { - val presenter = createLeaveRoomPresenter( - client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom(isPublic = false), - ) - } - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) - val confirmationState = awaitItem() - assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID)) - } - } - - @Test - fun `present - show last user in room confirmation`() = runTest { - val presenter = createLeaveRoomPresenter( - client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom(joinedMemberCount = 1), - ) - } - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) - val confirmationState = awaitItem() - assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID)) - } - } - - @Test - fun `present - show DM confirmation`() = runTest { - val presenter = createLeaveRoomPresenter( - client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom(activeMemberCount = 2, isDirect = true), - ) - } - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) - val confirmationState = awaitItem() - assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Dm(A_ROOM_ID)) - } - } - - @Test - fun `present - leaving a room leaves the room`() = runTest { - val leaveRoomLambda = lambdaRecorder> { Result.success(Unit) } - val presenter = createLeaveRoomPresenter( - client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom( - leaveRoomLambda = leaveRoomLambda - ), - ) - }, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) - advanceUntilIdle() - cancelAndIgnoreRemainingEvents() - assert(leaveRoomLambda) - .isCalledOnce() - .withNoParameter() - } - } - - @Test - fun `present - show error if leave room fails`() = runTest { - val presenter = createLeaveRoomPresenter( - client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom( - leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) } - ), - ) - } - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) - skipItems(1) // Skip show progress state - val errorState = awaitItem() - assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `present - show progress indicator while leaving a room`() = runTest { - val presenter = createLeaveRoomPresenter( - client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom( - leaveRoomLambda = { Result.success(Unit) } - ), - ) - } - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) - val progressState = awaitItem() - assertThat(progressState.progress).isEqualTo(LeaveRoomState.Progress.Shown) - val finalState = awaitItem() - assertThat(finalState.progress).isEqualTo(LeaveRoomState.Progress.Hidden) - } - } - - @Test - fun `present - hide error hides the error`() = runTest { - val presenter = createLeaveRoomPresenter( - client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom( - leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) } - ), - ) - } - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) - skipItems(1) // Skip show progress state - val errorState = awaitItem() - assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) - skipItems(1) // Skip hide progress state - errorState.eventSink(LeaveRoomEvent.HideError) - val hiddenErrorState = awaitItem() - assertThat(hiddenErrorState.error).isEqualTo(LeaveRoomState.Error.Hidden) - } - } -} - -private fun TestScope.createLeaveRoomPresenter( - client: MatrixClient = FakeMatrixClient(), -): LeaveRoomPresenter = LeaveRoomPresenter( - client = client, - dispatchers = testCoroutineDispatchers(false), -) diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt index 9267064f895..063579414fc 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt @@ -23,7 +23,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar @OptIn(ExperimentalMaterial3Api::class) @@ -37,7 +36,7 @@ fun DependenciesDetailsView( modifier = modifier, topBar = { TopAppBar( - title = { Text(text = licenseItem.safeName) }, + titleStr = licenseItem.safeName, navigationIcon = { BackButton(onClick = onBack) }, ) }, diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt index 09313ddfa8d..a0d3a19ed2b 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt @@ -17,6 +17,7 @@ import io.element.android.features.licenses.impl.LicensesProvider import io.element.android.features.licenses.impl.model.DependencyLicenseItem import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.extensions.runCatchingExceptions import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import javax.inject.Inject @@ -34,7 +35,7 @@ class DependencyLicensesListPresenter @Inject constructor( } var filter by remember { mutableStateOf("") } LaunchedEffect(Unit) { - runCatching { + runCatchingExceptions { licenses = AsyncData.Success(licensesProvider.provides().toPersistentList()) }.onFailure { licenses = AsyncData.Failure(it) diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt index 109878cffa9..3419665b659 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt @@ -7,7 +7,6 @@ package io.element.android.features.licenses.impl.list -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -15,7 +14,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedTextField import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,10 +31,11 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.ui.strings.CommonStrings -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun DependencyLicensesListView( state: DependencyLicensesListState, @@ -48,7 +47,7 @@ fun DependencyLicensesListView( modifier = modifier, topBar = { TopAppBar( - title = { Text(text = stringResource(CommonStrings.common_open_source_licenses)) }, + titleStr = stringResource(CommonStrings.common_open_source_licenses), navigationIcon = { BackButton(onClick = onBackClick) }, ) }, @@ -60,7 +59,7 @@ fun DependencyLicensesListView( ) { if (state.licenses.isSuccess()) { // Search field - OutlinedTextField( + TextField( value = state.filter, onValueChange = { state.eventSink(DependencyLicensesListEvent.SetFilter(it)) }, leadingIcon = { diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts index 998529088a9..d2998871653 100644 --- a/features/location/api/build.gradle.kts +++ b/features/location/api/build.gradle.kts @@ -5,6 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ +import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.readLocalProperty plugins { @@ -15,28 +17,45 @@ plugins { android { namespace = "io.element.android.features.location.api" + buildFeatures { + buildConfig = true + } + defaultConfig { - resValue( - type = "string", - name = "maptiler_api_key", - value = System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY") - ?: readLocalProperty("services.maptiler.apikey") + buildConfigFieldStr( + name = "MAPTILER_BASE_URL", + value = BuildTimeConfig.SERVICES_MAPTILER_BASE_URL ?: "https://api.maptiler.com/maps" + ) + buildConfigFieldStr( + name = "MAPTILER_API_KEY", + value = if (isEnterpriseBuild) { + BuildTimeConfig.SERVICES_MAPTILER_APIKEY + } else { + System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY") + ?: readLocalProperty("services.maptiler.apikey") + } ?: "" ) - resValue( - type = "string", - name = "maptiler_light_map_id", - value = System.getenv("ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID") - ?: readLocalProperty("services.maptiler.lightMapId") - // fall back to maptiler's default light map. + buildConfigFieldStr( + name = "MAPTILER_LIGHT_MAP_ID", + value = if (isEnterpriseBuild) { + BuildTimeConfig.SERVICES_MAPTILER_LIGHT_MAPID + } else { + System.getenv("ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID") + ?: readLocalProperty("services.maptiler.lightMapId") + } + // fall back to maptiler's default light map. ?: "basic-v2" ) - resValue( - type = "string", - name = "maptiler_dark_map_id", - value = System.getenv("ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID") - ?: readLocalProperty("services.maptiler.darkMapId") - // fall back to maptiler's default dark map. + buildConfigFieldStr( + name = "MAPTILER_DARK_MAP_ID", + value = if (isEnterpriseBuild) { + BuildTimeConfig.SERVICES_MAPTILER_DARK_MAPID + } else { + System.getenv("ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID") + ?: readLocalProperty("services.maptiler.darkMapId") + } + // fall back to maptiler's default dark map. ?: "basic-v2-dark" ) } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt new file mode 100644 index 00000000000..847bdf39217 --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt @@ -0,0 +1,12 @@ +/* + * 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.location.api + +interface LocationService { + fun isServiceAvailable(): Boolean +} 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 900679afbab..382a65ae473 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 @@ -11,6 +11,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember @@ -22,9 +23,10 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp -import coil.compose.AsyncImagePainter -import coil.compose.rememberAsyncImagePainter -import coil.request.ImageRequest +import coil3.Extras +import coil3.compose.AsyncImagePainter +import coil3.compose.rememberAsyncImagePainter +import coil3.request.ImageRequest import io.element.android.compound.theme.ElementTheme import io.element.android.features.location.api.internal.StaticMapPlaceholder import io.element.android.features.location.api.internal.StaticMapUrlBuilder @@ -33,7 +35,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.utils.CommonDrawables -import timber.log.Timber /** * Shows a static map image downloaded via a third party service's static maps API. @@ -56,7 +57,7 @@ fun StaticMapView( ) { val context = LocalContext.current var retryHash by remember { mutableIntStateOf(0) } - val builder = remember { StaticMapUrlBuilder(context) } + 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). @@ -75,14 +76,15 @@ fun StaticMapView( ) ) .size(width = constraints.maxWidth, height = constraints.maxHeight) - .setParameter("retry_hash", retryHash, memoryCacheKey = null) + .apply { + extras.set(Extras.Key("retry_hash"), retryHash).build() + } .build() - }.apply { - Timber.d("Static map image request: ${this?.data}") } ) - if (painter.state is AsyncImagePainter.State.Success) { + val collectedState = painter.state.collectAsState() + if (collectedState.value is AsyncImagePainter.State.Success) { Image( painter = painter, contentDescription = contentDescription, @@ -100,7 +102,8 @@ fun StaticMapView( ) } else { StaticMapPlaceholder( - showProgress = painter.state is AsyncImagePainter.State.Loading, + showProgress = collectedState.value.isLoading(), + canReload = builder.isServiceAvailable(), contentDescription = contentDescription, width = maxWidth, height = maxHeight, @@ -110,6 +113,11 @@ fun StaticMapView( } } +private fun AsyncImagePainter.State.isLoading(): Boolean { + return this is AsyncImagePainter.State.Empty || + this is AsyncImagePainter.State.Loading +} + @PreviewsDayNight @Composable internal fun StaticMapViewPreview() = ElementPreview { diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerConfig.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerConfig.kt deleted file mode 100644 index 9580280d743..00000000000 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerConfig.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.location.api.internal - -import android.content.Context -import io.element.android.features.location.api.R - -internal const val MAPTILER_BASE_URL = "https://api.maptiler.com/maps" - -internal fun Context.mapId(darkMode: Boolean) = when (darkMode) { - true -> getString(R.string.maptiler_dark_map_id) - false -> getString(R.string.maptiler_light_map_id) -} - -internal val Context.apiKey: String - get() = getString(R.string.maptiler_api_key) diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt index 9b621cbc8b8..eb8d0ea1b77 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt @@ -7,7 +7,7 @@ package io.element.android.features.location.api.internal -import android.content.Context +import io.element.android.features.location.api.BuildConfig import kotlin.math.roundToInt /** @@ -16,14 +16,16 @@ import kotlin.math.roundToInt * https://docs.maptiler.com/cloud/api/static-maps/ */ internal class MapTilerStaticMapUrlBuilder( + private val baseUrl: String, private val apiKey: String, private val lightMapId: String, private val darkMapId: String, ) : StaticMapUrlBuilder { - constructor(context: Context) : this( - apiKey = context.apiKey, - lightMapId = context.mapId(darkMode = false), - darkMapId = context.mapId(darkMode = true), + constructor() : this( + baseUrl = BuildConfig.MAPTILER_BASE_URL.removeSuffix("/"), + apiKey = BuildConfig.MAPTILER_API_KEY, + lightMapId = BuildConfig.MAPTILER_LIGHT_MAP_ID, + darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID, ) override fun build( @@ -55,8 +57,10 @@ internal class MapTilerStaticMapUrlBuilder( // image smaller than the available space in pixels. // The resulting image will have to be scaled to fit the available space in order // to keep the perceived content size constant at the expense of sharpness. - return "$MAPTILER_BASE_URL/$mapId/static/$lon,$lat,$finalZoom/${finalWidth}x${finalHeight}$scale.webp?key=$apiKey&attribution=bottomleft" + return "$baseUrl/$mapId/static/$lon,$lat,$finalZoom/${finalWidth}x${finalHeight}$scale.webp?key=$apiKey&attribution=bottomleft" } + + override fun isServiceAvailable() = apiKey.isNotEmpty() } private fun coerceWidthAndHeight(width: Int, height: Int, is2x: Boolean): Pair { diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt index 3146d589e4a..db75b5e30a5 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt @@ -9,21 +9,23 @@ package io.element.android.features.location.api.internal -import android.content.Context +import io.element.android.features.location.api.BuildConfig internal class MapTilerTileServerStyleUriBuilder( + private val baseUrl: String, private val apiKey: String, private val lightMapId: String, private val darkMapId: String, ) : TileServerStyleUriBuilder { - constructor(context: Context) : this( - apiKey = context.apiKey, - lightMapId = context.mapId(darkMode = false), - darkMapId = context.mapId(darkMode = true), + constructor() : this( + baseUrl = BuildConfig.MAPTILER_BASE_URL.removeSuffix("/"), + apiKey = BuildConfig.MAPTILER_API_KEY, + lightMapId = BuildConfig.MAPTILER_LIGHT_MAP_ID, + darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID, ) override fun build(darkMode: Boolean): String { val mapId = if (darkMode) darkMapId else lightMapId - return "$MAPTILER_BASE_URL/$mapId/style.json?key=$apiKey" + return "$baseUrl/$mapId/style.json?key=$apiKey" } } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/ModifierCenterBottomEdge.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/ModifierCenterBottomEdge.kt index 747eda0baec..40112cea4f7 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/ModifierCenterBottomEdge.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/ModifierCenterBottomEdge.kt @@ -15,13 +15,13 @@ import androidx.compose.ui.unit.IntOffset * Horizontally aligns the content to the center of the space. * Vertically aligns the bottom edge of the content to the center of the space. */ -fun Modifier.centerBottomEdge(scope: BoxScope): Modifier = with(scope) { - then( +fun Modifier.centerBottomEdge(scope: BoxScope): Modifier = this.then( + with(scope) { Modifier.align { size, space, _ -> IntOffset( x = (space.width - size.width) / 2, y = space.height / 2 - size.height, ) } - ) -} + } +) 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 0035a043c64..6ea4e9f9dea 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 @@ -9,32 +9,32 @@ package io.element.android.features.location.api.internal import androidx.compose.foundation.Image 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.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Refresh import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import io.element.android.compound.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.designsystem.utils.BooleanProvider import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun StaticMapPlaceholder( showProgress: Boolean, + canReload: Boolean, contentDescription: String?, width: Dp, height: Dp, @@ -55,12 +55,12 @@ internal fun StaticMapPlaceholder( ) if (showProgress) { CircularProgressIndicator() - } else { + } else if (canReload) { Column( horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( - imageVector = Icons.Default.Refresh, + imageVector = CompoundIcons.Restart(), contentDescription = null ) Text(text = stringResource(id = CommonStrings.action_static_map_load)) @@ -71,14 +71,24 @@ internal fun StaticMapPlaceholder( @PreviewsDayNight @Composable -internal fun StaticMapPlaceholderPreview( - @PreviewParameter(BooleanProvider::class) values: Boolean -) = ElementPreview { - StaticMapPlaceholder( - showProgress = values, - contentDescription = null, - width = 400.dp, - height = 400.dp, - onLoadMapClick = {}, - ) +internal fun StaticMapPlaceholderPreview() = ElementPreview { + Column( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + listOf( + true to false, + false to true, + false to false, + ).forEach { (showProgress, canReload) -> + StaticMapPlaceholder( + showProgress = showProgress, + canReload = canReload, + contentDescription = null, + width = 400.dp, + height = 200.dp, + onLoadMapClick = {}, + ) + } + } } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt index 3911e996c40..533ecf8d2d3 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt @@ -7,8 +7,6 @@ package io.element.android.features.location.api.internal -import android.content.Context - /** * Builds an URL for a 3rd party service provider static maps API. */ @@ -22,6 +20,8 @@ interface StaticMapUrlBuilder { height: Int, density: Float, ): String + + fun isServiceAvailable(): Boolean } -fun StaticMapUrlBuilder(context: Context): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder(context = context) +fun StaticMapUrlBuilder(): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder() diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt index 4d93a8d8bb5..051b2448d44 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt @@ -7,10 +7,8 @@ package io.element.android.features.location.api.internal -import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import io.element.android.compound.theme.ElementTheme /** @@ -24,7 +22,7 @@ interface TileServerStyleUriBuilder { ): String } -fun TileServerStyleUriBuilder(context: Context): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder(context = context) +fun TileServerStyleUriBuilder(): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder() /** * Provides and remembers a style URI for a MapLibre compatible tile server. @@ -33,9 +31,8 @@ fun TileServerStyleUriBuilder(context: Context): TileServerStyleUriBuilder = Map */ @Composable fun rememberTileStyleUrl(): String { - val context = LocalContext.current val darkMode = !ElementTheme.isLightTheme return remember(darkMode) { - TileServerStyleUriBuilder(context).build(darkMode) + TileServerStyleUriBuilder().build(darkMode) } } diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt index 30c65c3b1f8..ba44426a701 100644 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt @@ -12,11 +12,28 @@ import org.junit.Test class MapTilerStaticMapUrlBuilderTest { private val builder = MapTilerStaticMapUrlBuilder( + baseUrl = "https://base.url", apiKey = "anApiKey", lightMapId = "aLightMapId", darkMapId = "aDarkMapId", ) + @Test + fun `isServiceAvailable returns true if api key is not empty`() { + assertThat(builder.isServiceAvailable()).isTrue() + } + + @Test + fun `isServiceAvailable returns false if api key is empty`() { + val builderWithoutKey = MapTilerStaticMapUrlBuilder( + baseUrl = "https://base.url", + apiKey = "", + lightMapId = "aLightMapId", + darkMapId = "aDarkMapId", + ) + assertThat(builderWithoutKey.isServiceAvailable()).isFalse() + } + @Test fun `static map 1x density`() { assertThat( @@ -29,7 +46,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 600, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -44,7 +61,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 900, density = 1.5f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -59,7 +76,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 1200, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -74,7 +91,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 1800, density = 3f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -89,7 +106,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 2048, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/2048x1024.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/2048x1024.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -101,7 +118,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 4096, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x2048.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/1024x2048.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -113,7 +130,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 2048, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x512@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/1024x512@2x.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -125,7 +142,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 4096, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/512x1024@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/512x1024@2x.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -137,7 +154,7 @@ class MapTilerStaticMapUrlBuilderTest { height = Int.MAX_VALUE, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x1024@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/1024x1024@2x.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -152,7 +169,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 0, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -164,7 +181,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 0, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/0x0@2x.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -176,6 +193,6 @@ class MapTilerStaticMapUrlBuilderTest { height = Int.MIN_VALUE, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft") } } diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt index f65c0445409..dc53765836a 100644 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt @@ -12,6 +12,7 @@ import org.junit.Test class MapTilerTileServerStyleUriBuilderTest { private val builder = MapTilerTileServerStyleUriBuilder( + baseUrl = "https://base.url", apiKey = "anApiKey", lightMapId = "aLightMapId", darkMapId = "aDarkMapId", @@ -21,13 +22,13 @@ class MapTilerTileServerStyleUriBuilderTest { fun `light map uri`() { assertThat( builder.build(darkMode = false) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/style.json?key=anApiKey") + ).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey") } @Test fun `dark map uri`() { assertThat( builder.build(darkMode = true) - ).isEqualTo("https://api.maptiler.com/maps/aDarkMapId/style.json?key=anApiKey") + ).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey") } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt new file mode 100644 index 00000000000..e662ef81150 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt @@ -0,0 +1,21 @@ +/* + * 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.location.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.location.api.BuildConfig +import io.element.android.features.location.api.LocationService +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultLocationService @Inject constructor() : LocationService { + override fun isServiceAvailable(): Boolean { + return BuildConfig.MAPTILER_API_KEY.isNotEmpty() + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt index b059a88dbb9..21cc8df7bdd 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt @@ -11,12 +11,15 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.annotation.VisibleForTesting +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.location.api.Location import io.element.android.libraries.androidutils.system.openAppSettingsPage +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import timber.log.Timber +import java.util.Locale import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -24,8 +27,8 @@ class AndroidLocationActions @Inject constructor( @ApplicationContext private val context: Context ) : LocationActions { override fun share(location: Location, label: String?) { - runCatching { - val uri = Uri.parse(buildUrl(location, label)) + runCatchingExceptions { + val uri = buildUrl(location, label).toUri() val showMapsIntent = Intent(Intent.ACTION_VIEW).setData(uri) val chooserIntent = Intent.createChooser(showMapsIntent, null) chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -42,17 +45,14 @@ class AndroidLocationActions @Inject constructor( } } +// Ref: https://developer.android.com/guide/components/intents-common#ViewMap @VisibleForTesting internal fun buildUrl( location: Location, label: String?, urlEncoder: (String) -> String = Uri::encode ): String { - // Ref: https://developer.android.com/guide/components/intents-common#ViewMap - val base = "geo:0,0?q=%.6f,%.6f".format(location.lat, location.lon) - return if (label == null) { - base - } else { - "%s (%s)".format(base, urlEncoder(label)) - } + // This is needed so the coordinates are formatted with a dot as decimal separator + val locale = Locale.ENGLISH + return "geo:0,0?q=%.6f,%.6f (%s)".format(locale, location.lat, location.lon, urlEncoder(label.orEmpty())) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt index f5f6190855e..4710e4bd50d 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt @@ -24,15 +24,19 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.message.ReplyParameters +import io.element.android.libraries.matrix.api.room.message.replyInThread +import io.element.android.libraries.matrix.ui.messages.reply.eventId +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.launch import javax.inject.Inject class SendLocationPresenter @Inject constructor( permissionsPresenterFactory: PermissionsPresenter.Factory, - private val room: MatrixRoom, + private val room: JoinedRoom, private val analyticsService: AnalyticsService, private val messageComposerContext: MessageComposerContext, private val locationActions: LocationActions, @@ -98,15 +102,28 @@ class SendLocationPresenter @Inject constructor( event: SendLocationEvents.SendLocation, mode: SendLocationState.Mode, ) { + val replyMode = messageComposerContext.composerMode as? MessageComposerMode.Reply + val replyParams = replyMode?.replyToDetails?.let { details -> + if (replyMode.inThread) { + replyInThread(details.eventId()) + } else { + ReplyParameters( + inReplyToEventId = details.eventId(), + enforceThreadReply = false, + replyWithinThread = false + ) + } + } when (mode) { SendLocationState.Mode.PinLocation -> { val geoUri = event.cameraPosition.toGeoUri() - room.sendLocation( + room.liveTimeline.sendLocation( body = generateBody(geoUri), geoUri = geoUri, description = null, zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(), - assetType = AssetType.PIN + assetType = AssetType.PIN, + replyParameters = replyParams, ) analyticsService.capture( Composer( @@ -119,12 +136,13 @@ class SendLocationPresenter @Inject constructor( } SendLocationState.Mode.SenderLocation -> { val geoUri = event.toGeoUri() - room.sendLocation( + room.liveTimeline.sendLocation( body = generateBody(geoUri), geoUri = geoUri, description = null, zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(), - assetType = AssetType.SENDER + assetType = AssetType.SENDER, + replyParameters = replyParams, ) analyticsService.capture( Composer( diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt index 8f5ef7897c6..32df5af6c50 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt @@ -20,6 +20,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ListItem import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -28,7 +30,6 @@ 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.Location import io.element.android.features.location.api.internal.centerBottomEdge @@ -40,14 +41,11 @@ import io.element.android.features.location.impl.common.PermissionRationaleDialo 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.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberStandardBottomSheetState import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.maplibre.compose.CameraMode import io.element.android.libraries.maplibre.compose.CameraMoveStartedReason @@ -162,12 +160,7 @@ fun SendLocationView( sheetSwipeEnabled = false, topBar = { TopAppBar( - title = { - Text( - text = stringResource(CommonStrings.screen_share_location_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(CommonStrings.screen_share_location_title), navigationIcon = { BackButton(onClick = navigateUp) }, 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 380d5853523..03d2a3c714b 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,9 +12,6 @@ import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.LocationSearching -import androidx.compose.material.icons.filled.MyLocation import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -24,7 +21,6 @@ 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.compound.tokens.generated.TypographyTokens import io.element.android.features.location.api.internal.rememberTileStyleUrl @@ -34,7 +30,6 @@ import io.element.android.features.location.impl.common.PermissionRationaleDialo 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.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton @@ -104,12 +99,7 @@ fun ShowLocationView( modifier = modifier, topBar = { TopAppBar( - title = { - Text( - text = stringResource(CommonStrings.screen_view_location_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(CommonStrings.screen_view_location_title), navigationIcon = { BackButton( onClick = onBackClick, @@ -132,8 +122,8 @@ fun ShowLocationView( onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) }, ) { when (state.isTrackMyLocation) { - false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null) - true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null) + false -> Icon(imageVector = CompoundIcons.LocationNavigator(), contentDescription = null) + true -> Icon(imageVector = CompoundIcons.LocationNavigatorCentred(), contentDescription = null) } } }, diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt new file mode 100644 index 00000000000..fd687dd938a --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt @@ -0,0 +1,22 @@ +/* + * 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.location.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.api.BuildConfig +import org.junit.Test + +class DefaultLocationServiceTest { + @Test + fun `isServiceAvailable should return value depending on BuildConfig MAPTILER_API_KEY`() { + val locationService = DefaultLocationService() + assertThat(locationService.isServiceAvailable()).isEqualTo( + BuildConfig.MAPTILER_API_KEY.isNotEmpty() + ) + } +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt index 4a98e221675..5b584a39f88 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt @@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.location.api.Location import org.junit.Test import java.net.URLEncoder +import java.util.Locale internal class AndroidLocationActionsTest { // We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests @@ -25,7 +26,7 @@ internal class AndroidLocationActionsTest { ) val actual = buildUrl(location, null, ::urlEncoder) - val expected = "geo:0,0?q=1.234568,123.456789" + val expected = "geo:0,0?q=1.234568,123.456789 ()" assertThat(actual).isEqualTo(expected) } @@ -57,4 +58,20 @@ internal class AndroidLocationActionsTest { assertThat(actual).isEqualTo(expected) } + + @Test + fun `buildUrl - URL encodes coordinates in locale with comma decimal separator`() { + val location = Location( + lat = 1.000001, + lon = 2.000001, + accuracy = 0f + ) + // Set a locale with comma as decimal separator + Locale.setDefault(Locale.Category.FORMAT, Locale("pt", "BR")) + + val actual = buildUrl(location, "(weird/stuff here)", ::urlEncoder) + val expected = "geo:0,0?q=1.000001,2.000001 (%28weird%2Fstuff+here%29)" + + assertThat(actual).isEqualTo(expected) + } } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt index 69a8d2d032d..7addee38e4a 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -20,12 +20,14 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE 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.messages.test.FakeMessageComposerContext -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.message.ReplyParameters 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.core.aBuildMeta -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule @@ -47,12 +49,12 @@ class SendLocationPresenterTest { private val fakeBuildMeta = aBuildMeta(applicationName = "app name") private fun createSendLocationPresenter( - matrixRoom: MatrixRoom = FakeMatrixRoom(), + joinedRoom: JoinedRoom = FakeJoinedRoom(), ): SendLocationPresenter = SendLocationPresenter( permissionsPresenterFactory = object : PermissionsPresenter.Factory { override fun create(permissions: List): PermissionsPresenter = fakePermissionsPresenter }, - room = matrixRoom, + room = joinedRoom, analyticsService = fakeAnalyticsService, messageComposerContext = fakeMessageComposerContext, locationActions = fakeLocationActions, @@ -262,13 +264,15 @@ class SendLocationPresenterTest { @Test fun `share sender location`() = runTest { - val sendLocationResult = lambdaRecorder> { _, _, _, _, _ -> + val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ -> Result.success(Unit) } - val matrixRoom = FakeMatrixRoom( - sendLocationResult = sendLocationResult, + val joinedRoom = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendLocationLambda = sendLocationResult + }, ) - val sendLocationPresenter = createSendLocationPresenter(matrixRoom) + val sendLocationPresenter = createSendLocationPresenter(joinedRoom) fakePermissionsPresenter.givenState( aPermissionsState( permissions = PermissionsState.Permissions.AllGranted, @@ -307,6 +311,7 @@ class SendLocationPresenterTest { value(null), value(15), value(AssetType.SENDER), + value(null), ) assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1) @@ -323,13 +328,15 @@ class SendLocationPresenterTest { @Test fun `share pin location`() = runTest { - val sendLocationResult = lambdaRecorder> { _, _, _, _, _ -> + val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ -> Result.success(Unit) } - val matrixRoom = FakeMatrixRoom( - sendLocationResult = sendLocationResult, + val joinedRoom = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendLocationLambda = sendLocationResult + }, ) - val sendLocationPresenter = createSendLocationPresenter(matrixRoom) + val sendLocationPresenter = createSendLocationPresenter(joinedRoom) fakePermissionsPresenter.givenState( aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, @@ -368,6 +375,7 @@ class SendLocationPresenterTest { value(null), value(15), value(AssetType.PIN), + value(null), ) assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1) @@ -384,13 +392,15 @@ class SendLocationPresenterTest { @Test fun `composer context passes through analytics`() = runTest { - val sendLocationResult = lambdaRecorder> { _, _, _, _, _ -> + val sendLocationResult = lambdaRecorder> { _, _, _, _, _, _ -> Result.success(Unit) } - val matrixRoom = FakeMatrixRoom( - sendLocationResult = sendLocationResult, + val joinedRoom = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendLocationLambda = sendLocationResult + }, ) - val sendLocationPresenter = createSendLocationPresenter(matrixRoom) + val sendLocationPresenter = createSendLocationPresenter(joinedRoom) fakePermissionsPresenter.givenState( aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, diff --git a/features/location/test/build.gradle.kts b/features/location/test/build.gradle.kts new file mode 100644 index 00000000000..024ae2c3037 --- /dev/null +++ b/features/location/test/build.gradle.kts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.location.test" +} + +dependencies { + implementation(projects.features.location.api) +} diff --git a/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt new file mode 100644 index 00000000000..8ba29a65aa0 --- /dev/null +++ b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt @@ -0,0 +1,16 @@ +/* + * 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.location.test + +import io.element.android.features.location.api.LocationService + +class FakeLocationService( + private val isServiceAvailable: Boolean, +) : LocationService { + override fun isServiceAvailable() = isServiceAvailable +} diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 6e8784b72cb..ad77b20e600 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -14,6 +14,10 @@ plugins { android { namespace = "io.element.android.features.lockscreen.impl" + + testOptions { + unitTests.isIncludeAndroidResources = true + } } setupAnvil() @@ -21,6 +25,7 @@ setupAnvil() dependencies { api(projects.features.lockscreen.api) implementation(projects.appconfig) + implementation(projects.features.enterprise.api) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) @@ -29,6 +34,8 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.cryptography.api) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.testtags) + implementation(projects.libraries.uiUtils) implementation(projects.features.logout.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.sessionStorage.api) @@ -41,6 +48,9 @@ dependencies { testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) + testImplementation(libs.androidx.compose.ui.test.junit) + testImplementation(libs.androidx.test.ext.junit) testImplementation(projects.libraries.matrix.test) testImplementation(projects.tests.testutils) testImplementation(projects.libraries.cryptography.test) @@ -49,4 +59,5 @@ dependencies { testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.services.appnavstate.test) testImplementation(projects.features.logout.test) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } 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 fa2a5b493e3..85ec2e31307 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 @@ -17,6 +17,7 @@ import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.storage.LockScreenStore import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +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.sessionstorage.api.observer.SessionListener @@ -42,6 +43,7 @@ class DefaultLockScreenService @Inject constructor( private val featureFlagService: FeatureFlagService, private val lockScreenStore: LockScreenStore, private val pinCodeManager: PinCodeManager, + @AppCoroutineScope private val coroutineScope: CoroutineScope, private val sessionObserver: SessionObserver, private val appForegroundStateService: AppForegroundStateService, @@ -94,7 +96,7 @@ class DefaultLockScreenService @Inject constructor( */ private fun observeAppForegroundState() { coroutineScope.launch { - appForegroundStateService.start() + appForegroundStateService.startObservingForeground() appForegroundStateService.isInForeground.collect { isInForeground -> if (isInForeground) { lockJob?.cancel() 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 e3fc7148d13..7358cf248e5 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 @@ -13,6 +13,7 @@ import androidx.biometric.BiometricPrompt.CryptoObject import androidx.biometric.BiometricPrompt.PromptInfo import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.api.SecretKeyRepository import kotlinx.coroutines.CancellationException @@ -119,7 +120,7 @@ private class AuthenticationCallback( private fun Cipher?.isValid(): Boolean { if (this == null) return false - return runCatching { + return runCatchingExceptions { doFinal("biometric_challenge".toByteArray()) }.isSuccess } 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 62de1f902d4..a817faf3533 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 @@ -31,6 +31,7 @@ import io.element.android.libraries.cryptography.api.SecretKeyRepository import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -47,6 +48,7 @@ class DefaultBiometricAuthenticatorManager @Inject constructor( private val lockScreenConfig: LockScreenConfig, private val encryptionDecryptionService: EncryptionDecryptionService, private val secretKeyRepository: SecretKeyRepository, + @AppCoroutineScope private val coroutineScope: CoroutineScope, ) : BiometricAuthenticatorManager { private val callbacks = CopyOnWriteArrayList() @@ -87,7 +89,9 @@ class DefaultBiometricAuthenticatorManager @Inject constructor( @Composable override fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator { - val isBiometricAllowed by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false) + val isBiometricAllowed by remember { + lockScreenStore.isBiometricUnlockAllowed() + }.collectAsState(initial = false) val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState() val isAvailable by remember(lifecycleState) { derivedStateOf { isBiometricAllowed && hasAvailableAuthenticator } 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 f9a2cd87679..9f44b03a100 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 @@ -20,6 +20,7 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricAuthentica import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.storage.LockScreenStore import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.annotations.AppCoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -29,6 +30,7 @@ class LockScreenSettingsPresenter @Inject constructor( private val pinCodeManager: PinCodeManager, private val lockScreenStore: LockScreenStore, private val biometricAuthenticatorManager: BiometricAuthenticatorManager, + @AppCoroutineScope private val coroutineScope: CoroutineScope, ) : Presenter { @Composable @@ -38,7 +40,9 @@ class LockScreenSettingsPresenter @Inject constructor( value = !lockScreenConfig.isPinMandatory && hasPinCode } } - val isBiometricEnabled by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false) + val isBiometricEnabled by remember { + lockScreenStore.isBiometricUnlockAllowed() + }.collectAsState(initial = false) var showRemovePinConfirmation by remember { mutableStateOf(false) } 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 c0a70bac666..a843525d008 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 @@ -11,16 +11,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.compound.theme.ElementTheme import io.element.android.features.lockscreen.impl.R import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun LockScreenSettingsView( @@ -35,15 +36,19 @@ fun LockScreenSettingsView( modifier = modifier ) { PreferenceCategory(showTopDivider = false) { - PreferenceText( - title = stringResource(id = R.string.screen_app_lock_settings_change_pin), - onClick = onChangePinClick + ListItem( + headlineContent = { + Text(stringResource(id = R.string.screen_app_lock_settings_change_pin)) + }, + onClick = onChangePinClick, ) PreferenceDivider() if (state.showRemovePinOption) { - PreferenceText( - title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), - tintColor = ElementTheme.colors.textCriticalPrimary, + ListItem( + headlineContent = { + Text(stringResource(id = R.string.screen_app_lock_settings_remove_pin)) + }, + style = ListItemStyle.Destructive, onClick = { state.eventSink(LockScreenSettingsEvents.OnRemovePin) } 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 d01c045e447..81bfa4c1a43 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 @@ -16,11 +16,10 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -29,6 +28,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource 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.lockscreen.impl.R import io.element.android.features.lockscreen.impl.components.PinEntryTextField import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure @@ -89,7 +89,7 @@ private fun SetupPinHeader( stringResource(id = R.string.screen_app_lock_setup_choose_pin) }, subTitle = stringResource(id = R.string.screen_app_lock_setup_pin_context, appName), - iconStyle = BigIcon.Style.Default(Icons.Filled.Lock), + iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()), ) } } @@ -125,6 +125,7 @@ private fun SetupPinContent( } @Composable +@ReadOnlyComposable private fun SetupPinFailure.content(): String { return when (this) { SetupPinFailure.ForbiddenPin -> stringResource(id = R.string.screen_app_lock_setup_pin_forbidden_dialog_content) @@ -133,6 +134,7 @@ private fun SetupPinFailure.content(): String { } @Composable +@ReadOnlyComposable private fun SetupPinFailure.title(): String { return when (this) { SetupPinFailure.ForbiddenPin -> stringResource(id = R.string.screen_app_lock_setup_pin_forbidden_dialog_title) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt index 9f7baed365a..cea53bb844d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt @@ -7,11 +7,9 @@ package io.element.android.features.lockscreen.impl.unlock -import android.app.Activity import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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 @@ -19,8 +17,6 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.compound.theme.ElementTheme -import io.element.android.features.logout.api.util.onSuccessLogout import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @@ -42,8 +38,6 @@ class PinUnlockNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = presenter.present() - val activity = LocalContext.current as Activity - val isDark = ElementTheme.isLightTheme.not() LaunchedEffect(state.isUnlocked) { if (state.isUnlocked) { onUnlock() @@ -54,7 +48,6 @@ class PinUnlockNode @AssistedInject constructor( // UnlockNode is only used for in-app unlock, so we can safely set isInAppUnlock to true. // It's set to false in PinUnlockActivity. isInAppUnlock = true, - onSuccessLogout = { onSuccessLogout(activity, isDark, it) }, modifier = modifier ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 129d76f4262..707ca6b710e 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 @@ -26,6 +26,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.di.annotations.AppCoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -34,6 +35,7 @@ class PinUnlockPresenter @Inject constructor( private val pinCodeManager: PinCodeManager, private val biometricAuthenticatorManager: BiometricAuthenticatorManager, private val logoutUseCase: LogoutUseCase, + @AppCoroutineScope private val coroutineScope: CoroutineScope, private val pinUnlockHelper: PinUnlockHelper, ) : Presenter { @@ -53,7 +55,7 @@ class PinUnlockPresenter @Inject constructor( mutableStateOf(false) } val signOutAction = remember { - mutableStateOf>(AsyncAction.Uninitialized) + mutableStateOf>(AsyncAction.Uninitialized) } var biometricUnlockResult by remember { mutableStateOf(null) @@ -169,7 +171,7 @@ class PinUnlockPresenter @Inject constructor( } } - private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { + private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { logoutUseCase.logout(ignoreSdkError = true) }.runCatchingUpdatingState(signOutAction) 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 191e3151d98..dd24fbefd76 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 @@ -18,7 +18,7 @@ data class PinUnlockState( val showWrongPinTitle: Boolean, val remainingAttempts: AsyncData, val showSignOutPrompt: Boolean, - val signOutAction: AsyncAction, + val signOutAction: AsyncAction, val showBiometricUnlock: Boolean, val isUnlocked: Boolean, val biometricUnlockResult: BiometricAuthenticator.AuthenticationResult?, 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 51c05820dd8..54647eaad87 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 @@ -23,27 +23,29 @@ open class PinUnlockStateProvider : PreviewParameterProvider { aPinUnlockState(showWrongPinTitle = true), aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showBiometricUnlock = false), - aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), + aPinUnlockState(showSignOutPrompt = true, remainingAttempts = AsyncData.Success(0)), aPinUnlockState(signOutAction = AsyncAction.Loading), - aPinUnlockState(biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure( - BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled") - )), + aPinUnlockState( + biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure( + BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled") + ) + ), ) } fun aPinUnlockState( pinEntry: PinEntry = PinEntry.createEmpty(4), - remainingAttempts: Int = 3, + remainingAttempts: AsyncData = AsyncData.Success(3), showWrongPinTitle: Boolean = false, showSignOutPrompt: Boolean = false, showBiometricUnlock: Boolean = true, biometricUnlockResult: BiometricAuthenticator.AuthenticationResult? = null, isUnlocked: Boolean = false, - signOutAction: AsyncAction = AsyncAction.Uninitialized, + signOutAction: AsyncAction = AsyncAction.Uninitialized, ) = PinUnlockState( pinEntry = AsyncData.Success(pinEntry), showWrongPinTitle = showWrongPinTitle, - remainingAttempts = AsyncData.Success(remainingAttempts), + remainingAttempts = remainingAttempts, showSignOutPrompt = showSignOutPrompt, showBiometricUnlock = showBiometricUnlock, signOutAction = signOutAction, 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 3923ced61dc..baa856307c4 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 @@ -25,14 +25,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Lock -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -44,6 +39,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.components.PinEntryTextField import io.element.android.features.lockscreen.impl.pin.model.PinDigit @@ -68,7 +64,6 @@ import io.element.android.libraries.ui.strings.CommonStrings fun PinUnlockView( state: PinUnlockState, isInAppUnlock: Boolean, - onSuccessLogout: (logoutUrlResult: String?) -> Unit, modifier: Modifier = Modifier, ) { OnLifecycleEvent { _, event -> @@ -90,12 +85,7 @@ fun PinUnlockView( AsyncAction.Loading -> { ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) } - is AsyncAction.Success -> { - val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout) - LaunchedEffect(state) { - latestOnSuccessLogout(state.signOutAction.data) - } - } + is AsyncAction.Success, is AsyncAction.Confirming, is AsyncAction.Failure, AsyncAction.Uninitialized -> Unit @@ -299,13 +289,13 @@ private fun PinUnlockHeader( horizontalAlignment = Alignment.CenterHorizontally, ) { if (isInAppUnlock) { - BigIcon(style = BigIcon.Style.Default(Icons.Filled.Lock)) + BigIcon(style = BigIcon.Style.Default(CompoundIcons.LockSolid())) } else { Icon( modifier = Modifier .size(32.dp), tint = ElementTheme.colors.iconPrimary, - imageVector = Icons.Filled.Lock, + imageVector = CompoundIcons.LockSolid(), contentDescription = null, ) } @@ -316,7 +306,7 @@ private fun PinUnlockHeader( .fillMaxWidth(), textAlign = TextAlign.Center, style = ElementTheme.typography.fontHeadingMdBold, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, ) Spacer(Modifier.height(8.dp)) val remainingAttempts = state.remainingAttempts.dataOrNull() @@ -330,9 +320,9 @@ private fun PinUnlockHeader( "" } val subtitleColor = if (state.showWrongPinTitle) { - MaterialTheme.colorScheme.error + ElementTheme.colors.textCriticalPrimary } else { - MaterialTheme.colorScheme.secondary + ElementTheme.colors.textSecondary } Text( text = subtitle, @@ -370,7 +360,6 @@ internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStateProvider: PinUnlockView( state = state, isInAppUnlock = true, - onSuccessLogout = {}, ) } } @@ -382,7 +371,6 @@ internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::clas PinUnlockView( state = state, isInAppUnlock = false, - onSuccessLogout = {}, ) } } 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 e308865315e..dd273ebac1c 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 @@ -15,14 +15,14 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope -import io.element.android.compound.theme.ElementTheme +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter import io.element.android.features.lockscreen.impl.unlock.PinUnlockView import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings -import io.element.android.features.logout.api.util.onSuccessLogout 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.preferences.api.store.AppPreferencesStore import kotlinx.coroutines.launch @@ -38,19 +38,23 @@ class PinUnlockActivity : AppCompatActivity() { @Inject lateinit var presenter: PinUnlockPresenter @Inject lateinit var lockScreenService: LockScreenService @Inject lateinit var appPreferencesStore: AppPreferencesStore + @Inject lateinit var enterpriseService: EnterpriseService + @Inject lateinit var buildMeta: BuildMeta override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) bindings().inject(this) setContent { - ElementThemeApp(appPreferencesStore) { + ElementThemeApp( + appPreferencesStore = appPreferencesStore, + enterpriseService = enterpriseService, + buildMeta = buildMeta, + ) { val state = presenter.present() - val isDark = ElementTheme.isLightTheme.not() PinUnlockView( state = state, isInAppUnlock = false, - onSuccessLogout = { onSuccessLogout(this, isDark, it) }, ) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt index 596c6b1b591..820b5574230 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt @@ -27,6 +27,12 @@ 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.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.coerceIn import androidx.compose.ui.unit.dp @@ -37,6 +43,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toSp import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.ui.utils.time.digit import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -60,7 +68,22 @@ fun PinKeypad( val horizontalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterHorizontally) val verticalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterVertically) Column( - modifier = modifier, + modifier = modifier.onKeyEvent { event -> + if (event.type == KeyEventType.KeyUp) { + val digitChar = event.digit + if (digitChar != null) { + onClick(PinKeypadModel.Number(digitChar)) + true + } else if (event.key == Key.Backspace) { + onClick(PinKeypadModel.Back) + true + } else { + false + } + } else { + false + } + }, verticalArrangement = verticalArrangement, horizontalAlignment = horizontalAlignment, ) { @@ -183,7 +206,7 @@ private fun PinKeypadBackButton( ) { Icon( imageVector = Icons.AutoMirrored.Filled.Backspace, - contentDescription = null, + contentDescription = stringResource(CommonStrings.a11y_delete), ) } } diff --git a/features/lockscreen/impl/src/main/res/values-cy/translations.xml b/features/lockscreen/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..d82bc28cc7a --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,46 @@ + + + "dilysu biometreg" + "datgloi biometreg" + "Datgloi gyda biometreg" + "Cadarnhau biometreg" + "Wedi anghofio\'ch PIN?" + "Newid cod PIN" + "Caniatáu datgloi biometreg" + "Dileu PIN" + "Ydych chi\'n siŵr eich bod am ddileu\'r PIN?" + "Tynnu\'r PIN?" + "Caniatáu %1$s" + "Byddai\'n well gen i ddefnyddio PIN" + "Arbedwch beth amser i chi\'ch hun a defnyddiwch %1$s i ddatgloi\'r ap bob tro" + "Dewiswch PIN" + "Cadarnhau eich PIN" + "Clowch %1$s i ychwanegu diogelwch ychwanegol i\'ch sgyrsiau. + +Dewiswch rywbeth cofiadwy. Os byddwch chi\'n anghofio\'r PIN hwn, byddwch chi\'n cael eich allgofnodi o\'r ap." + "Does dim mod dewis hwn fel eich cod PIN am resymau diogelwch" + "Dewiswch PIN gwahanol" + "Rhowch yr un PIN ddwywaith" + "Nid yw\'r PINau\'n cyfateb" + "Bydd angen i chi ail-fewngofnodi a chreu PIN newydd i barhau" + "Rydych chi\'n cael eich allgofnodi" + + "Does gennych %1$d ceisiadau i ddatgloi" + "Mae gennych %1$d cais i ddatgloi" + "Mae gennych %1$d gais i ddatgloi" + "Mae gennych %1$d chais i ddatgloi" + "Mae gennych %1$d chais i ddatgloi" + "Mae gennych %1$d cais i ddatgloi" + + + "PIN anghywir. Does gennych %1$d cais arall" + "PIN anghywir. Mae gennych %1$d cais arall" + "PIN anghywir. Mae gennych %1$d gais arall" + "PIN anghywir. Mae gennych %1$d chais arall" + "PIN anghywir. Mae gennych %1$d chais arall" + "PIN anghywir. Mae gennych %1$d cais arall" + + "Defnyddio biometreg" + "Defnyddio PIN" + "Yn allgofnodi…" + diff --git a/features/lockscreen/impl/src/main/res/values-da/translations.xml b/features/lockscreen/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..ffbd657970c --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,38 @@ + + + "biometrisk godkendelse" + "biometrisk oplåsning" + "Lås op med biometri" + "Bekræft biometri" + "Glemt PIN-kode?" + "Skift PIN-kode" + "Tillad biometrisk oplåsning" + "Fjern PIN-koden" + "Er du sikker på, at du vil fjerne PIN-koden?" + "Fjern PIN-koden?" + "Tillad %1$s" + "Jeg foretrækker at bruge PIN-kode" + "Spar dig selv lidt tid og brug den %1$s til at låse appen op hver gang" + "Vælg PIN-kode" + "Bekræft PIN-kode" + "Lås %1$s for at tilføje ekstra sikkerhed til dine samtaler. + +Vælg noget mindeværdigt. Hvis du glemmer denne pinkode, bliver du logget ud af appen." + "Du kan ikke vælge dette som din PIN-kode af sikkerhedsmæssige årsager" + "Vælg en anden PIN-kode" + "Indtast venligst den samme PIN-kode to gange" + "PIN-koderne stemmer ikke overens" + "Du vil være nødt til at logge ind igen og oprette en ny PIN-kode for at fortsætte." + "Du bliver logget ud" + + "Du har %1$d forsøg på at låse op" + "Du har %1$d forsøg på at låse op" + + + "Forkert PIN-kode. Du har %1$d chance mere" + "Forkert PIN-kode. Du har %1$d flere chancer" + + "Brug biometri" + "Brug PIN-kode" + "Logger ud…" + 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 e56938285cc..f7e06457b41 100644 --- a/features/lockscreen/impl/src/main/res/values-de/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-de/translations.xml @@ -8,29 +8,29 @@ "PIN-Code ändern" "Biometrisches Entsperren zulassen" "Pin entfernen" - "Bist du sicher, dass du die PIN entfernen willst?" + "Sind Sie sicher, dass Sie die PIN entfernen wollen?" "PIN entfernen?" "%1$s zulassen" "Ich möchte diese PIN verwenden." "Spare dir etwas Zeit und benutze %1$s, um die App zu entsperren" "PIN wählen" "PIN bestätigen" - "Sperre %1$s mit einem PIN Code, um den Zugriff auf deine Chats zu beschränken. + "Erhöhen Sie die Sicherheit von %1$s mit einem PIN Code. -Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt." +Wählen Sie etwas Einprägsames. Wenn Sie die PIN vergessen, werden Sie aus der App ausgeloggt." "Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden." "Bitte eine andere PIN verwenden." "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" - "Du wirst abgemeldet" + "Um fortzufahren, müssen Sie sich erneut anmelden und eine neue PIN erstellen" + "Sie werden abgemeldet" - "Du hast %1$d Versuch zu entsperren" - "Du hast %1$d Versuche zum Entsperren" + "Sie haben %1$d Entsperrversuch" + "Sie haben %1$d Entsperrversuche" - "Falsche PIN. Du hast %1$d weiteren Versuch" - "Falsche PIN. Du hast %1$d weitere Versuche" + "Falsche PIN. Sie haben %1$d weiteren Versuch" + "Falsche PIN. Sie haben %1$d weitere Versuche" "Biometrie verwenden" "PIN verwenden" diff --git a/features/lockscreen/impl/src/main/res/values-el/translations.xml b/features/lockscreen/impl/src/main/res/values-el/translations.xml index e4e7d566b67..d36cbd4e848 100644 --- a/features/lockscreen/impl/src/main/res/values-el/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-el/translations.xml @@ -2,7 +2,7 @@ "βιομετρική ταυτοποίηση" "βιομετρικό ξεκλείδωμα" - "Ξεκλείδωμα με βιομετρικά" + "Ξεκλείδωμα με βιομετρικά στοιχεία" "Επιβεβαίωσε τον βιομετρικό έλεγχο ταυτότητας" "Ξέχασες το PIN;" "Αλλαγή κωδικού PIN" @@ -15,9 +15,9 @@ "Εξοικονόμησε χρόνο και χρησιμοποίησε %1$s για να ξεκλειδώσεις την εφαρμογή κάθε φορά" "Επέλεξε PIN" "Επιβεβαίωση PIN" - "Κλείδωμα του %1$s για να προσθέσεις επιπλέον ασφάλεια στις συνομιλίες σου. + "Κλειδώστε το %1$s για να προσθέσετε επιπλέον ασφάλεια στις συνομιλίες σας. -Επέλεξε κάτι αξιομνημόνευτο. Εάν ξεχάσεις αυτό το PIN, θα αποσυνδεθείς από την εφαρμογή." +Επιλέξτε κάτι που θα θυμάστε εύκολα. Εάν ξεχάσετε αυτό το PIN, θα αποσυνδεθείτε από την εφαρμογή." "Δεν μπορείς να το επιλέξεις ως κωδικό PIN για λόγους ασφαλείας" "Επέλεξε διαφορετικό PIN" "Παρακαλώ εισήγαγε το ίδιο PIN δύο φορές" @@ -25,8 +25,8 @@ "Θα χρειαστεί να συνδεθείς ξανά και να δημιουργήσεις ένα νέο PIN για να προχωρήσεις" "Έχεις αποσυνδεθεί" - "Έχεις %1$d προσπάθεια για ξεκλείδωμα" - "Έχεις %1$d προσπάθειες για ξεκλείδωμα" + "Έχετε %1$d προσπάθεια να ξεκλειδώσετε" + "Έχετε %1$d προσπάθειες να ξεκλειδώσετε" "Λάθος PIN. Έχεις %1$d ακόμη ευκαιρία" diff --git a/features/lockscreen/impl/src/main/res/values-es/translations.xml b/features/lockscreen/impl/src/main/res/values-es/translations.xml index 1ab64c8f3b2..c0e34a37634 100644 --- a/features/lockscreen/impl/src/main/res/values-es/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-es/translations.xml @@ -3,6 +3,7 @@ "autenticación biométrica" "desbloqueo biométrico" "Desbloquear con biométrico" + "Confirmar datos biométricos" "¿Olvidaste el PIN?" "Cambiar código PIN" "Permitir desbloqueo biométrico" diff --git a/features/lockscreen/impl/src/main/res/values-eu/translations.xml b/features/lockscreen/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..74c5e79ffd0 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,33 @@ + + + "autentifikazio biometrikoa" + "desblokeo biometrikoa" + "Desblokeatu biometria bidez" + "PINa ahaztu duzu?" + "Aldatu PIN kodea" + "Baimendu desblokeo biometrikoa" + "Kendu PIN kodea" + "Ziur PINa kendu nahi duzula?" + "PINa kendu?" + "Baimendu %1$s" + "Nahiago dut PINa erabili" + "Aukeratu PINa" + "Berretsi PINa" + "Segurtasun arrazoiak direla eta, ezin duzu hau aukeratu PIN kode gisa" + "Aukeratu PIN ezberdin bat" + "Sartu birritan PIN bera" + "PINak ez datoz bat" + "Saioa berriro hasi eta PIN berri bat sortu beharko duzu aurrera jarraitzeko" + "Saioa amaitzen ari zara" + + "Saiakera %1$d duzu desblokeatzeko" + "%1$d saiakera dituzu desblokeatzeko" + + + "PIN okerra. Aukera %1$d gehiago duzu" + "Pin okerra. %1$d aukera gehiago dituzu." + + "Erabili biometria" + "Erabili PINa" + "Saioa amaitzen…" + 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 f9a1e0d1ed8..2d224ea3025 100644 --- a/features/lockscreen/impl/src/main/res/values-fa/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-fa/translations.xml @@ -3,6 +3,7 @@ "هویت‌سنجی زیستی" "قفل‌گشایی زیست‌سنجی" "قفل‌گشایی با زیست‌سنجی" + "تأیید زیست‌سنجی" "فراموشی پین؟" "تغییر کد پین" "احازه به قفل گشایی زیست‌سنجی" @@ -14,12 +15,23 @@ "زمیانتان را ذخیره کرده و از %1$s برای قفل‌گشایی هربارهٔ کاره استفاده کنید" "گزینش پین" "تأیید پین" + "قفل %1$s برای افزودن امنیت بیشتر به گفتگوهایتان. + +چیزی به یاد ماندنی انتخاب کنید. اگر این پین را فراموش کنید، از برنامه خارج خواهید شد." "به دلیل امنیتی نمی‌توانید این پین را برگزینید" "گزینشی پینی متفاوت" "لطفاً یک پین را دو بار وارد کنید" "پین‌ها مطابق نیستند" "برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید" "دارید خارج می‌شوید" + + "شما %1$d تلاش برای باز کردن قفل دارید" + "شما %1$d تلاش برای باز کردن قفل دارید" + + + "پین اشتباه است. شما %1$d شانس دیگر دارید" + "پین اشتباه است. شما %1$d شانس دیگر دارید" + "استفاده از زیست‌سنجی" "استفاده از پین" "خارج شدن…" diff --git a/features/lockscreen/impl/src/main/res/values-lt/translations.xml b/features/lockscreen/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..b6b5eaa6193 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,4 @@ + + + "Atsijungiama…" + diff --git a/features/lockscreen/impl/src/main/res/values-nb/translations.xml b/features/lockscreen/impl/src/main/res/values-nb/translations.xml index cd619e09d4c..89ca8c0eaff 100644 --- a/features/lockscreen/impl/src/main/res/values-nb/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-nb/translations.xml @@ -1,7 +1,38 @@ + "biometrisk autentisering" + "biometrisk opplåsing" + "Lås opp med biometri" + "Bekreft biometri" + "Glemt PIN-koden?" + "Endre PIN-kode" + "Tillat biometrisk opplåsing" + "Fjern PIN-kode" + "Er du sikker på at du vil fjerne PIN-koden?" + "Fjerne PIN-kode?" + "Tillat %1$s" + "Jeg vil heller bruke PIN-kode" + "Spar deg selv litt tid og bruk%1$s for å låse opp appen hver gang" + "Velg PIN-kode" + "Bekreft PIN-kode" "Lås %1$s for å legge til ekstra sikkerhet til chattene dine. Velg noe som du husker. Hvis du glemmer denne PIN-koden, blir du logget ut av appen." + "Du kan ikke velge dette som PIN-kode av sikkerhetsmessige årsaker" + "Velg en annen PIN-kode" + "Skriv inn samme PIN-kode to ganger" + "PIN-kodene samsvarer ikke" + "Du må logge inn på nytt og opprette en ny PIN-kode for å fortsette" + "Du blir logget av" + + "Du har %1$d forsøk på å låse opp" + "Du har %1$d forsøk på å låse opp" + + + "Feil PIN-kode. Du har %1$d forsøk igjen" + "Feil PIN-kode. Du har %1$d forsøk igjen" + + "Bruk biometri" + "Bruk PIN-kode" "Logger ut…" diff --git a/features/lockscreen/impl/src/main/res/values-pt-rBR/translations.xml b/features/lockscreen/impl/src/main/res/values-pt-rBR/translations.xml index da8b201b6d3..ab3ffc4c06d 100644 --- a/features/lockscreen/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-pt-rBR/translations.xml @@ -3,6 +3,7 @@ "autenticação por biometria" "desbloqueio por biometria" "Desbloquear com biometria" + "Confirmar biometria" "Esqueceu o PIN?" "Alterar código de PIN" "Permitir desbloqueio biométrico" diff --git a/features/lockscreen/impl/src/main/res/values-sv/translations.xml b/features/lockscreen/impl/src/main/res/values-sv/translations.xml index d022e6b9531..a559e4c909c 100644 --- a/features/lockscreen/impl/src/main/res/values-sv/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sv/translations.xml @@ -3,6 +3,7 @@ "biometrisk autentisering" "biometrisk upplåsning" "Lås upp med biometri" + "Bekräfta biometriskt" "Glömt PIN-kod?" "Byt PIN-kod" "Tillåt biometrisk upplåsning" diff --git a/features/lockscreen/impl/src/main/res/values-tr/translations.xml b/features/lockscreen/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..5a9b94af112 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,38 @@ + + + "biyometrik kimlik doğrulama" + "biyometrik kilit açma" + "Biyometrik ile kilit aç" + "Biyometrik doğrulama" + "PIN\'i unuttum" + "PIN kodunu değiştir" + "Biyometrik kilit açmaya izin ver" + "PIN kodunu kaldır" + "PIN\'i kaldırmak istediğinizden emin misiniz?" + "PIN\'i kaldır?" + "İzin ver %1$s" + "PIN kullanmayı tercih ederim" + "%1$s kullanarak oturum açarken kendinize zaman kazandırın" + "PIN Seç" + "PIN\'i onayla" + "Sohbetlerinize ekstra güvenlik eklemek için %1$s kilitleyin. + +Hatırlanabilir bir şey seçin. Bu PIN\'i unutursanız, uygulamadan çıkış yaparsınız." + "Güvenlik nedeniyle bunu PIN kodunuz olarak seçemezsiniz" + "Farklı bir PIN seçin" + "Lütfen aynı PIN\'i iki kez girin" + "PIN\'ler eşleşmiyor" + "Devam etmek için yeniden oturum açmanız ve yeni bir PIN oluşturmanız gerekir" + "Oturumunuz kapatılıyor" + + "Kilidi açmak için %1$d deneme hakkınız var" + "Kilidi açmak için %1$d deneme hakkınız var" + + + "Yanlış PIN. %1$d kere daha şansınız var" + "Yanlış PIN. %1$d kere daha şansınız var" + + "Biyometrik kullan" + "PIN kullan" + "Oturum kapatılıyor…" + diff --git a/features/lockscreen/impl/src/main/res/values-ur/translations.xml b/features/lockscreen/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..85629f7cb06 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,37 @@ + + + "زیست سنجی تصدیق" + "زیست سنجی فتحِ قفل" + "زیست سنجی کے ساتھ فتح قفل کریں" + "‏PIN بھول گئے؟" + "PIN رمز بدلیں" + "زیست سنجی فتحِ قفل کی اجازت دیں" + "PIN ہٹائیں" + "کیا آپ کو یقین ہے کہ آپ PIN ہٹانا چاہتے ہیں؟" + "PIN ہٹائیں؟" + "%1$s کی اجازت دیں" + "میں اس کے بجائے PIN استعمال کروں گا" + "اپنے آپ کو کچھ وقت بچائیں اور ہر بار اطلاقیے کو غیر مقفل کرنے کے لئے %1$s کا استعمال کریں۔" + "PIN چنیں" + "PIN کی تصدیق کریں" + "اپنی گفتگوہا میں اضافی سلامتی شامل کرنے کیلئے %1$s مقفل کریں۔ + +کوئی یادگار چیز چنیں۔ اگر آپ اس PIN کو بھول گئے، آپ طلاقیے سے خارج ہوجائیں گے۔" + "حفاظتی وجوہات کی بنا پر آپ اسے اپنے PIN رمز کے طور پر منتخب نہیں کر سکتے" + "ایک مختلف PIN چنیں" + "برائے مہربانی ایک ہی PIN دو بار درج کریں۔" + "PINs مماثل نہیں ہیں" + "آگے بڑھنے کیلئے آپکو دوبارہ داخل ہونے اور ایک نیا PIN بنانے کی ضرورت ہوگی۔" + "آپکو خارج کیا جا رہا ہے" + + "آپکے پاس %1$d غیر مقفل کرنے کی کوشش ہے" + "آپکے پاس %1$d غیر مقفل کرنے کی کوششیں ہیں" + + + "غلط PIN۔ آپ کے پاس %1$d مزید موقع ہے" + "غلط PIN۔ آپ کے پاس %1$d مزید موقعے ہیں" + + "زیست سنجی استعمال کریں" + "PIN استعمال کریں" + "خارج ہورہاہے…" + 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 feabcf35b2c..45af2682e12 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 @@ -20,14 +20,14 @@ import io.element.android.features.lockscreen.impl.storage.LockScreenStore import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.test -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test class LockScreenSettingsPresenterTest { @Test fun `present - remove pin option is hidden when mandatory`() = runTest { - val presenter = createLockScreenSettingsPresenter(this, lockScreenConfig = aLockScreenConfig(isPinMandatory = true)) + val presenter = createLockScreenSettingsPresenter(lockScreenConfig = aLockScreenConfig(isPinMandatory = true)) presenter.test { awaitItem().also { state -> assertThat(state.showRemovePinOption).isFalse() @@ -37,7 +37,7 @@ class LockScreenSettingsPresenterTest { @Test fun `present - remove pin flow`() = runTest { - val presenter = createLockScreenSettingsPresenter(this) + val presenter = createLockScreenSettingsPresenter() presenter.test { consumeItemsUntilPredicate { state -> state.showRemovePinOption @@ -71,7 +71,6 @@ class LockScreenSettingsPresenterTest { isDeviceSecured = true, ) val presenter = createLockScreenSettingsPresenter( - coroutineScope = this, biometricAuthenticatorManager = fakeBiometricAuthenticatorManager ) presenter.test { @@ -88,7 +87,6 @@ class LockScreenSettingsPresenterTest { } ) val presenter = createLockScreenSettingsPresenter( - coroutineScope = this, biometricAuthenticatorManager = fakeBiometricAuthenticatorManager ) presenter.test { @@ -110,7 +108,6 @@ class LockScreenSettingsPresenterTest { } ) val presenter = createLockScreenSettingsPresenter( - coroutineScope = this, biometricAuthenticatorManager = fakeBiometricAuthenticatorManager ) presenter.test { @@ -130,7 +127,6 @@ class LockScreenSettingsPresenterTest { ) val lockScreenStore = InMemoryLockScreenStore() val presenter = createLockScreenSettingsPresenter( - coroutineScope = this, lockScreenStore = lockScreenStore, biometricAuthenticatorManager = fakeBiometricAuthenticatorManager ) @@ -148,8 +144,7 @@ class LockScreenSettingsPresenterTest { } } - private suspend fun createLockScreenSettingsPresenter( - coroutineScope: CoroutineScope, + private suspend fun TestScope.createLockScreenSettingsPresenter( lockScreenConfig: LockScreenConfig = aLockScreenConfig(), biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(), lockScreenStore: LockScreenStore = InMemoryLockScreenStore(), @@ -160,7 +155,7 @@ class LockScreenSettingsPresenterTest { return LockScreenSettingsPresenter( lockScreenStore = lockScreenStore, pinCodeManager = pinCodeManager, - coroutineScope = coroutineScope, + coroutineScope = this, lockScreenConfig = lockScreenConfig, biometricAuthenticatorManager = biometricAuthenticatorManager, ) 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 f03e61bdb75..5fdf1342eae 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 @@ -24,7 +24,7 @@ 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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -34,7 +34,7 @@ class PinUnlockPresenterTest { @Test fun `present - success verify flow`() = runTest { - val presenter = createPinUnlockPresenter(this) + val presenter = createPinUnlockPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -71,7 +71,7 @@ class PinUnlockPresenterTest { @Test fun `present - failure verify flow`() = runTest { - val presenter = createPinUnlockPresenter(this) + val presenter = createPinUnlockPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -98,9 +98,9 @@ class PinUnlockPresenterTest { @Test fun `present - forgot pin flow`() = runTest { - val signOutLambda = lambdaRecorder { "" } + val signOutLambda = lambdaRecorder {} val signOut = FakeLogoutUseCase(signOutLambda) - val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut) + val presenter = createPinUnlockPresenter(logoutUseCase = signOut) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -135,8 +135,7 @@ class PinUnlockPresenterTest { dataOrNull()?.assertText(text) } - private suspend fun createPinUnlockPresenter( - scope: CoroutineScope, + private suspend fun TestScope.createPinUnlockPresenter( biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(), callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }), @@ -149,7 +148,7 @@ class PinUnlockPresenterTest { pinCodeManager = pinCodeManager, biometricAuthenticatorManager = biometricAuthenticatorManager, logoutUseCase = logoutUseCase, - coroutineScope = scope, + coroutineScope = this, pinUnlockHelper = PinUnlockHelper(biometricAuthenticatorManager, pinCodeManager), ) } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt new file mode 100644 index 00000000000..389ebf83c73 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt @@ -0,0 +1,76 @@ +/* + * 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.lockscreen.impl.unlock + +import androidx.biometric.BiometricPrompt +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator +import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError +import io.element.android.libraries.architecture.AsyncData +import org.junit.Test + +class PinUnlockStateTest { + @Test + fun `isSignOutPromptCancellable should have expected values`() { + assertThat(aPinUnlockState(remainingAttempts = AsyncData.Uninitialized).isSignOutPromptCancellable).isTrue() + assertThat(aPinUnlockState(remainingAttempts = AsyncData.Success(1)).isSignOutPromptCancellable).isTrue() + assertThat(aPinUnlockState(remainingAttempts = AsyncData.Success(0)).isSignOutPromptCancellable).isFalse() + } + + @Test + fun `biometricUnlockErrorMessage and showBiometricUnlockError should have expected values`() { + listOf( + null, + BiometricAuthenticator.AuthenticationResult.Failure(), + BiometricAuthenticator.AuthenticationResult.Success, + ).forEach { biometricUnlockResult -> + aPinUnlockState( + biometricUnlockResult = biometricUnlockResult, + ).let { + assertThat(it.biometricUnlockErrorMessage).isNull() + assertThat(it.showBiometricUnlockError).isFalse() + } + } + listOf( + BiometricPrompt.ERROR_HW_UNAVAILABLE, + BiometricPrompt.ERROR_UNABLE_TO_PROCESS, + BiometricPrompt.ERROR_TIMEOUT, + BiometricPrompt.ERROR_NO_SPACE, + BiometricPrompt.ERROR_CANCELED, + BiometricPrompt.ERROR_VENDOR, + BiometricPrompt.ERROR_USER_CANCELED, + BiometricPrompt.ERROR_NO_BIOMETRICS, + BiometricPrompt.ERROR_HW_NOT_PRESENT, + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL, + BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED, + ).forEach { code -> + aPinUnlockState( + biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure( + error = BiometricUnlockError(code, "Error message") + ), + ).let { + assertThat(it.biometricUnlockErrorMessage).isNull() + assertThat(it.showBiometricUnlockError).isFalse() + } + } + listOf( + BiometricPrompt.ERROR_LOCKOUT, + BiometricPrompt.ERROR_LOCKOUT_PERMANENT, + ).forEach { code -> + aPinUnlockState( + biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure( + error = BiometricUnlockError(code, "Error message") + ), + ).let { + assertThat(it.biometricUnlockErrorMessage).isEqualTo("Error message") + assertThat(it.showBiometricUnlockError).isTrue() + } + } + } +} 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 new file mode 100644 index 00000000000..e51b007312c --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt @@ -0,0 +1,131 @@ +/* + * 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.lockscreen.impl.unlock.keypad + +import android.view.KeyEvent +import androidx.activity.ComponentActivity +import androidx.compose.ui.input.key.Key +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.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`() { + val eventsRecorder = EventsRecorder() + 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`() { + val eventsRecorder = EventsRecorder() + 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`() { + val eventsRecorder = EventsRecorder() + rule.setPinKeyPad(onClick = eventsRecorder) + rule.onNodeWithText("1").requestFocus() + rule.onAllNodes(isRoot())[0].performKeyInput { + val keys = listOf( + Key.A, + Key.NumPad1, + Key.NumPad2, + Key.NumPad3, + Key.NumPad4, + Key.NumPad5, + Key.NumPad6, + Key.NumPad7, + Key.NumPad8, + Key.NumPad9, + Key.NumPad0, + Key(KeyEvent.KEYCODE_1), + Key(KeyEvent.KEYCODE_2), + Key(KeyEvent.KEYCODE_3), + Key(KeyEvent.KEYCODE_4), + Key(KeyEvent.KEYCODE_5), + Key(KeyEvent.KEYCODE_6), + Key(KeyEvent.KEYCODE_7), + Key(KeyEvent.KEYCODE_8), + Key(KeyEvent.KEYCODE_9), + Key(KeyEvent.KEYCODE_0), + Key.Backspace, + ) + for (key in keys) { + pressKey(key) + } + } + eventsRecorder.assertList( + listOf( + // Note that the first key is not a number, but a letter so it's ignored as input + // Then we have the numpad keys + PinKeypadModel.Number('1'), + PinKeypadModel.Number('2'), + PinKeypadModel.Number('3'), + PinKeypadModel.Number('4'), + PinKeypadModel.Number('5'), + PinKeypadModel.Number('6'), + PinKeypadModel.Number('7'), + PinKeypadModel.Number('8'), + PinKeypadModel.Number('9'), + PinKeypadModel.Number('0'), + // And the normal keys from the number row in the keyboard + PinKeypadModel.Number('1'), + PinKeypadModel.Number('2'), + PinKeypadModel.Number('3'), + PinKeypadModel.Number('4'), + PinKeypadModel.Number('5'), + PinKeypadModel.Number('6'), + PinKeypadModel.Number('7'), + PinKeypadModel.Number('8'), + PinKeypadModel.Number('9'), + PinKeypadModel.Number('0'), + PinKeypadModel.Back, + ) + ) + } + + private fun AndroidComposeTestRule.setPinKeyPad( + onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(), + ) { + setContent { + PinKeypad( + onClick = onClick, + maxWidth = 1000.dp, + maxHeight = 1000.dp, + ) + } + } +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt index ead74221c13..b3ef99ee198 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt @@ -7,28 +7,26 @@ package io.element.android.features.login.api -import android.os.Parcelable 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 kotlinx.parcelize.Parcelize interface LoginEntryPoint : FeatureEntryPoint { data class Params( - val flowType: LoginFlowType + val accountProvider: String?, + val loginHint: String?, ) + interface Callback : Plugin { + fun onReportProblem() + } + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder interface NodeBuilder { fun params(params: Params): NodeBuilder + fun callback(callback: Callback): NodeBuilder fun build(): Node } } - -@Parcelize -enum class LoginFlowType : Parcelable { - SIGN_IN_MANUAL, - SIGN_IN_QR_CODE, - SIGN_UP -} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt new file mode 100644 index 00000000000..67eb284944d --- /dev/null +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt @@ -0,0 +1,12 @@ +/* + * 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.login.api + +interface LoginIntentResolver { + fun parse(uriString: String): LoginParams? +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginParams.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginParams.kt new file mode 100644 index 00000000000..84dcac40a65 --- /dev/null +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginParams.kt @@ -0,0 +1,21 @@ +/* + * 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.login.api + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Parameters to start the login flow, when the application is opened + * from a mobile.element.io link. + */ +@Parcelize +data class LoginParams( + val accountProvider: String, + val loginHint: String? +) : Parcelable diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 327eaa54169..a4f20fb97df 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -28,9 +28,12 @@ setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP) dependencies { implementation(projects.appconfig) + implementation(projects.features.enterprise.api) + implementation(projects.features.rageshake.api) 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.network) @@ -40,6 +43,7 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.qrcode) implementation(projects.libraries.oidc.api) + implementation(projects.libraries.uiUtils) implementation(libs.androidx.browser) implementation(platform(libs.network.retrofit.bom)) implementation(libs.androidx.webkit) @@ -55,8 +59,11 @@ dependencies { testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(projects.features.login.test) + testImplementation(projects.features.enterprise.test) + testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.oidc.impl) + testImplementation(projects.libraries.oidc.test) testImplementation(projects.libraries.permissions.test) testImplementation(projects.tests.testutils) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt index 902cf4ca335..b6eca77a497 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt @@ -23,7 +23,15 @@ class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint { return object : LoginEntryPoint.NodeBuilder { override fun params(params: LoginEntryPoint.Params): LoginEntryPoint.NodeBuilder { - plugins += LoginFlowNode.Inputs(flowType = params.flowType) + plugins += LoginFlowNode.Params( + accountProvider = params.accountProvider, + loginHint = params.loginHint, + ) + return this + } + + override fun callback(callback: LoginEntryPoint.Callback): LoginEntryPoint.NodeBuilder { + plugins += callback return this } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolver.kt new file mode 100644 index 00000000000..4b17980fa6d --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolver.kt @@ -0,0 +1,30 @@ +/* + * 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.login.impl + +import androidx.core.net.toUri +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.login.api.LoginIntentResolver +import io.element.android.features.login.api.LoginParams +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultLoginIntentResolver @Inject constructor() : LoginIntentResolver { + override fun parse(uriString: String): LoginParams? { + val uri = uriString.toUri() + if (uri.host != "mobile.element.io") return null + if (uri.path.orEmpty().startsWith("/element").not()) return null + val accountProvider = uri.getQueryParameter("account_provider") ?: return null + val loginHint = uri.getQueryParameter("login_hint") + return LoginParams( + accountProvider = accountProvider, + loginHint = loginHint, + ) + } +} 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 9b9ca2ef135..2d791f0f9a1 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 @@ -9,15 +9,16 @@ package io.element.android.features.login.impl import android.app.Activity import android.os.Parcelable +import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.singleTop @@ -25,14 +26,17 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.compound.theme.ElementTheme -import io.element.android.features.login.api.LoginFlowType +import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode +import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode +import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs @@ -42,7 +46,6 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcActionFlow -import io.element.android.libraries.oidc.api.OidcEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -54,33 +57,31 @@ class LoginFlowNode @AssistedInject constructor( private val accountProviderDataSource: AccountProviderDataSource, private val defaultLoginUserStory: DefaultLoginUserStory, private val oidcActionFlow: OidcActionFlow, - private val oidcEntryPoint: OidcEntryPoint, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Root, + initialElement = NavTarget.OnBoarding, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, plugins = plugins, ) { - private var activity: Activity? = null - private var darkTheme: Boolean = false - - data class Inputs( - val flowType: LoginFlowType, + data class Params( + val accountProvider: String?, + val loginHint: String?, ) : NodeInputs - private val inputs: Inputs = inputs() + private var activity: Activity? = null + private var darkTheme: Boolean = false - private var customChromeTabStarted = false + private var externalAppStarted = false override fun onBuilt() { super.onBuilt() defaultLoginUserStory.setLoginFlowIsDone(false) lifecycle.subscribe( onResume = { - if (customChromeTabStarted) { - customChromeTabStarted = false + if (externalAppStarted) { + externalAppStarted = false // Workaround to detect that the Custom Chrome Tab has been closed // If there is no coming OidcAction (that would end this Node), // consider that the user has cancelled the login @@ -96,10 +97,18 @@ class LoginFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - data object Root : NavTarget + data object OnBoarding : NavTarget + + @Parcelize + data object QrCode : NavTarget @Parcelize - data object ConfirmAccountProvider : NavTarget + data class ConfirmAccountProvider( + val isAccountCreation: Boolean, + ) : NavTarget + + @Parcelize + data object ChooseAccountProvider : NavTarget @Parcelize data object ChangeAccountProvider : NavTarget @@ -112,38 +121,81 @@ class LoginFlowNode @AssistedInject constructor( @Parcelize data class CreateAccount(val url: String) : NavTarget - - @Parcelize - data class OidcView(val oidcDetails: OidcDetails) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Root -> { - if (inputs.flowType == LoginFlowType.SIGN_IN_QR_CODE) { - createNode(buildContext) - } else { - resolve(NavTarget.ConfirmAccountProvider, buildContext) + NavTarget.OnBoarding -> { + val callback = object : OnBoardingNode.Callback { + override fun onSignUp() { + backstack.push( + NavTarget.ConfirmAccountProvider(isAccountCreation = true) + ) + } + + override fun onSignIn(mustChooseAccountProvider: Boolean) { + backstack.push( + if (mustChooseAccountProvider) { + NavTarget.ChooseAccountProvider + } else { + NavTarget.ConfirmAccountProvider(isAccountCreation = false) + } + ) + } + + override fun onSignInWithQrCode() { + backstack.push(NavTarget.QrCode) + } + + override fun onReportProblem() { + plugins().forEach { it.onReportProblem() } + } + + override fun onOidcDetails(oidcDetails: OidcDetails) { + navigateToMas(oidcDetails) + } + + override fun onCreateAccountContinue(url: String) { + backstack.push(NavTarget.CreateAccount(url)) + } + + override fun onLoginPasswordNeeded() { + backstack.push(NavTarget.LoginPassword) + } } + val params = inputs() + val inputs = OnBoardingNode.Params( + accountProvider = params.accountProvider, + loginHint = params.loginHint, + ) + createNode(buildContext, listOf(callback, inputs)) } - NavTarget.ConfirmAccountProvider -> { + NavTarget.ChooseAccountProvider -> { + val callback = object : ChooseAccountProviderNode.Callback { + override fun onOidcDetails(oidcDetails: OidcDetails) { + navigateToMas(oidcDetails) + } + + override fun onCreateAccountContinue(url: String) { + backstack.push(NavTarget.CreateAccount(url)) + } + + override fun onLoginPasswordNeeded() { + backstack.push(NavTarget.LoginPassword) + } + } + createNode(buildContext, listOf(callback)) + } + NavTarget.QrCode -> { + createNode(buildContext) + } + is NavTarget.ConfirmAccountProvider -> { val inputs = ConfirmAccountProviderNode.Inputs( - isAccountCreation = inputs.flowType == LoginFlowType.SIGN_UP, + isAccountCreation = navTarget.isAccountCreation, ) val callback = object : ConfirmAccountProviderNode.Callback { override fun onOidcDetails(oidcDetails: OidcDetails) { - if (oidcEntryPoint.canUseCustomTab()) { - // In this case open a Chrome Custom tab - activity?.let { - customChromeTabStarted = true - oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url) - } - } else { - // Fallback to external app mode - activity?.let { - oidcEntryPoint.openUrlInExternalApp(it, oidcDetails.url) - } - } + navigateToMas(oidcDetails) } override fun onCreateAccountContinue(url: String) { @@ -164,7 +216,10 @@ class LoginFlowNode @AssistedInject constructor( val callback = object : ChangeAccountProviderNode.Callback { override fun onDone() { // Go back to the Account Provider screen - backstack.singleTop(NavTarget.ConfirmAccountProvider) + val confirmAccountProvider = backstack.elements.value.firstOrNull { + it.key.navTarget is NavTarget.ConfirmAccountProvider + }?.key?.navTarget ?: NavTarget.ConfirmAccountProvider(isAccountCreation = false) + backstack.singleTop(confirmAccountProvider) } override fun onOtherClick() { @@ -178,7 +233,10 @@ class LoginFlowNode @AssistedInject constructor( val callback = object : SearchAccountProviderNode.Callback { override fun onDone() { // Go back to the Account Provider screen - backstack.singleTop(NavTarget.ConfirmAccountProvider) + val confirmAccountProvider = backstack.elements.value.firstOrNull { + it.key.navTarget is NavTarget.ConfirmAccountProvider + }?.key?.navTarget ?: NavTarget.ConfirmAccountProvider(isAccountCreation = false) + backstack.singleTop(confirmAccountProvider) } } @@ -187,9 +245,6 @@ class LoginFlowNode @AssistedInject constructor( NavTarget.LoginPassword -> { createNode(buildContext) } - is NavTarget.OidcView -> { - oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url) - } is NavTarget.CreateAccount -> { val inputs = CreateAccountNode.Inputs( url = navTarget.url, @@ -199,9 +254,16 @@ class LoginFlowNode @AssistedInject constructor( } } + private fun navigateToMas(oidcDetails: OidcDetails) { + activity?.let { + externalAppStarted = true + it.openUrlInChromeCustomTab(null, darkTheme, oidcDetails.url) + } + } + @Composable override fun View(modifier: Modifier) { - activity = LocalContext.current as? Activity + activity = requireNotNull(LocalActivity.current) darkTheme = !ElementTheme.isLightTheme DisposableEffect(Unit) { onDispose { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt index 913cfb6f0c1..9ebc246e25b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt @@ -7,7 +7,8 @@ package io.element.android.features.login.impl.accountprovider -import io.element.android.features.login.impl.util.defaultAccountProvider +import io.element.android.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import kotlinx.coroutines.flow.MutableStateFlow @@ -16,14 +17,25 @@ import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject @SingleIn(AppScope::class) -class AccountProviderDataSource @Inject constructor() { +class AccountProviderDataSource @Inject constructor( + enterpriseService: EnterpriseService, +) { + private val defaultAccountProvider = + (enterpriseService.defaultHomeserverList().firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: AuthenticationConfig.MATRIX_ORG_URL) + .let { url -> + AccountProvider( + url = url, + subtitle = null, + isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, + isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, + ) + } + private val accountProvider: MutableStateFlow = MutableStateFlow( defaultAccountProvider ) - fun flow(): StateFlow { - return accountProvider.asStateFlow() - } + val flow: StateFlow = accountProvider.asStateFlow() fun reset() { accountProvider.tryEmit(defaultAccountProvider) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt new file mode 100644 index 00000000000..92fda4cf14a --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt @@ -0,0 +1,75 @@ +/* + * 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.login.impl.accountprovider + +import androidx.compose.foundation.clickable +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.padding +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.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.login.impl.R +import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom +import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Text + +/** + * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 + */ +@Composable +fun AccountProviderOtherView( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + ) { + HorizontalDivider() + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 44.dp) + .padding(vertical = 4.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RoundedIconAtom( + size = RoundedIconAtomSize.Medium, + imageVector = CompoundIcons.Search(), + tint = ElementTheme.colors.iconPrimary, + ) + Text( + modifier = Modifier + .padding(start = 16.dp) + .weight(1f), + text = stringResource(R.string.screen_change_account_provider_other), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun AccountProviderOtherViewPreview() = ElementPreview { + AccountProviderOtherView( + onClick = { }, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt index ddc635b5df8..a5f0fd7d3ba 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt @@ -21,10 +21,16 @@ open class AccountProviderProvider : PreviewParameterProvider { ) } -fun anAccountProvider() = AccountProvider( - url = AuthenticationConfig.MATRIX_ORG_URL, - subtitle = "Matrix.org is an open network for secure, decentralized communication.", - isPublic = true, - isMatrixOrg = true, - isValid = true, +fun anAccountProvider( + url: String = AuthenticationConfig.MATRIX_ORG_URL, + subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.", + isPublic: Boolean = true, + isMatrixOrg: Boolean = true, + isValid: Boolean = true, +) = AccountProvider( + url = url, + subtitle = subtitle, + isPublic = isPublic, + isMatrixOrg = isMatrixOrg, + isValid = isValid, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt index fc2f518873e..b61f0283a71 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -40,6 +39,7 @@ fun AccountProviderView( item: AccountProvider, onClick: () -> Unit, modifier: Modifier = Modifier, + selected: Boolean = false, ) { Column( modifier = modifier @@ -67,8 +67,8 @@ fun AccountProviderView( } else { RoundedIconAtom( size = RoundedIconAtomSize.Medium, - imageVector = CompoundIcons.Search(), - tint = MaterialTheme.colorScheme.primary, + imageVector = CompoundIcons.Host(), + tint = ElementTheme.colors.iconPrimary, ) } Text( @@ -77,16 +77,25 @@ fun AccountProviderView( .weight(1f), text = item.title, style = ElementTheme.typography.fontBodyLgMedium, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, ) if (item.isPublic) { Icon( modifier = Modifier .padding(start = 10.dp) .size(16.dp), - resourceId = R.drawable.ic_public, + imageVector = CompoundIcons.Public(), contentDescription = null, - tint = Color.Unspecified, + tint = ElementTheme.colors.iconSecondary, + ) + } + if (selected) { + Icon( + modifier = Modifier + .padding(start = 10.dp), + imageVector = CompoundIcons.Check(), + contentDescription = null, + tint = ElementTheme.colors.iconAccentPrimary, ) } } @@ -96,7 +105,7 @@ fun AccountProviderView( .padding(start = 46.dp, bottom = 12.dp, end = 26.dp), text = item.subtitle, style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt index 9806c792ad9..c0cdf7c06d2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError @@ -26,6 +27,7 @@ import javax.inject.Inject class ChangeServerPresenter @Inject constructor( private val authenticationService: MatrixAuthenticationService, private val accountProviderDataSource: AccountProviderDataSource, + private val enterpriseService: EnterpriseService, ) : Presenter { @Composable override fun present(): ChangeServerState { @@ -53,6 +55,12 @@ class ChangeServerPresenter @Inject constructor( changeServerAction: MutableState>, ) = launch { suspend { + if (enterpriseService.isAllowedToConnectToHomeserver(data.url).not()) { + throw UnauthorizedAccountProviderException( + unauthorisedAccountProviderTitle = data.title, + authorisedAccountProviderTitles = enterpriseService.defaultHomeserverList(), + ) + } authenticationService.setHomeserver(data.url).map { authenticationService.getHomeserverDetails().value!! // Valid, remember user choice diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt index 8ed40ccedc6..2549109488c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt @@ -18,6 +18,14 @@ open class ChangeServerStateProvider : PreviewParameterProvider { - when (val error = state.changeServerAction.error) { + when (val error = state.changeServerAction.error as? ChangeServerError) { is ChangeServerError.Error -> { ErrorDialog( modifier = modifier, @@ -53,6 +56,20 @@ fun ChangeServerView( } ) } + is ChangeServerError.UnauthorizedAccountProvider -> { + ErrorDialog( + modifier = modifier, + content = stringResource( + id = R.string.screen_change_server_error_unauthorized_homeserver, + LocalBuildMeta.current.applicationName, + error.unauthorisedAccountProviderTitle, + ), + onSubmit = { + eventSink.invoke(ChangeServerEvents.ClearError) + } + ) + } + null -> Unit } } is AsyncData.Loading -> ProgressDialog() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/UnauthorizedAccountProviderException.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/UnauthorizedAccountProviderException.kt new file mode 100644 index 00000000000..1ceb2ab3437 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/UnauthorizedAccountProviderException.kt @@ -0,0 +1,13 @@ +/* + * 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.login.impl.changeserver + +class UnauthorizedAccountProviderException( + val unauthorisedAccountProviderTitle: String, + val authorisedAccountProviderTitles: List, +) : Exception() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt index a3397dc41b7..be526ccb0d1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt @@ -14,6 +14,7 @@ import io.element.android.features.login.impl.R import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -29,7 +30,10 @@ internal fun SlidingSyncNotSupportedDialog( onSubmitClick = onLearnMoreClick, onCancelClick = onDismiss, title = stringResource(CommonStrings.dialog_title_error), - content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message), + content = stringResource( + id = R.string.screen_change_server_error_no_sliding_sync_message, + LocalBuildMeta.current.applicationName, + ), ) } 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 b450aaea531..6d678854c1f 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 @@ -9,21 +9,39 @@ package io.element.android.features.login.impl.error import androidx.annotation.StringRes import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.stringResource import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException import io.element.android.libraries.matrix.api.auth.AuthenticationException +import io.element.android.libraries.ui.strings.CommonStrings sealed class ChangeServerError : Throwable() { - data class Error(@StringRes val messageId: Int) : ChangeServerError() { + data class Error( + @StringRes val messageId: Int? = null, + val messageStr: String? = null, + ) : ChangeServerError() { @Composable - fun message(): String = stringResource(messageId) + @ReadOnlyComposable + fun message(): String = messageStr ?: stringResource(messageId ?: CommonStrings.error_unknown) } + + data class UnauthorizedAccountProvider( + val unauthorisedAccountProviderTitle: String, + val authorisedAccountProviderTitles: List, + ) : ChangeServerError() + data object SlidingSyncAlert : ChangeServerError() companion object { fun from(error: Throwable): ChangeServerError = when (error) { is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert - else -> Error(R.string.screen_change_server_error_invalid_homeserver) + is AuthenticationException.Oidc -> Error(messageStr = error.message) + is UnauthorizedAccountProviderException -> UnauthorizedAccountProvider( + unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle, + authorisedAccountProviderTitles = error.authorisedAccountProviderTitles, + ) + else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver) } } } 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 new file mode 100644 index 00000000000..459f45a06c7 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt @@ -0,0 +1,121 @@ +/* + * 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.login.impl.login + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import io.element.android.features.login.impl.DefaultLoginUserStory +import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter +import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter +import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.auth.OidcPrompt +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * This class is responsible for managing the login flow, including handling OIDC actions and + * submitting login requests. + * It's an helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter] + * and [ChooseAccountProviderPresenter]. + */ +class LoginHelper @Inject constructor( + private val oidcActionFlow: OidcActionFlow, + private val authenticationService: MatrixAuthenticationService, + private val defaultLoginUserStory: DefaultLoginUserStory, + private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, +) { + private val loginModeState: MutableState> = mutableStateOf(AsyncData.Uninitialized) + + @Composable + fun collectLoginMode(): State> { + LaunchedEffect(Unit) { + oidcActionFlow.collect { oidcAction -> + if (oidcAction != null) { + onOidcAction(oidcAction) + } + } + } + return loginModeState + } + + fun clearError() { + loginModeState.value = AsyncData.Uninitialized + } + + fun submit( + coroutineScope: CoroutineScope, + isAccountCreation: Boolean, + homeserverUrl: String, + loginHint: String?, + ) = coroutineScope.launch { + suspend { + authenticationService.setHomeserver(homeserverUrl).map { + val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!! + if (matrixHomeServerDetails.supportsOidcLogin) { + // Retrieve the details right now + 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) + LoginMode.AccountCreation(url) + } else if (matrixHomeServerDetails.supportsPasswordLogin) { + LoginMode.PasswordLogin + } else { + error("Unsupported login flow") + } + }.getOrThrow() + }.runCatchingUpdatingState( + state = loginModeState, + errorTransform = { + when (it) { + is AccountCreationNotSupported -> it + else -> ChangeServerError.from(it) + } + } + ) + } + + private suspend fun onOidcAction(oidcAction: OidcAction) { + loginModeState.value = AsyncData.Loading() + when (oidcAction) { + OidcAction.GoBack -> { + authenticationService.cancelOidcLogin() + .onSuccess { + loginModeState.value = AsyncData.Uninitialized + } + .onFailure { failure -> + loginModeState.value = AsyncData.Failure(failure) + } + } + is OidcAction.Success -> { + authenticationService.loginWithOidc(oidcAction.url) + .onSuccess { _ -> + defaultLoginUserStory.setLoginFlowIsDone(true) + } + .onFailure { failure -> + loginModeState.value = AsyncData.Failure(failure) + } + } + } + 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 new file mode 100644 index 00000000000..571b0182977 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt @@ -0,0 +1,16 @@ +/* + * 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.login.impl.login + +import io.element.android.libraries.matrix.api.auth.OidcDetails + +sealed interface LoginMode { + data object PasswordLogin : 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 new file mode 100644 index 00000000000..abf1327913c --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt @@ -0,0 +1,89 @@ +/* + * 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.login.impl.login + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog +import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.theme.LocalBuildMeta +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun LoginModeView( + loginMode: AsyncData, + onClearError: () -> Unit, + onLearnMoreClick: () -> Unit, + onOidcDetails: (OidcDetails) -> Unit, + onNeedLoginPassword: () -> Unit, + onCreateAccountContinue: (url: String) -> Unit +) { + when (loginMode) { + is AsyncData.Failure -> { + when (val error = loginMode.error) { + is ChangeServerError -> { + when (error) { + is ChangeServerError.Error -> { + ErrorDialog( + content = error.message(), + onSubmit = onClearError, + ) + } + is ChangeServerError.SlidingSyncAlert -> { + SlidingSyncNotSupportedDialog( + onLearnMoreClick = { + onLearnMoreClick() + onClearError() + }, + onDismiss = onClearError, + ) + } + is ChangeServerError.UnauthorizedAccountProvider -> { + ErrorDialog( + content = stringResource( + id = R.string.screen_change_server_error_unauthorized_homeserver, + LocalBuildMeta.current.applicationName, + error.unauthorisedAccountProviderTitle, + ), + onSubmit = onClearError, + ) + } + } + } + is AccountCreationNotSupported -> { + ErrorDialog( + content = stringResource(CommonStrings.error_account_creation_not_possible), + onSubmit = onClearError, + ) + } + else -> { + ErrorDialog( + content = stringResource(CommonStrings.error_unknown), + onSubmit = onClearError, + ) + } + } + } + is AsyncData.Loading -> Unit // The Continue button shows the loading state + is AsyncData.Success -> { + when (val loginModeData = loginMode.data) { + is LoginMode.Oidc -> onOidcDetails(loginModeData.oidcDetails) + LoginMode.PasswordLogin -> onNeedLoginPassword() + is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url) + } + // Also clear the data, to let the next screen be able to go back + onClearError() + } + AsyncData.Uninitialized -> Unit + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt index 3d1ce69b51f..bb3da316b1b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -8,29 +8,46 @@ package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import io.element.android.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.api.canConnectToAnyHomeserver import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.uri.ensureProtocol import javax.inject.Inject class ChangeAccountProviderPresenter @Inject constructor( private val changeServerPresenter: Presenter, + private val enterpriseService: EnterpriseService, ) : Presenter { @Composable override fun present(): ChangeAccountProviderState { + val staticAccountProviderList = remember { + enterpriseService.defaultHomeserverList() + .filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } + .map { it.ensureProtocol() } + .ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) } + .map { url -> + AccountProvider( + url = url, + subtitle = null, + isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, + isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, + isValid = true, + ) + } + } + + val canSearchForAccountProviders = remember { + enterpriseService.canConnectToAnyHomeserver() + } + val changeServerState = changeServerPresenter.present() return ChangeAccountProviderState( - // Just matrix.org by default for now - accountProviders = listOf( - AccountProvider( - url = AuthenticationConfig.MATRIX_ORG_URL, - subtitle = null, - isPublic = true, - isMatrixOrg = true, - isValid = true, - ) - ), + accountProviders = staticAccountProviderList, + canSearchForAccountProviders = canSearchForAccountProviders, changeServerState = changeServerState, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt index 9211e90bb56..e8e1f21cd85 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt @@ -11,7 +11,8 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.changeserver.ChangeServerState // Do not use default value, so no member get forgotten in the presenters. -data class ChangeAccountProviderState constructor( +data class ChangeAccountProviderState( val accountProviders: List, + val canSearchForAccountProviders: Boolean, val changeServerState: ChangeServerState, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt index d061303f3a4..435eee7f897 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt @@ -8,20 +8,28 @@ package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.aChangeServerState open class ChangeAccountProviderStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aChangeAccountProviderState(), + aChangeAccountProviderState(canSearchForAccountProviders = false), // Add other state here ) } -fun aChangeAccountProviderState() = ChangeAccountProviderState( - accountProviders = listOf( +fun aChangeAccountProviderState( + accountProviders: List = listOf( anAccountProvider() ), - changeServerState = aChangeServerState(), + canSearchForAccountProviders: Boolean = true, + changeServerState: ChangeServerState = aChangeServerState(), +) = ChangeAccountProviderState( + accountProviders = accountProviders, + canSearchForAccountProviders = canSearchForAccountProviders, + changeServerState = changeServerState, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt index 7f280609473..c4542dfb0df 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt @@ -19,16 +19,15 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Home 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.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProviderOtherView import io.element.android.features.login.impl.accountprovider.AccountProviderView import io.element.android.features.login.impl.changeserver.ChangeServerEvents import io.element.android.features.login.impl.changeserver.ChangeServerView @@ -74,7 +73,7 @@ fun ChangeAccountProviderView( ) { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp), - iconStyle = BigIcon.Style.Default(Icons.Filled.Home), + iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()), title = stringResource(id = R.string.screen_change_account_provider_title), subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle), ) @@ -96,13 +95,11 @@ fun ChangeAccountProviderView( ) } // Other - AccountProviderView( - item = AccountProvider( - url = "", - title = stringResource(id = R.string.screen_change_account_provider_other), - ), - onClick = onOtherProviderClick - ) + if (state.canSearchForAccountProviders) { + AccountProviderOtherView( + onClick = onOtherProviderClick + ) + } Spacer(Modifier.height(32.dp)) } ChangeServerView( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvents.kt new file mode 100644 index 00000000000..76c86d18c23 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvents.kt @@ -0,0 +1,16 @@ +/* + * 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.login.impl.screens.chooseaccountprovider + +import io.element.android.features.login.impl.accountprovider.AccountProvider + +sealed interface ChooseAccountProviderEvents { + data class SelectAccountProvider(val accountProvider: AccountProvider) : ChooseAccountProviderEvents + data object Continue : ChooseAccountProviderEvents + data object ClearError : ChooseAccountProviderEvents +} 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 new file mode 100644 index 00000000000..2189252d01e --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt @@ -0,0 +1,62 @@ +/* + * 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.login.impl.screens.chooseaccountprovider + +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 com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.login.impl.util.openLearnMorePage +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails + +@ContributesNode(AppScope::class) +class ChooseAccountProviderNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: ChooseAccountProviderPresenter, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onLoginPasswordNeeded() + fun onOidcDetails(oidcDetails: OidcDetails) + fun onCreateAccountContinue(url: String) + } + + private fun onOidcDetails(oidcDetails: OidcDetails) { + plugins().forEach { it.onOidcDetails(oidcDetails) } + } + + private fun onLoginPasswordNeeded() { + plugins().forEach { it.onLoginPasswordNeeded() } + } + + private fun onCreateAccountContinue(url: String) { + plugins().forEach { it.onCreateAccountContinue(url) } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + ChooseAccountProviderView( + state = state, + modifier = modifier, + onBackClick = ::navigateUp, + onOidcDetails = ::onOidcDetails, + onNeedLoginPassword = ::onLoginPasswordNeeded, + onLearnMoreClick = { openLearnMorePage(context) }, + onCreateAccountContinue = ::onCreateAccountContinue, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt new file mode 100644 index 00000000000..464e30936fd --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -0,0 +1,80 @@ +/* + * 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.login.impl.screens.chooseaccountprovider + +import androidx.compose.runtime.Composable +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 io.element.android.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.uri.ensureProtocol +import javax.inject.Inject + +class ChooseAccountProviderPresenter @Inject constructor( + private val enterpriseService: EnterpriseService, + private val loginHelper: LoginHelper, +) : Presenter { + @Composable + override fun present(): ChooseAccountProviderState { + val localCoroutineScope = rememberCoroutineScope() + val loginMode by loginHelper.collectLoginMode() + + var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) } + + fun handleEvent(event: ChooseAccountProviderEvents) { + when (event) { + ChooseAccountProviderEvents.Continue -> { + selectedAccountProvider?.let { + loginHelper.submit( + coroutineScope = localCoroutineScope, + isAccountCreation = false, + homeserverUrl = it.url, + loginHint = null, + ) + } + } + is ChooseAccountProviderEvents.SelectAccountProvider -> { + // Ensure that the user do not change the server during processing + if (loginMode is AsyncData.Uninitialized) { + selectedAccountProvider = event.accountProvider + } + } + ChooseAccountProviderEvents.ClearError -> loginHelper.clearError() + } + } + + val staticAccountProviderList = remember { + // The list cannot contains ANY_ACCOUNT_PROVIDER ("*") and cannot be empty at this point + enterpriseService.defaultHomeserverList() + .map { it.ensureProtocol() } + .map { url -> + AccountProvider( + url = url, + subtitle = null, + isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, + isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, + isValid = true, + ) + } + } + + return ChooseAccountProviderState( + accountProviders = staticAccountProviderList, + selectedAccountProvider = selectedAccountProvider, + loginMode = loginMode, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt new file mode 100644 index 00000000000..35913340473 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt @@ -0,0 +1,23 @@ +/* + * 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.login.impl.screens.chooseaccountprovider + +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncData + +// Do not use default value, so no member get forgotten in the presenters. +data class ChooseAccountProviderState( + val accountProviders: List, + val selectedAccountProvider: AccountProvider?, + val loginMode: AsyncData, + val eventSink: (ChooseAccountProviderEvents) -> Unit, +) { + val submitEnabled: Boolean + get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt new file mode 100644 index 00000000000..b921fee3308 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt @@ -0,0 +1,77 @@ +/* + * 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.login.impl.screens.chooseaccountprovider + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncData + +open class ChooseAccountProviderStateProvider : PreviewParameterProvider { + private val server1 = anAccountProvider( + url = "https://server1.io", + subtitle = null, + isPublic = false, + isMatrixOrg = false, + ) + private val server2 = anAccountProvider( + url = "https://server2.io", + subtitle = null, + isPublic = false, + isMatrixOrg = false, + ) + private val server3 = anAccountProvider( + url = "https://server3.io", + subtitle = null, + isPublic = false, + isMatrixOrg = false, + ) + override val values: Sequence + get() = sequenceOf( + aChooseAccountProviderState( + accountProviders = listOf( + server1, + server2, + server3, + ) + ), + aChooseAccountProviderState( + accountProviders = listOf( + server1, + server2, + server3, + ), + selectedAccountProvider = server2, + ), + aChooseAccountProviderState( + accountProviders = listOf( + server1, + server2, + server3, + ), + selectedAccountProvider = server2, + loginMode = AsyncData.Loading(), + ), + // Add other state here + ) +} + +fun aChooseAccountProviderState( + accountProviders: List = listOf( + anAccountProvider() + ), + selectedAccountProvider: AccountProvider? = null, + loginMode: AsyncData = AsyncData.Uninitialized, + eventSink: (ChooseAccountProviderEvents) -> Unit = {}, +) = ChooseAccountProviderState( + accountProviders = accountProviders, + selectedAccountProvider = selectedAccountProvider, + loginMode = loginMode, + eventSink = eventSink, +) 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 new file mode 100644 index 00000000000..760e39ce851 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt @@ -0,0 +1,150 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.features.login.impl.screens.chooseaccountprovider + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.accountprovider.AccountProviderView +import io.element.android.features.login.impl.login.LoginModeView +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.BigIcon +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.Button +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun ChooseAccountProviderView( + state: ChooseAccountProviderState, + onBackClick: () -> Unit, + onOidcDetails: (OidcDetails) -> Unit, + onNeedLoginPassword: () -> Unit, + onLearnMoreClick: () -> Unit, + onCreateAccountContinue: (url: String) -> Unit, + modifier: Modifier = Modifier, +) { + val isLoading by remember(state.loginMode) { + derivedStateOf { + state.loginMode is AsyncData.Loading + } + } + + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + navigationIcon = { BackButton(onClick = onBackClick) } + ) + } + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(padding) + .consumeWindowInsets(padding) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(state = rememberScrollState()) + ) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp), + iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()), + title = stringResource(id = R.string.screen_server_confirmation_title_picker_mode), + subTitle = null, + ) + + state.accountProviders.forEach { item -> + val alteredItem = if (item.isMatrixOrg) { + // Set the subtitle from the resource + item.copy( + subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle), + ) + } else { + item + } + AccountProviderView( + item = alteredItem, + selected = item == state.selectedAccountProvider, + onClick = { + state.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(item)) + } + ) + } + Spacer(Modifier.height(32.dp)) + // Flexible spacing to keep the submit button at the bottom + Spacer(modifier = Modifier.weight(1f)) + Button( + text = stringResource(id = CommonStrings.action_continue), + showProgress = isLoading, + onClick = { + state.eventSink(ChooseAccountProviderEvents.Continue) + }, + enabled = state.submitEnabled || isLoading, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(48.dp)) + } + LoginModeView( + loginMode = state.loginMode, + onClearError = { + state.eventSink(ChooseAccountProviderEvents.ClearError) + }, + onLearnMoreClick = onLearnMoreClick, + onOidcDetails = onOidcDetails, + onNeedLoginPassword = onNeedLoginPassword, + onCreateAccountContinue = onCreateAccountContinue, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountProviderStateProvider::class) state: ChooseAccountProviderState) = ElementPreview { + ChooseAccountProviderView( + state = state, + onBackClick = { }, + onLearnMoreClick = { }, + onOidcDetails = { }, + onNeedLoginPassword = { }, + onCreateAccountContinue = { }, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index 359e556e696..3bcc81ac831 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -8,38 +8,20 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported -import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever -import io.element.android.libraries.architecture.AsyncData +import io.element.android.features.login.impl.login.LoginHelper import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.auth.OidcPrompt -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcActionFlow -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch class ConfirmAccountProviderPresenter @AssistedInject constructor( @Assisted private val params: Params, private val accountProviderDataSource: AccountProviderDataSource, - private val authenticationService: MatrixAuthenticationService, - private val oidcActionFlow: OidcActionFlow, - private val defaultLoginUserStory: DefaultLoginUserStory, - private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, + private val loginHelper: LoginHelper, ) : Presenter { data class Params( val isAccountCreation: Boolean, @@ -52,94 +34,30 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( @Composable override fun present(): ConfirmAccountProviderState { - val accountProvider by accountProviderDataSource.flow().collectAsState() + val accountProvider by accountProviderDataSource.flow.collectAsState() val localCoroutineScope = rememberCoroutineScope() - val loginFlowAction: MutableState> = remember { - mutableStateOf(AsyncData.Uninitialized) - } - - LaunchedEffect(Unit) { - oidcActionFlow.collect { oidcAction -> - if (oidcAction != null) { - onOidcAction(oidcAction, loginFlowAction) - } - } - } + val loginMode by loginHelper.collectLoginMode() fun handleEvents(event: ConfirmAccountProviderEvents) { when (event) { ConfirmAccountProviderEvents.Continue -> { - localCoroutineScope.submit(accountProvider.url, loginFlowAction) + loginHelper.submit( + coroutineScope = localCoroutineScope, + isAccountCreation = params.isAccountCreation, + homeserverUrl = accountProvider.url, + loginHint = null, + ) } - ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = AsyncData.Uninitialized + ConfirmAccountProviderEvents.ClearError -> loginHelper.clearError() } } return ConfirmAccountProviderState( accountProvider = accountProvider, isAccountCreation = params.isAccountCreation, - loginFlow = loginFlowAction.value, + loginMode = loginMode, eventSink = ::handleEvents ) } - - private fun CoroutineScope.submit( - homeserverUrl: String, - loginFlowAction: MutableState>, - ) = launch { - suspend { - authenticationService.setHomeserver(homeserverUrl).map { - val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!! - if (matrixHomeServerDetails.supportsOidcLogin) { - // Retrieve the details right now - val oidcPrompt = if (params.isAccountCreation) OidcPrompt.Create else OidcPrompt.Consent - LoginFlow.OidcFlow(authenticationService.getOidcUrl(oidcPrompt).getOrThrow()) - } else if (params.isAccountCreation) { - val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) - LoginFlow.AccountCreationFlow(url) - } else if (matrixHomeServerDetails.supportsPasswordLogin) { - LoginFlow.PasswordLogin - } else { - error("Unsupported login flow") - } - }.getOrThrow() - }.runCatchingUpdatingState( - state = loginFlowAction, - errorTransform = { - when (it) { - is AccountCreationNotSupported -> it - else -> ChangeServerError.from(it) - } - } - ) - } - - private suspend fun onOidcAction( - oidcAction: OidcAction, - loginFlowAction: MutableState>, - ) { - loginFlowAction.value = AsyncData.Loading() - when (oidcAction) { - OidcAction.GoBack -> { - authenticationService.cancelOidcLogin() - .onSuccess { - loginFlowAction.value = AsyncData.Uninitialized - } - .onFailure { failure -> - loginFlowAction.value = AsyncData.Failure(failure) - } - } - is OidcAction.Success -> { - authenticationService.loginWithOidc(oidcAction.url) - .onSuccess { _ -> - defaultLoginUserStory.setLoginFlowIsDone(true) - } - .onFailure { failure -> - loginFlowAction.value = AsyncData.Failure(failure) - } - } - } - oidcActionFlow.reset() - } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index aaf0d2562ac..d0da9ab85ef 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -8,21 +8,15 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.login.LoginMode import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.auth.OidcDetails // Do not use default value, so no member get forgotten in the presenters. data class ConfirmAccountProviderState( val accountProvider: AccountProvider, val isAccountCreation: Boolean, - val loginFlow: AsyncData, + val loginMode: AsyncData, val eventSink: (ConfirmAccountProviderEvents) -> Unit ) { - val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is AsyncData.Uninitialized || loginFlow is AsyncData.Loading) -} - -sealed interface LoginFlow { - data object PasswordLogin : LoginFlow - data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow - data class AccountCreationFlow(val url: String) : LoginFlow + val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt index 3edf84f7c15..90d34571e9d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.libraries.architecture.AsyncData @@ -22,7 +23,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + loginMode: AsyncData = AsyncData.Uninitialized, eventSink: (ConfirmAccountProviderEvents) -> Unit = {}, ) = ConfirmAccountProviderState( accountProvider = accountProvider, isAccountCreation = isAccountCreation, - loginFlow = loginFlow, + loginMode = loginMode, eventSink = eventSink ) 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 79d76f41ec5..5143b988e7b 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 @@ -9,8 +9,6 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -19,16 +17,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog -import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.BigIcon -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.Button @@ -48,9 +44,9 @@ fun ConfirmAccountProviderView( onChange: () -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginFlow) { + val isLoading by remember(state.loginMode) { derivedStateOf { - state.loginFlow is AsyncData.Loading + state.loginMode is AsyncData.Loading } } val eventSink = state.eventSink @@ -60,7 +56,7 @@ fun ConfirmAccountProviderView( header = { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 60.dp), - iconStyle = BigIcon.Style.Default(Icons.Filled.AccountCircle), + iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()), title = stringResource( id = if (state.isAccountCreation) { R.string.screen_account_provider_signup_title @@ -92,7 +88,7 @@ fun ConfirmAccountProviderView( /* TextButton( text = stringResource(id = R.string.screen_account_provider_change), onClick = onChange, - enabled = true, + enabled = !isLoading, modifier = Modifier .fillMaxWidth() .testTag(TestTags.loginChangeServer) @@ -100,48 +96,16 @@ fun ConfirmAccountProviderView( } } ) { - when (state.loginFlow) { - is AsyncData.Failure -> { - when (val error = state.loginFlow.error) { - is ChangeServerError.Error -> { - ErrorDialog( - content = error.message(), - onSubmit = { - eventSink.invoke(ConfirmAccountProviderEvents.ClearError) - } - ) - } - is ChangeServerError.SlidingSyncAlert -> { - SlidingSyncNotSupportedDialog( - onLearnMoreClick = { - onLearnMoreClick() - eventSink(ConfirmAccountProviderEvents.ClearError) - }, - onDismiss = { - eventSink(ConfirmAccountProviderEvents.ClearError) - } - ) - } - is AccountCreationNotSupported -> { - ErrorDialog( - content = stringResource(CommonStrings.error_account_creation_not_possible), - onSubmit = { - eventSink.invoke(ConfirmAccountProviderEvents.ClearError) - } - ) - } - } - } - is AsyncData.Loading -> Unit // The Continue button shows the loading state - is AsyncData.Success -> { - when (val loginFlowState = state.loginFlow.data) { - is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails) - LoginFlow.PasswordLogin -> onNeedLoginPassword() - is LoginFlow.AccountCreationFlow -> onCreateAccountContinue(loginFlowState.url) - } - } - AsyncData.Uninitialized -> Unit - } + LoginModeView( + loginMode = state.loginMode, + onClearError = { + eventSink(ConfirmAccountProviderEvents.ClearError) + }, + onLearnMoreClick = onLearnMoreClick, + onOidcDetails = onOidcDetails, + onNeedLoginPassword = onNeedLoginPassword, + onCreateAccountContinue = onCreateAccountContinue, + ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountNode.kt index 6cd039a2c75..128a4c03e87 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountNode.kt @@ -8,9 +8,9 @@ package io.element.android.features.login.impl.screens.createaccount import android.app.Activity +import androidx.activity.compose.LocalActivity 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 @@ -41,7 +41,7 @@ class CreateAccountNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val activity = LocalContext.current as Activity + val activity = requireNotNull(LocalActivity.current) val isDark = ElementTheme.isLightTheme.not() val state = presenter.present() CreateAccountView( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt index 61d90b84e51..371c3c39106 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt @@ -19,16 +19,23 @@ import dagger.assisted.AssistedInject import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.extensions.flatMap +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration.Companion.seconds class CreateAccountPresenter @AssistedInject constructor( @Assisted private val url: String, private val authenticationService: MatrixAuthenticationService, + private val clientProvider: MatrixClientProvider, private val defaultLoginUserStory: DefaultLoginUserStory, private val messageParser: MessageParser, private val buildMeta: BuildMeta, @@ -68,11 +75,17 @@ class CreateAccountPresenter @AssistedInject constructor( private fun CoroutineScope.importSession(message: String, loggedInState: MutableState>) = launch { loggedInState.value = AsyncAction.Loading - runCatching { + runCatchingExceptions { messageParser.parse(message) }.flatMap { externalSession -> authenticationService.importCreatedSession(externalSession) }.onSuccess { sessionId -> + tryOrNull { + // Wait until the session is verified + val client = clientProvider.getOrRestore(sessionId).getOrThrow() + val sessionVerificationService = client.sessionVerificationService() + withTimeout(10.seconds) { sessionVerificationService.sessionVerifiedStatus.first { it.isVerified() } } + } // We will not navigate to the WaitList screen, so the login user story is done defaultLoginUserStory.setLoginFlowIsDone(true) loggedInState.value = AsyncAction.Success(sessionId) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt index 3618bc301c0..e2e7ac32511 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt @@ -17,7 +17,7 @@ open class CreateAccountStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLoginPasswordState(), // Loading - aLoginPasswordState().copy(loginAction = AsyncData.Loading()), + aLoginPasswordState(loginAction = AsyncData.Loading()), // Error - aLoginPasswordState().copy(loginAction = AsyncData.Failure(Exception("An error occurred"))), + aLoginPasswordState(loginAction = AsyncData.Failure(Exception("An error occurred"))), ) } -fun aLoginPasswordState() = LoginPasswordState( - accountProvider = anAccountProvider(), - formState = LoginFormState.Default, - loginAction = AsyncData.Uninitialized, - eventSink = {} +fun aLoginPasswordState( + accountProvider: AccountProvider = anAccountProvider(), + formState: LoginFormState = LoginFormState.Default, + loginAction: AsyncData = AsyncData.Uninitialized, + eventSink: (LoginPasswordEvents) -> Unit = {}, +) = LoginPasswordState( + accountProvider = accountProvider, + formState = formState, + loginAction = loginAction, + eventSink = eventSink, +) + +fun aLoginFormState( + login: String = "", + password: String = "", +) = LoginFormState( + login = login, + password = password, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index c61a3cac724..5cb0c23d9c1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -7,6 +7,7 @@ package io.element.android.features.login.impl.screens.loginpassword +import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -21,8 +22,6 @@ 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.material.icons.Icons -import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -30,12 +29,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.platform.LocalAutofillManager import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -53,7 +54,6 @@ import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.form.textFieldState -import io.element.android.libraries.designsystem.modifiers.autofill import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -73,6 +73,13 @@ fun LoginPasswordView( onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { + val autofillManager = LocalAutofillManager.current + + BackHandler { + autofillManager?.cancel() + onBackClick() + } + val isLoading by remember(state.loginAction) { derivedStateOf { state.loginAction is AsyncData.Loading @@ -84,6 +91,8 @@ fun LoginPasswordView( // Clear focus to prevent keyboard issues with textfields focusManager.clearFocus(force = true) + autofillManager?.commit() + state.eventSink(LoginPasswordEvents.Submit) } @@ -92,7 +101,12 @@ fun LoginPasswordView( topBar = { TopAppBar( title = {}, - navigationIcon = { BackButton(onClick = onBackClick) }, + navigationIcon = { + BackButton(onClick = { + autofillManager?.cancel() + onBackClick() + }) + }, ) } ) { padding -> @@ -100,17 +114,17 @@ fun LoginPasswordView( Column( modifier = Modifier - .fillMaxSize() - .imePadding() - .padding(padding) - .consumeWindowInsets(padding) - .verticalScroll(state = scrollState) - .padding(start = 20.dp, end = 20.dp, bottom = 20.dp), + .fillMaxSize() + .imePadding() + .padding(padding) + .consumeWindowInsets(padding) + .verticalScroll(state = scrollState) + .padding(start = 20.dp, end = 20.dp, bottom = 20.dp), ) { // Title IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 20.dp, start = 16.dp, end = 16.dp), - iconStyle = BigIcon.Style.Default(Icons.Filled.AccountCircle), + iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()), title = stringResource( id = R.string.screen_account_provider_signin_title, state.accountProvider.title @@ -139,8 +153,8 @@ fun LoginPasswordView( onClick = ::submit, enabled = state.submitEnabled || isLoading, modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.loginContinue) + .fillMaxWidth() + .testTag(TestTags.loginContinue) ) Spacer(modifier = Modifier.height(48.dp)) } @@ -155,7 +169,6 @@ fun LoginPasswordView( } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun LoginForm( state: LoginPasswordState, @@ -177,14 +190,9 @@ private fun LoginForm( .fillMaxWidth() .onTabOrEnterKeyFocusNext(focusManager) .testTag(TestTags.loginEmailUsername) - .autofill( - autofillTypes = listOf(AutofillType.Username), - onFill = { - val sanitized = it.sanitize() - loginFieldState = sanitized - eventSink(LoginPasswordEvents.SetLogin(sanitized)) - } - ), + .semantics { + contentType = ContentType.Username + }, placeholder = stringResource(CommonStrings.common_username), onValueChange = { val sanitized = it.sanitize() @@ -203,6 +211,7 @@ private fun LoginForm( { Box(Modifier.clickable { loginFieldState = "" + eventSink(LoginPasswordEvents.SetLogin("")) }) { Icon( imageVector = CompoundIcons.Close(), @@ -228,14 +237,9 @@ private fun LoginForm( .fillMaxWidth() .onTabOrEnterKeyFocusNext(focusManager) .testTag(TestTags.loginPassword) - .autofill( - autofillTypes = listOf(AutofillType.Password), - onFill = { - val sanitized = it.sanitize() - passwordFieldState = sanitized - eventSink(LoginPasswordEvents.SetPassword(sanitized)) - } - ), + .semantics { + contentType = ContentType.Password + }, onValueChange = { val sanitized = it.sanitize() passwordFieldState = sanitized diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvents.kt new file mode 100644 index 00000000000..5e0cc054fca --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvents.kt @@ -0,0 +1,17 @@ +/* + * 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.login.impl.screens.onboarding + +sealed interface OnBoardingEvents { + data class OnSignIn( + val defaultAccountProvider: String + ) : OnBoardingEvents + + data object OnVersionClick : OnBoardingEvents + data object ClearError : OnBoardingEvents +} 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 new file mode 100644 index 00000000000..d9c1615fde8 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt @@ -0,0 +1,101 @@ +/* + * 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. + */ + +package io.element.android.features.login.impl.screens.onboarding + +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 com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.login.impl.util.openLearnMorePage +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails + +@ContributesNode(AppScope::class) +class OnBoardingNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: OnBoardingPresenter.Factory, +) : Node( + buildContext = buildContext, + plugins = plugins +) { + interface Callback : Plugin { + fun onSignUp() + fun onSignIn(mustChooseAccountProvider: Boolean) + fun onSignInWithQrCode() + fun onReportProblem() + fun onLoginPasswordNeeded() + fun onOidcDetails(oidcDetails: OidcDetails) + fun onCreateAccountContinue(url: String) + } + + data class Params( + val accountProvider: String?, + val loginHint: String?, + ) : NodeInputs + + private val params = inputs() + + private val presenter = presenterFactory.create( + params = params, + ) + + private fun onSignIn(mustChooseAccountProvider: Boolean) { + plugins().forEach { it.onSignIn(mustChooseAccountProvider) } + } + + private fun onSignUp() { + plugins().forEach { it.onSignUp() } + } + + private fun onSignInWithQrCode() { + plugins().forEach { it.onSignInWithQrCode() } + } + + private fun onReportProblem() { + plugins().forEach { it.onReportProblem() } + } + + private fun onOidcDetails(data: OidcDetails) { + plugins().forEach { it.onOidcDetails(data) } + } + + private fun onLoginPasswordNeeded() { + plugins().forEach { it.onLoginPasswordNeeded() } + } + + private fun onCreateAccountContinue(url: String) { + plugins().forEach { it.onCreateAccountContinue(url) } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + OnBoardingView( + state = state, + modifier = modifier, + onSignIn = ::onSignIn, + onCreateAccount = ::onSignUp, + onSignInWithQrCode = ::onSignInWithQrCode, + onReportProblem = ::onReportProblem, + onOidcDetails = ::onOidcDetails, + onNeedLoginPassword = ::onLoginPasswordNeeded, + onLearnMoreClick = { openLearnMorePage(context) }, + onCreateAccountContinue = ::onCreateAccountContinue, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt new file mode 100644 index 00000000000..37dce8e4b5a --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -0,0 +1,114 @@ +/* + * 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. + */ + +package io.element.android.features.login.impl.screens.onboarding + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.appconfig.OnBoardingConfig +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.api.canConnectToAnyHomeserver +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.ui.utils.MultipleTapToUnlock + +class OnBoardingPresenter @AssistedInject constructor( + @Assisted private val params: OnBoardingNode.Params, + private val buildMeta: BuildMeta, + private val featureFlagService: FeatureFlagService, + private val enterpriseService: EnterpriseService, + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, + private val loginHelper: LoginHelper, +) : Presenter { + @AssistedFactory + interface Factory { + fun create( + params: OnBoardingNode.Params, + ): OnBoardingPresenter + } + + private val multipleTapToUnlock = MultipleTapToUnlock() + + @Composable + override fun present(): OnBoardingState { + val localCoroutineScope = rememberCoroutineScope() + val forcedAccountProvider = remember { + // If defaultHomeserverList() returns a singleton list, this is the default account provider. + // In this case, the user can sign in using this homeserver, or use QrCode login + enterpriseService.defaultHomeserverList().singleOrNull() + } + val canConnectToAnyHomeserver = remember { + enterpriseService.canConnectToAnyHomeserver() + } + val mustChooseAccountProvider = remember { + !canConnectToAnyHomeserver && enterpriseService.defaultHomeserverList().size > 1 + } + val linkAccountProvider by produceState(initialValue = null) { + // Account provider from the link, if allowed by the enterprise service + value = params.accountProvider?.takeIf { + enterpriseService.isAllowedToConnectToHomeserver(it) + } + } + val defaultAccountProvider = remember(linkAccountProvider) { + // If there is a forced account provider, this is the default account provider + // Else use the account provider passed in the params if any and if allowed + forcedAccountProvider ?: linkAccountProvider + } + val canLoginWithQrCode by produceState(initialValue = false, linkAccountProvider) { + value = linkAccountProvider == null && + featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin) + } + val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } + var showReportBug by rememberSaveable { mutableStateOf(false) } + + val loginMode by loginHelper.collectLoginMode() + + fun handleEvent(event: OnBoardingEvents) { + when (event) { + is OnBoardingEvents.OnSignIn -> loginHelper.submit( + coroutineScope = localCoroutineScope, + isAccountCreation = false, + homeserverUrl = event.defaultAccountProvider, + loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, + ) + OnBoardingEvents.ClearError -> loginHelper.clearError() + OnBoardingEvents.OnVersionClick -> { + if (canReportBug) { + if (multipleTapToUnlock.unlock(localCoroutineScope)) { + showReportBug = true + } + } + } + } + } + + return OnBoardingState( + productionApplicationName = buildMeta.productionApplicationName, + defaultAccountProvider = defaultAccountProvider, + mustChooseAccountProvider = mustChooseAccountProvider, + canLoginWithQrCode = canLoginWithQrCode, + canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT, + canReportBug = canReportBug && showReportBug, + loginMode = loginMode, + version = buildMeta.versionName, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt new file mode 100644 index 00000000000..1e55c3af2d5 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +package io.element.android.features.login.impl.screens.onboarding + +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncData + +data class OnBoardingState( + val productionApplicationName: String, + val defaultAccountProvider: String?, + val mustChooseAccountProvider: Boolean, + val canLoginWithQrCode: Boolean, + val canCreateAccount: Boolean, + val canReportBug: Boolean, + val version: String, + val loginMode: AsyncData, + val eventSink: (OnBoardingEvents) -> Unit, +) { + val submitEnabled: Boolean + get() = defaultAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt new file mode 100644 index 00000000000..cdf77da5233 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt @@ -0,0 +1,46 @@ +/* + * 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. + */ + +package io.element.android.features.login.impl.screens.onboarding + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncData + +open class OnBoardingStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anOnBoardingState(), + anOnBoardingState(canLoginWithQrCode = true), + anOnBoardingState(canCreateAccount = true), + anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true), + anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true), + anOnBoardingState(defaultAccountProvider = "element.io", canCreateAccount = false, canReportBug = true), + ) +} + +fun anOnBoardingState( + productionApplicationName: String = "Element", + defaultAccountProvider: String? = null, + mustChooseAccountProvider: Boolean = false, + canLoginWithQrCode: Boolean = false, + canCreateAccount: Boolean = false, + canReportBug: Boolean = false, + version: String = "1.0.0", + loginMode: AsyncData = AsyncData.Uninitialized, + eventSink: (OnBoardingEvents) -> Unit = {}, +) = OnBoardingState( + productionApplicationName = productionApplicationName, + defaultAccountProvider = defaultAccountProvider, + mustChooseAccountProvider = mustChooseAccountProvider, + canLoginWithQrCode = canLoginWithQrCode, + canCreateAccount = canCreateAccount, + canReportBug = canReportBug, + version = version, + loginMode = loginMode, + eventSink = eventSink, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt new file mode 100644 index 00000000000..6c67e75cd14 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -0,0 +1,242 @@ +/* + * 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. + */ + +package io.element.android.features.login.impl.screens.onboarding + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.BiasAlignment +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 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.login.impl.R +import io.element.android.features.login.impl.login.LoginModeView +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom +import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage +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 +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.strings.CommonStrings + +// Refs: +// FTUE: +// - https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0 +// ElementX: +// - https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=1816-97419 +@Composable +fun OnBoardingView( + state: OnBoardingState, + onSignInWithQrCode: () -> Unit, + onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, + onCreateAccount: () -> Unit, + onOidcDetails: (OidcDetails) -> Unit, + onNeedLoginPassword: () -> Unit, + onLearnMoreClick: () -> Unit, + onCreateAccountContinue: (url: String) -> Unit, + onReportProblem: () -> Unit, + modifier: Modifier = Modifier, +) { + OnBoardingPage( + modifier = modifier, + content = { + OnBoardingContent(state = state) + LoginModeView( + loginMode = state.loginMode, + onClearError = { + state.eventSink(OnBoardingEvents.ClearError) + }, + onLearnMoreClick = onLearnMoreClick, + onOidcDetails = onOidcDetails, + onNeedLoginPassword = onNeedLoginPassword, + onCreateAccountContinue = onCreateAccountContinue, + ) + }, + footer = { + OnBoardingButtons( + state = state, + onSignInWithQrCode = onSignInWithQrCode, + onSignIn = onSignIn, + onCreateAccount = onCreateAccount, + onReportProblem = onReportProblem, + ) + } + ) +} + +@Composable +private fun OnBoardingContent(state: OnBoardingState) { + Box( + modifier = Modifier.fillMaxSize(), + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = BiasAlignment( + horizontalBias = 0f, + verticalBias = -0.4f + ) + ) { + ElementLogoAtom( + size = ElementLogoAtomSize.Large, + modifier = Modifier.padding(top = ElementLogoAtomSize.Large.shadowRadius / 2) + ) + } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = BiasAlignment( + horizontalBias = 0f, + verticalBias = 0.6f + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = CenterHorizontally, + ) { + Text( + text = stringResource(id = R.string.screen_onboarding_welcome_title), + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontHeadingLgBold, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.screen_onboarding_welcome_message, state.productionApplicationName), + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = 17.sp), + textAlign = TextAlign.Center + ) + } + } + } +} + +@Composable +private fun OnBoardingButtons( + state: OnBoardingState, + onSignInWithQrCode: () -> Unit, + onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, + onCreateAccount: () -> Unit, + onReportProblem: () -> Unit, +) { + val isLoading by remember(state.loginMode) { + derivedStateOf { + state.loginMode is AsyncData.Loading + } + } + + ButtonColumnMolecule { + val signInButtonStringRes = if (state.canLoginWithQrCode || state.canCreateAccount) { + R.string.screen_onboarding_sign_in_manually + } else { + CommonStrings.action_continue + } + if (state.canLoginWithQrCode) { + Button( + text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code), + leadingIcon = IconSource.Vector(CompoundIcons.QrCode()), + onClick = onSignInWithQrCode, + modifier = Modifier.fillMaxWidth() + ) + } + val defaultAccountProvider = state.defaultAccountProvider + if (defaultAccountProvider == null) { + Button( + text = stringResource(id = signInButtonStringRes), + onClick = { + onSignIn(state.mustChooseAccountProvider) + }, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.onBoardingSignIn) + ) + } else { + Button( + text = stringResource(id = R.string.screen_onboarding_sign_in_to, defaultAccountProvider), + showProgress = isLoading, + onClick = { + state.eventSink(OnBoardingEvents.OnSignIn(defaultAccountProvider)) + }, + enabled = state.submitEnabled || isLoading, + modifier = Modifier + .fillMaxWidth() + ) + } + if (state.canCreateAccount) { + TextButton( + text = stringResource(id = R.string.screen_onboarding_sign_up), + onClick = onCreateAccount, + modifier = Modifier + .fillMaxWidth() + ) + } + if (state.canReportBug) { + // Add a report problem text button. Use a Text since we need a special theme here. + Text( + modifier = Modifier + .clickable(onClick = onReportProblem) + .padding(16.dp), + text = stringResource(id = CommonStrings.common_report_a_problem), + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + } else { + Text( + modifier = Modifier + .clickable { + state.eventSink(OnBoardingEvents.OnVersionClick) + } + .padding(16.dp), + text = stringResource(id = R.string.screen_onboarding_app_version, state.version), + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun OnBoardingViewPreview( + @PreviewParameter(OnBoardingStateProvider::class) state: OnBoardingState +) = ElementPreview { + OnBoardingView( + state = state, + onSignInWithQrCode = {}, + onSignIn = {}, + onCreateAccount = {}, + onReportProblem = {}, + onOidcDetails = {}, + onNeedLoginPassword = {}, + onLearnMoreClick = {}, + onCreateAccountContinue = {}, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt index 692d9209586..49b94ac5042 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt @@ -15,6 +15,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException import io.element.android.features.login.impl.qrcode.QrCodeLoginManager import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -36,6 +38,7 @@ class QrCodeScanPresenter @Inject constructor( private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory, private val qrCodeLoginManager: QrCodeLoginManager, private val coroutineDispatchers: CoroutineDispatchers, + private val enterpriseService: EnterpriseService, ) : Presenter { private var isScanning by mutableStateOf(true) @@ -90,9 +93,17 @@ class QrCodeScanPresenter @Inject constructor( launch(coroutineDispatchers.computation) { suspend { - qrCodeLoginDataFactory.parseQrCodeData(code).onFailure { + val data = qrCodeLoginDataFactory.parseQrCodeData(code).onFailure { Timber.e(it, "Error parsing QR code data") }.getOrThrow() + val serverName = data.serverName() + if (serverName != null && enterpriseService.isAllowedToConnectToHomeserver(serverName).not()) { + throw UnauthorizedAccountProviderException( + unauthorisedAccountProviderTitle = serverName, + authorisedAccountProviderTitles = enterpriseService.defaultHomeserverList(), + ) + } + data }.runCatchingUpdatingState(codeScannedAction) }.invokeOnCompletion { isProcessingCode.set(false) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt index e9d32c0699f..cdea9f8b411 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.login.impl.screens.qrcode.scan import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException @@ -19,6 +20,15 @@ open class QrCodeScanStateProvider : PreviewParameterProvider { aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Loading), aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(Exception("Error"))), aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(QrLoginException.OtherDeviceNotSignedIn)), + aQrCodeScanState( + isScanning = false, + authenticationAction = AsyncAction.Failure( + UnauthorizedAccountProviderException( + unauthorisedAccountProviderTitle = "example.com", + authorisedAccountProviderTitles = listOf("element.io", "element.org"), + ) + ) + ), // Add other state here ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt index 7f7df3e3b25..0c5598c5281 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.progressSemantics -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -36,6 +35,7 @@ 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.login.impl.R +import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage import io.element.android.libraries.designsystem.components.BigIcon @@ -137,26 +137,38 @@ private fun ColumnScope.Buttons( verticalAlignment = Alignment.CenterVertically, ) { Icon( - imageVector = CompoundIcons.Error(), - tint = MaterialTheme.colorScheme.error, + imageVector = CompoundIcons.ErrorSolid(), + tint = ElementTheme.colors.iconCriticalPrimary, contentDescription = null, modifier = Modifier.size(24.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( text = when (error) { + is UnauthorizedAccountProviderException -> { + stringResource( + id = R.string.screen_change_server_error_unauthorized_homeserver_title, + error.unauthorisedAccountProviderTitle, + ) + } is QrLoginException.OtherDeviceNotSignedIn -> { stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_subtitle) } else -> stringResource(R.string.screen_qr_code_login_invalid_scan_state_subtitle) }, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.error, + color = ElementTheme.colors.textCriticalPrimary, style = ElementTheme.typography.fontBodySmMedium, ) } Text( text = when (error) { + is UnauthorizedAccountProviderException -> { + stringResource( + id = R.string.screen_change_server_error_unauthorized_homeserver_content, + error.authorisedAccountProviderTitles.joinToString(), + ) + } is QrLoginException.OtherDeviceNotSignedIn -> { stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_description) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt index afa7dbe8a10..13bfd9e38ed 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue @@ -57,6 +56,7 @@ 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.TextField +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt deleted file mode 100644 index 6d06ad8c3e5..00000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.login.impl.util - -import io.element.android.appconfig.AuthenticationConfig -import io.element.android.features.login.impl.accountprovider.AccountProvider - -val defaultAccountProvider = AccountProvider( - url = AuthenticationConfig.DEFAULT_HOMESERVER_URL, - subtitle = null, - isPublic = AuthenticationConfig.DEFAULT_HOMESERVER_URL == AuthenticationConfig.MATRIX_ORG_URL, - isMatrixOrg = AuthenticationConfig.DEFAULT_HOMESERVER_URL == AuthenticationConfig.MATRIX_ORG_URL, -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt index 6c10faa05bb..36ee64c600f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt @@ -9,11 +9,11 @@ package io.element.android.features.login.impl.util import android.content.Context import android.content.Intent -import android.net.Uri +import androidx.core.net.toUri import io.element.android.appconfig.AuthenticationConfig import io.element.android.libraries.core.data.tryOrNull fun openLearnMorePage(context: Context) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(AuthenticationConfig.SLIDING_SYNC_READ_MORE_URL)) + val intent = Intent(Intent.ACTION_VIEW, AuthenticationConfig.SLIDING_SYNC_READ_MORE_URL.toUri()) tryOrNull { context.startActivity(intent) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt index a5d9ec754ae..3793f3a53b2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt @@ -7,7 +7,7 @@ package io.element.android.features.login.impl.web -import android.net.Uri +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.resolver.network.WellknownAPI @@ -43,7 +43,7 @@ class DefaultWebClientUrlForAuthenticationRetriever @Inject constructor( } val registrationHelperUrl = result.registrationHelperUrl return if (registrationHelperUrl != null) { - Uri.parse(registrationHelperUrl) + registrationHelperUrl.toUri() .buildUpon() .appendQueryParameter("hs_url", homeServerUrl) .build() diff --git a/features/login/impl/src/main/res/drawable/ic_public.xml b/features/login/impl/src/main/res/drawable/ic_public.xml deleted file mode 100644 index fc1eacbc9f5..00000000000 --- a/features/login/impl/src/main/res/drawable/ic_public.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - 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 e4276c99b8f..6fde5f6622a 100644 --- a/features/login/impl/src/main/res/values-be/translations.xml +++ b/features/login/impl/src/main/res/values-be/translations.xml @@ -16,9 +16,7 @@ "Нам не ўдалося звязацца з гэтым хатнім серверам. Упэўніцеся, што вы правільна ўвялі URL-адрас хатняга сервера. Калі URL-адрас пазначаны правільна, звярніцеся да адміністратара хатняга сервера за дадатковай дапамогай." "Sliding sync недаступны з-за праблемы ў вядомым файле: %1$s" - "Выбачце, гэты сервер не падтрымлівае sliding sync." "URL хатняга сервера" - "Вы можаце падключыцца толькі да існуючага сервера, які падтрымлівае sliding sync. Адміністратару хатняга сервера запатрабуецца наладзіць яго. %1$s" "Які адрас вашага сервера?" "Выберыце свой сервер" "Стварыць уліковы запіс" @@ -31,6 +29,12 @@ "Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі." "Сардэчна запрашаем!" "Увайсці ў %1$s" + "Увайсці ўручную" + "Увайсці з QR-кодам" + "Стварыць уліковы запіс" + "Сардэчна запрашаем у самы хуткі %1$s. Перавага ў хуткасці і прастаце." + "Сардэчна запрашаем у %1$s. Зараджаны, для хуткасці і прастаты." + "Будзьце ў сваім element" "Ўсталяванне бяспечнага злучэння" "Не атрымалася ўсталяваць бяспечнае злучэнне з новай прыладай. Існуючыя прылады па-ранейшаму ў бяспецы, і вам не трэба турбавацца пра іх." "Што зараз?" 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 eced3ef8280..bd321a9c5a8 100644 --- a/features/login/impl/src/main/res/values-bg/translations.xml +++ b/features/login/impl/src/main/res/values-bg/translations.xml @@ -12,13 +12,21 @@ "Използвайте друг доставчик на акаунт, като например собствен частен сървър или работен акаунт." "Промяна на доставчика на акаунт" "Какъв е адресът на вашия сървър?" + "Създаване на акаунт" "Този акаунт бе деактивиран." "Неправилно потребителско име и/или парола" "Въведете своите данни" "Matrix е отворена мрежа за сигурна, децентрализирана комуникация." "Добре дошли отново!" "Влизане в %1$s" + "Влизане ръчно" + "Влизане с QR код" + "Създаване на акаунт" + "Добре дошли в най-бързия %1$s досега. Супер зареден за скорост и простота." + "Добре дошли в %1$s. Супер зареден за скорост и простота." + "Бъдете в стихията си" "Повторен опит" + "Вашият код за потвърждение" "Промяна на доставчика на акаунт" "Matrix е отворена мрежа за сигурна, децентрализирана комуникация." "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли." 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 27aea5c8112..9d6e4c51f72 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -14,11 +14,14 @@ "Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet." "Změnit poskytovatele účtu" "Nepodařilo se nám připojit k tomuto domovskému serveru. Zkontrolujte prosím, zda jste správně zadali adresu URL domovského serveru. Pokud je adresa URL správná, obraťte se na správce domovského serveru, který vám poskytne další pomoc." - "Klouzavá synchronizace není k dispozici kvůli problému se souborem well-known: + "Server není k dispozici kvůli problému se souborem well-known: %1$s" - "Tento server v současné době nepodporuje klouzavou synchronizaci." + "Vybraný poskytovatel účtu nepodporuje klouzavou synchronizaci. Pro použití %1$s je nutná aktualizace serveru." + "Uživateli %1$s není dovoleno se připojit do %2$s." + "Tato aplikace byla nakonfigurována tak, aby umožňovala: %1$s." + "Poskytovatel účtu %1$s není povolen." "Adresa URL domovského serveru" - "Můžete se připojit pouze k serveru, který podporuje klouzavou synchronizaci. Správce vašeho domovského serveru jej bude muset nakonfigurovat. %1$s" + "Zadejte adresu domény." "Jaká je adresa vašeho serveru?" "Vyberte váš server" "Vytvořit účet" @@ -31,6 +34,13 @@ "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci." "Vítejte zpět!" "Přihlaste se k %1$s" + "Ruční přihlášení" + "Přihlásit se do %1$s" + "Přihlásit se pomocí QR kódu" + "Vytvořit účet" + "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" "Navazování zabezpečeného spojení" "K novému zařízení se nepodařilo navázat bezpečné připojení. Vaše stávající zařízení jsou stále v bezpečí a nemusíte se o ně obávat." "Co teď?" @@ -79,5 +89,6 @@ Zkuste se přihlásit ručně nebo naskenujte QR kód pomocí jiného zařízen "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci." "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily." "Chystáte se přihlásit do služby %1$s" + "Vyberte poskytovatele účtu" "Chystáte se vytvořit účet na %1$s" diff --git a/features/login/impl/src/main/res/values-cy/translations.xml b/features/login/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..a9e4b615419 --- /dev/null +++ b/features/login/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,95 @@ + + + "Newid darparwr cyfrif" + "Cyfeiriad y gweinydd cartref" + "Rhowch derm chwilio neu gyfeiriad parth." + "Chwiliwch am gwmni, gweinyddwr cymunedol neu weinydd preifat." + "Canfod darparwr cyfrif" + "Dyma lle bydd eich sgyrsiau\'n byw - yn union fel y byddech chi\'n defnyddio darparwr e-bost i gadw\'ch e-byst." + "Rydych ar fin mewngofnodi i %s" + "Dyma lle bydd eich sgyrsiau\'n byw - yn union fel y byddech chi\'n defnyddio darparwr e-bost i gadw\'ch e-byst." + "Rydych chi ar fin creu cyfrif ar %s" + "Mae Matrix.org yn weinydd mawr, rhad ac am ddim ar y rhwydwaith Matrix cyhoeddus ar gyfer cyfathrebu diogel, datganoledig, sy\'n cael ei redeg gan y Matrix.org Foundation." + "Arall" + "Defnyddiwch ddarparwr cyfrif gwahanol, fel eich gweinydd preifat eich hun neu gyfrif gwaith." + "Newid darparwr cyfrif" + "Doedd dim modd i ni gyrraedd y gweinydd cartref hwn. Gwiriwch eich bod wedi rhoi URL y gweinydd cartref yn gywir. Os yw\'r URL yn gywir, cysylltwch â gweinyddwr eich gweinydd cartref am ragor o help." + "Dyw cydweddu llithrig ddim ar gael oherwydd problem yn y ffeil .well-known: +%1$s" + "Dyw\'r darparwr cyfrif hwn ddim yn cefnogi cydweddu llithro. Mae angen uwchraddio\'r gweinydd i ddefnyddio %1$s." + "Does dim caniatâd i %1$s gysylltu â %2$s." + "Mae\'r ap hwn wedi\'i ffurfweddu i ganiatáu: %1$s." + "Dyw darparwr cyfrif %1$s dddim yn cael ei ganiatáu." + "URL y Gweinydd Cartref" + "Rhowch gyfeiriad parth." + "Beth yw cyfeiriad eich gweinydd?" + "Dewiswch eich gweinydd" + "Creu cyfrif" + "Mae\'r cyfrif hwn wedi\'i gau." + "Enw defnyddiwr a/neu gyfrinair anghywir" + "Nid yw hwn yn ddynodwr defnyddiwr dilys. Fformat disgwyliedig: ‘@user:homeserver.org’" + "Mae\'r gweinydd hwn wedi\'i ffurfweddu i ddefnyddio tocynnau adnewyddu. Nid yw\'r rhain yn cael eu cefnogi wrth ddefnyddio mewngofnodi ar sail cyfrinair." + "Nid yw\'r gweinydd cartref ddewiswyd yn cefnogi cyfrinair na mewngofnodi OIDC. Cysylltwch â\'ch gweinyddwr neu dewis gweinydd cartref arall." + "Rhowch eich manylion" + "Mae Matrix yn rhwydwaith agored ar gyfer cyfathrebu diogel, datganoledig." + "Croeso nôl!" + "Mewngofnodi i %1$s" + "Fersiwn %1$s" + "Mewngofnodwch â llaw" + "Mewngofnodi i %1$s" + "Mewngofnodwch gyda chod QR" + "Creu cyfrif" + "Croeso i\'r %1$s cyflymaf erioed. Yn nodedig am gyflymder a symlrwydd." + "Croeso i %1$s. Yn nodedig ar gyfer cyflymder a symlrwydd." + "Byddwch yn eich elfen" + "Yn creu cysylltiad diogel" + "Nid oedd modd gwneud cysylltiad diogel â\'r ddyfais newydd. Mae eich dyfeisiau presennol yn dal yn ddiogel a does dim angen i chi boeni amdanyn nhw." + "Beth nawr?" + "Ceisiwch fewngofnodi eto gyda chod QR rhag ofn bod hyn yn broblem rhwydwaith" + "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" + "Mewngofnodwch i\'ch dyfais arall ac yna ceisio eto, neu defnyddio ddyfais arall sydd eisoes wedi\'fewngofnodi." + "Dyw\'r ddyfais arall heb ei mewngofnodi" + "Cafodd y mewngofnodi ei ddiddymu ar y ddyfais arall." + "Cais mewngofnodi wedi\'i ddiddymu" + "Cafodd y mewngofnodi ar y ddyfais arall ei wrthod." + "Gwrthodwyd y mewngofnodi" + "Mewngofnodi wedi dod i ben. Ceisiwch eto." + "Heb gwblhau\'r mewngofnodi mewn pryd" + "Nid yw eich dyfais arall yn cefnogi mewngofnodi i %s gyda chod QR. + +Ceisiwch fewngofnodi â llaw, neu sganiwch y cod QR gyda dyfais arall." + "Nid yw\'r cod QR yn cael ei gefnogi" + "Nid yw darparwr eich cyfrif yn cefnogi %1$s." + "%1$s heb ei gefnogi" + "Yn barod i sganio" + "Agor %1$s ar ddyfais bwrdd gwaith" + "Cliciwch ar eich afatar" + "Dewiswch %1$s" + "“Cysylltu dyfais newydd”" + "Sganiwch y cod QR gyda\'r ddyfais hon" + "Dim ond ar gael os yw darparwr eich cyfrif yn ei gefnogi." + "Agorwch %1$s ar ddyfais arall i gael y cod QR" + "Defnyddiwch y cod QR sy\'n cael ei ddangos ar y ddyfais arall." + "Ceisiwch eto" + "Cod QR anghywir" + "Mynd i osodiadau camera" + "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" + "Sganiwch y cod QR" + "Cychwyn eto" + "Digwyddodd gwall annisgwyl. Ceisiwch eto." + "Yn aros am eich dyfais arall" + "Mae\'n bosibl y bydd darparwr eich cyfrif yn gofyn am y cod canlynol i ddilysu\'r mewngofnodi." + "Eich cod dilysu" + "Newid darparwr cyfrif" + "Gweinydd preifat ar gyfer gweithwyr Element." + "Mae Matrix yn rhwydwaith agored ar gyfer cyfathrebu diogel, datganoledig." + "Dyma lle bydd eich sgyrsiau\'n byw - yn union fel y byddech chi\'n defnyddio darparwr e-bost i gadw\'ch e-byst." + "Rydych ar fin mewngofnodi i %1$s" + "Dewiswch ddarparwr cyfrif" + "Rydych chi ar fin creu cyfrif ar %1$s" + diff --git a/features/login/impl/src/main/res/values-da/translations.xml b/features/login/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..cefd0b75a43 --- /dev/null +++ b/features/login/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,95 @@ + + + "Skift kontoudbyder" + "Hjemmeserverens adresse" + "Indtast et søgeudtryk eller en domæneadresse." + "Søg efter en virksomhed, et fællesskab eller en privat server." + "Find en kontoudbyder" + "Det er her, dine samtaler vil bo - ligesom du ville bruge en e-mail-udbyder til at opbevare dine e-mails." + "Du er ved at logge ind på %s" + "Det er her, dine samtaler vil bo - ligesom du ville bruge en e-mail-udbyder til at opbevare dine e-mails." + "Du er ved at oprette en konto på %s" + "Matrix.org er en stor, gratis server på det offentlige Matrix-netværk til sikker, decentraliseret kommunikation, drevet af Matrix.org Foundation." + "Anden" + "Brug en anden kontoudbyder, f.eks. din egen private server eller en arbejdskonto." + "Skift kontoudbyder" + "Vi kunne ikke nå denne hjemmeserver. Kontroller, at du har indtastet hjemmeserverens URL korrekt. Hvis URL-adressen er korrekt, skal du kontakte administratoren på din hjemmeserver for at få yderligere hjælp." + "Serveren er ikke tilgængelig på grund af et problem i .well-known-filen: +%1$s" + "Den valgte kontoudbyder understøtter ikke sliding sync. En opgradering af serveren er nødvendig for at bruge %1$s." + "%1$s har ikke tilladelse til at oprette forbindelse til %2$s." + "Denne app er konfigureret til at tillade: %1$s." + "Kontoudbyder %1$s er ikke tilladt." + "URL-adressen på din hjemmeserver" + "Indtast en domæneadresse." + "Hvad er adressen på din server?" + "Vælg din server" + "Opret konto" + "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 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" + "Version %1$s" + "Log ind manuelt" + "Log ind på %1$s" + "Log ind med QR-kode" + "Opret konto" + "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" + "Etablerer en sikker forbindelse" + "Der kunne ikke oprettes en sikker forbindelse til den nye enhed. Dine eksisterende enheder er stadig sikre, og du behøver ikke bekymre dig om dem." + "Hvad nu?" + "Prøv at logge ind igen med en QR-kode, hvis dette skyldtes et netværksproblem" + "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" + "Log på din anden enhed, og prøv derefter igen, eller brug en anden enhed, der allerede er logget på." + "Den anden enhed er ikke logget ind" + "Login blev annulleret på den anden enhed." + "Anmodning om login annulleret" + "Login blev afvist på den anden enhed." + "Login afvist" + "Login er udløbet. Prøv venligst igen." + "Login blev ikke afsluttet i tide" + "Din anden enhed understøtter ikke at logge ind på %s med en QR-kode. + +Prøv at logge ind manuelt, eller scan QR-koden med en anden enhed." + "QR-kode understøttes ikke" + "Din kontoudbyder understøtter ikke %1$s." + "%1$s understøttes ikke" + "Klar til at scanne" + "Åbn %1$s på en stationær enhed" + "Klik på din avatar" + "Vælg %1$s" + "\"Tilknyt ny enhed\"" + "Scan QR-koden med denne enhed" + "Kun tilgængeligt, hvis din kontoudbyder understøtter det." + "Åbn %1$s på en anden enhed for at få QR-koden" + "Brug den QR-kode, der bliver vist på den anden enhed." + "Prøv igen" + "Forkert QR-kode" + "Gå til kameraindstillinger" + "Du skal give tilladelse til at %1$s kan benytte enhedens kamera, for at fortsætte." + "Tillad kameraadgang for at scanne QR-koden" + "Scan QR-koden" + "Begynd forfra" + "Der opstod en uventet fejl. Prøv venligst igen." + "Venter på din anden enhed" + "Din kontoudbyder kan bede om følgende kode for at bekræfte login." + "Din bekræftelseskode" + "Skift kontoudbyder" + "En privat server til Element-medarbejdere." + "Matrix er et åbent netværk for sikker, decentraliseret kommunikation." + "Det er her, dine samtaler vil bo - ligesom du ville bruge en e-mail-udbyder til at opbevare dine e-mails." + "Du er ved at logge ind på %1$s" + "Vælg din kontoudbyder" + "Du er ved at oprette en konto på %1$s" + 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 0b6dd41c968..6dbfdcf032d 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -6,19 +6,22 @@ "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ürden." - "Du bist dabei, dich bei %s anzumelden" + "Sie sind dabei, sich bei %s anzumelden" "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" + "Sie sind 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" "Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Geschäftskonto." "Kontoanbieter wechseln" - "Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfe, ob du die Homeserver-URL korrekt eingegeben hast. Wenn die URL korrekt ist, wende dich an deinen Homeserver-Administrator, um weitere Hilfe zu erhalten." - "Sliding Sync ist aufgrund eines Problems im \"well-known file\" nicht verfügbar: + "Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfen Sie ob die Homeserver-URL korrekt eingegeben wurde. Wenn die URL korrekt ist, wenden Sie sich an ihren Homeserver- Administrator, um weitere Hilfe zu erhalten." + "Der Server ist aufgrund eines Problems in der \".well-known\" Datei nicht verfügbar: %1$s" - "Dieser Server unterstützt derzeit kein Sliding Sync." + "Der gewählte Kontoanbieter unterstützt Sliding Sync nicht. Für die Verwendung von %1$s ist ein Upgrade des Servers erforderlich." + "%1$s darf keine Verbindung zu %2$s herstellen." + "Diese App wurde so konfiguriert, dass sie %1$s zulässt." + "Kontoanbieter %1$s ist nicht zulässig." "Homeserver-URL" - "Du kannst nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss das konfigurieren. %1$s" + "Geben Sie eine Domainadresse ein." "Wie lautet die Adresse deines Servers?" "Wähle deinen Server aus" "Konto erstellen" @@ -31,6 +34,14 @@ "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." "Willkommen zurück!" "Anmelden bei %1$s" + "Version %1$s" + "Manuell anmelden" + "Bei %1$s anmelden" + "Mit QR-Code anmelden" + "Konto erstellen" + "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" "Sichere Verbindung aufbauen" "Es konnte keine sichere Verbindung zu dem neuen Gerät hergestellt werden." "Und jetzt?" @@ -38,7 +49,7 @@ "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." + "Sie werden aufgefordert, die beiden auf diesem Gerät angezeigten Ziffern einzugeben." "Trage die unten angezeigte Zahl auf einem anderen Device ein" "Melde dich auf deinem anderen Gerät an und versuche es dann noch einmal oder verwende ein anderes Gerät, das bereits angemeldet ist." "Anderes Gerät ist nicht angemeldet" @@ -53,7 +64,7 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Gerät." "QR-Code wird nicht unterstützt" "Ihr Kontoanbieter unterstützt %1$s nicht." - "%1$swird nicht unterstützt" + "%1$s wird nicht unterstützt" "Bereit zum Scannen" "%1$s auf einem Desktop-Gerät öffnen" "Klick auf deinen Avatar" @@ -66,7 +77,7 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger "Erneut versuchen" "Falscher QR-Code" "Gehe zu den Kameraeinstellungen" - "Du musst %1$s die Erlaubnis erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren." + "Sie müssen %1$s die Erlaubnis erteilen, die Kamera Ihres Geräts zu verwenden um fortzufahren." "Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes" "QR-Code scannen" "Neu beginnen" @@ -78,6 +89,7 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger "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ürden." - "Du bist dabei, dich bei %1$s anzumelden" - "Du bist dabei, ein Konto auf %1$s zu erstellen" + "Sie sind dabei, sich bei %1$s anzumelden" + "Kontoanbieter auswählen" + "Sie sind dabei, ein Konto auf %1$s 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 532ee17023f..047cd3baa03 100644 --- a/features/login/impl/src/main/res/values-el/translations.xml +++ b/features/login/impl/src/main/res/values-el/translations.xml @@ -14,11 +14,14 @@ "Χρησιμοποίησε διαφορετικό πάροχο λογαριασμού, όπως τον δικό σου ιδιωτικό διακομιστή ή έναν εργασιακό λογαριασμό." "Αλλαγή παρόχου λογαριασμού" "Δεν μπορούσαμε να επικοινωνήσουμε με αυτόν τον οικιακό διακομιστή. Βεβαιώσου ότι έχεις εισαγάγει σωστά τη διεύθυνση URL του αρχικού διακομιστή. Εάν η διεύθυνση URL είναι σωστή, επικοινώνησε με τον διαχειριστή του κεντρικού διακομιστή για περαιτέρω βοήθεια." - "Το Sliding sync δεν είναι διαθέσιμο εξαιτίας ενός ζητήματος σε ένα πολύ γνωστό αρχείο: + "Ο διακομιστής δεν είναι διαθέσιμος λόγω προβλήματος στο αρχείο .well-known: %1$s" - "Αυτός ο διακομιστής προς το παρόν δεν υποστηρίζει Sliding sync." + "Ο επιλεγμένος πάροχος λογαριασμού δεν υποστηρίζει sliding sync. Απαιτείται αναβάθμιση στο διακομιστή για χρήση του %1$s." + "%1$s δεν επιτρέπεται να συνδεθεί με %2$s." + "Αυτή η εφαρμογή έχει ρυθμιστεί ώστε να επιτρέπει: %1$s." + "Ο πάροχος %1$s λογαριασμού δεν επιτρέπεται." "URL οικιακού διακομιστή" - "Μπορείτε να συνδεθείς μόνο σε υπάρχοντα διακομιστή που υποστηρίζει Sliding sync. Ο διαχειριστής του οικιακού διακομιστή σου θα πρέπει να το ρυθμίσει. %1$s" + "Εισήγαγε μια διεύθυνση τομέα." "Ποια είναι η διεύθυνση του διακομιστή σου;" "Επέλεξε το διακομιστή σου" "Δημιουργία λογαριασμού" @@ -31,6 +34,13 @@ "Το Matrix είναι ένα ανοιχτό δίκτυο για ασφαλή, αποκεντρωμένη επικοινωνία." "Καλωσόρισες ξανά!" "Συνδέσου στο %1$s" + "Σύνδεση χειροκίνητα" + "Συνδέσου στο %1$s" + "Συνδέσου με κωδικό QR" + "Δημιουργία λογαριασμού" + "Καλώς ήλθατε στο γρηγορότερο %1$s όλων των εποχών. Υπερτροφοδοτούμενο με ταχύτητα και απλότητα." + "Καλώς ήρθες στο %1$s. Υπερφορτισμένο, για ταχύτητα και απλότητα." + "Μείνε στο element σου" "Εγκαθίδρυση ασφαλούς σύνδεσης" "Δεν ήταν δυνατή η πραγματοποίηση ασφαλούς σύνδεσης στη νέα συσκευή. Οι υπάρχουσες συσκευές σας εξακολουθούν να είναι ασφαλείς και δεν χρειάζεται να ανησυχείς για αυτές." "Τί είναι πάλι;" @@ -79,5 +89,6 @@ "Το Matrix είναι ένα ανοιχτό δίκτυο για ασφαλή, αποκεντρωμένη επικοινωνία." "Εδώ θα ζουν οι συνομιλίες σου - όπως θα χρησιμοποιούσες έναν πάροχο email για να διατηρήσεις τα email σου." "Πρόκειται να συνδεθείς στο %1$s" + "Επέλεξε πάροχο λογαριασμού" "Πρόκειται να δημιουργήσεις έναν λογαριασμό στο %1$s" diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index 60324ae89d1..d4841e21717 100644 --- a/features/login/impl/src/main/res/values-es/translations.xml +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -1,40 +1,94 @@ - "Cambiar el proveedor de la cuenta" - "Dirección del servidor principal" - "Introduzca un término de búsqueda o una dirección de dominio." + "Cambiar proveedor de cuentas" + "Dirección del servidor base" + "Introduce un término de búsqueda o una dirección de dominio." "Busca una empresa, comunidad o servidor privado." - "Encontrar un proveedor de cuenta" + "Encontrar un proveedor de cuentas" "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos." "Estás a punto de iniciar sesión en %s" "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos." "Estás a punto de crear una cuenta en %s" "Matrix.org es un servidor grande y gratuito en la red pública Matrix para una comunicación segura y descentralizada, administrado por la Fundación Matrix.org." "Otro" - "Usa un proveedor de cuenta diferente, como tu propio servidor privado o una cuenta de trabajo." - "Cambiar el proveedor de la cuenta" - "No hemos podido acceder a este servidor. Comprueba que has introducido correctamente la dirección del servidor. Si la dirección es correcta, ponte en contacto con el administrador del servidor para obtener más ayuda." - "Sliding sync no está disponible debido a un problema en el archivo well-known: + "Usa un proveedor de cuentas diferente, como tu propio servidor privado o una cuenta de trabajo." + "Cambiar proveedor de cuentas" + "No hemos podido acceder a este servidor base. Comprueba que has introducido correctamente la dirección. Si es correcta, ponte en contacto con el administrador de tu servidor base para obtener más ayuda." + "El servidor no está disponible debido a un problema en el archivo .well-known: %1$s" - "Este servidor no soporta sliding sync." - "Dirección del homeserver" - "Solo puedes conectarte a un servidor que soporte sliding sync. El administrador de tu servidor tendrá que configurarlo. %1$s" + "El proveedor de cuentas seleccionado no admite sliding sync. Es necesario actualizar el servidor para usar %1$s." + "%1$s no está autorizado para conectarse a %2$s." + "Esta aplicación se ha configurado para permitir: %1$s." + "No se permite el proveedor de cuentas %1$s." + "URL del servidor base" + "Introduce una dirección de dominio." "¿Cuál es la dirección de tu servidor?" "Selecciona tu servidor" + "Crear cuenta" "Esta cuenta ha sido desactivada." "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 seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver." + "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!" "Iniciar sesión en %1$s" - "Inténtalo de nuevo" - "Cambiar el proveedor de la cuenta" + "Iniciar sesión manualmente" + "Iniciar sesión en %1$s" + "Iniciar sesión con un código QR" + "Crear cuenta" + "Bienvenido al %1$s más rápido de todos los tiempos. Diseñado para la velocidad y la simplicidad." + "Bienvenido a %1$s. Vitaminado, para mayor rapidez y sencillez." + "Siéntete en tu Elemento" + "Estableciendo una conexión segura" + "No se pudo establecer una conexión segura con el nuevo dispositivo. Tus dispositivos actuales siguen siendo seguros y no tienes que preocuparte por ellos." + "¿Y ahora qué?" + "Intenta iniciar sesión de nuevo con un código QR en caso de que se trate de un problema de red" + "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" + "Inicia sesión en tu otro dispositivo e inténtalo de nuevo, o usa otro dispositivo en el que ya hayas iniciado sesión." + "El otro dispositivo no tiene iniciada la sesión" + "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." + "Inicio de sesión rechazado" + "El inicio de sesión ha caducado. Inténtalo de nuevo." + "El inicio de sesión no se completó a tiempo" + "Tu otro dispositivo no admite el inicio de sesión en %s con un código QR. + +Intenta iniciar sesión manualmente o escanea el código QR con otro dispositivo." + "Código QR no admitido" + "Tu proveedor de cuentas no es compatible con %1$s." + "%1$s no admitido" + "Listo para escanear" + "Abre %1$s en un dispositivo de escritorio" + "Haz clic en tu avatar" + "Selecciona %1$s" + "«Vincular un dispositivo nuevo»" + "Escanea el código QR con este dispositivo" + "Sólo disponible si tu proveedor de cuentas lo admite." + "Abre %1$s en otro dispositivo para obtener el código QR" + "Usa el código QR que se muestra en el otro dispositivo." + "Intentar de nuevo" + "Código QR incorrecto" + "Ir a los ajustes de la cámara" + "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" + "Escanea el código QR" + "Empezar de nuevo" + "Se ha producido un error inesperado. Vuelve a intentarlo." + "A la espera de tu otro dispositivo" + "Puede que tu proveedor de cuentas te pida el siguiente código para verificar el inicio de sesión." + "Tu código de verificación" + "Cambiar proveedor de cuentas" "Un servidor privado para los empleados de Element." "Matrix es una red abierta para una comunicación segura y descentralizada." "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos." "Estás a punto de iniciar sesión en %1$s" + "Elegir proveedor de cuentas" "Estás a punto de crear una cuenta en %1$s" 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 832b0a5b383..9cdc09404c8 100644 --- a/features/login/impl/src/main/res/values-et/translations.xml +++ b/features/login/impl/src/main/res/values-et/translations.xml @@ -14,11 +14,14 @@ "Kasuta erinevat teenusepakkujat, milleks võib olla ka sinu oma server või töökoha hallatav server." "Muuda teenusepakkujat" "Me ei suutnud luuaühendust selle koduserveriga. Palun kontrolli, kas koduserveri aadress on õige. Kui aadress on õige, siis täiendavat teavet oskab sulle anda koduserveri haldaja." - "Sliding sync režiim pole saadaval vea tõttu well-known failis: + "Server pole saadaval vea tõttu well-known failis: %1$s" - "See koduserver hetkel ei toeta „Sliding sync“ režiimi" + "Valitud teenusepakkuja ei toeta „sliding sync“ režiimi. Rakenduse %1$s kasutamiseks on vaja serverit uuendada." + "%1$s ei saa kasutada %2$s koduserverit." + "See rakendus on seadistatud järgneva koduserveri kasutamiseks: %1$s." + "%1$s teenusepakkuja pole lubatud." "Koduserveri url" - "Sa saad luua ühendust vaid olemasoleva serveriga, mis toetab Sliding sync režiimi. Sinu koduserveri haldur peaks selle seadistama. %1$s" + "Sisesta domeeni aadress." "Mis on sinu koduserveri aadress?" "Vali oma server" "Loo kasutajakonto" @@ -31,6 +34,14 @@ "Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks." "Tere tulemast tagasi!" "Logi sisse serverisse %1$s" + "Versioon %1$s" + "Logi sisse käsitsi" + "Logi sisse teenusesse %1$s" + "Logi sisse QR-koodi alusel" + "Loo kasutajakonto" + "Läbi aegade kiireim ja mugavaim %1$s." + "Tere tulemast kasutama kiiret ja lihtsat suhtlusrakendust %1$s." + "Ole oma elemendis" "Loome turvalist ühendust" "Turvalise ühenduse loomine uue seadmega ei õnnestunud. Sinu olemasolevad seadmed on jätkuvalt turvatud ja sa ei pea nende pärast muretsema." "Mida järgmiseks teeme?" @@ -79,5 +90,6 @@ Proovi käsitsi sisselogimist või skaneeri QR-koodi mõne muu seadmega.""Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks." "See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat." "Sa oled sisselogimas koduserverisse %1$s" + "Vali teenusepakkuja" "Sa oled loomas kasutajakontot koduserveris %1$s" diff --git a/features/login/impl/src/main/res/values-eu/translations.xml b/features/login/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..ecf908f21ff --- /dev/null +++ b/features/login/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,71 @@ + + + "Aldatu kontu-hornitzailea" + "Zerbitzariaren helbidea" + "Sartu bilaketa-kontsulta edo domeinu-helbidea." + "Bilatu enpresa, komunitate edo zerbitzari pribatu bat." + "Bilatu kontu-hornitzaile bat" + "%s(e)n saioa hastear zaude" + "%s(e)n kontua sortzear zaude" + "Beste bat" + "Erabili beste kontu-hornitzaile bat, hala nola zure zerbitzari pribatua edo laneko kontu bat." + "Aldatu kontu-hornitzailea" + "Zerbitzariaren URLa" + "Sartu domeinu-helbide bat." + "Zein da zure zerbitzariaren helbidea?" + "Hautatu zure zerbitzaria" + "Sortu kontua" + "Kontu hau desaktibatuta dago." + "Erabiltzaile-izena edo/eta pasahitza okerrak" + "Sartu zure datuak" + "Matrix komunikazio seguru eta deszentralizaturako sare irekia da." + "Ongi etorri!" + "Hasi saioa %1$s(e)n" + "Hasi saioa eskuz" + "Hasi saioa QR kodearekin" + "Sortu kontua" + "Ongi etorri inoizko %1$s azkarrenera. Abiaduraz eta sinpletasunaz gainkargatua." + "Ongi etorri %1$s-ra. Abiaduraz eta sinpletasunez gainezka." + "Egon zure saltsan" + "Konexio segurua ezartzen" + "Ezin izan da konexio segururik ezarri gailu berriarekin. Lehendik dauden gailuak seguru daude oraindik ere eta ez duzu haietaz kezkatu beharrik." + "Orain zer?" + "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" + "Hasi saioa beste gailu batean eta saiatu berriro, edo erabili saioa hasita duen beste gailuren bat." + "Saioa hasteko eskaera bertan behera utzi da beste gailuan" + "Saioa hasteko eskaera bertan behera utzi da" + "Saioa hasteari uko egin zaio beste dispositiboan." + "Saio-hasiera ukatu da" + "Saio-hasiera iraungi da. Saiatu berriro." + "Saio-hasiera ez da garaiz gauzatu." + "Beste gailua ez da bateragarria QR kodeak erabiliz %s(e)n saioa hastearekin. + +Saiatu saioa eskuz hasten, edo eskaneatu QR kodea beste gailu batean." + "QR kodea ez da bateragarria" + "Zure kontu-hornitzailea ez da %1$s-ekin bateragarria." + "%1$s ez da bateragarria" + "Eskaneatzeko prest" + "Klikatu zure abatarrean" + "Hautatu %1$s" + "\"Lotu gailu berria\"" + "Eskaneatu QR kodea gailu honekin" + "Erabili beste gailuan agertzen den QR kodea." + "Saiatu berriro" + "QR kode okerra" + "Joan kameraren ezarpenetara" + "Eskaneatu QR kodea" + "Hasi berriro" + "Ustekabeko errore bat gertatu da. Saiatu berriro." + "Beste gailuaren zain" + "Zure kontu-hornitzaileak hurrengo kodea eska diezazuke saio-hasiera egiaztatzeko." + "Egiaztapen-kodea" + "Aldatu kontu-hornitzailea" + "Elementeko langileentzako zerbitzari pribatua." + "Matrix komunikazio seguru eta deszentralizaturako sare irekia da." + "%1$s(e)n saioa hastear zaude" + "%1$s(e)n kontua sortzear zaude" + 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 29ec79b9fd3..625b03db4bb 100644 --- a/features/login/impl/src/main/res/values-fa/translations.xml +++ b/features/login/impl/src/main/res/values-fa/translations.xml @@ -13,19 +13,26 @@ "دیگر" "استفاده از فراهم کنندهٔ حسابی دیگر چون کارساز خصوصی خوتان یا حسابی کاری." "تغییر فراهم کنندهٔ حساب" - "این کارساز در حال حاضر از هم‌گام سازی اسلایدی پشتیبانی نمی‌کند." + "ما نتوانستیم به این سرور خانگی برسیم. لطفاً بررسی کنید که URL سرور اصلی را به درستی وارد کرده اید. اگر URL صحیح است، برای کمک بیشتر با مدیر سرور خانگی خود تماس بگیرید." "نشانی کارساز خانگی" - "تنها می‌توانید به کارسازهای موجودی که از هم‌گام سازی اسلاید پشتیبانی می‌کنند وصل شود. مدیر کارساز خانگیتان باید پیکربندیش کند. %1$s" + "ورود نشانی دامنه." "نشانی کارسازتان چیست؟" "کارسازتان را برگزینید" "ایجاد حساب" "این حساب از کار افتاده است." "نام کاربری یا گذرواژه نامعتبر است" "این یک شناسه کاربری معتبر نیست. قالب صحیح: ‪«@user:homeserver.or" + "سرور اصلی انتخاب شده از رمز عبور یا ورود OIDC پشتیبانی نمی کند. لطفا با مدیر خود تماس بگیرید یا یک سرور خانگی دیگر را انتخاب کنید." "جزییاتتان را وارد کنید" "ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است." "خوش برگشتید!" "ورود به %1$s" + "ورود دستی" + "ورود با کد QR" + "ایجاد حساب" + "به سریع‌ترین %1$s خوش آمدید. بازطرّاحی شده برای سرعت و سادگی." + "به %1$s خوش آمدید. بازطرّاحی شده برای سرعت و سادگی." + "در المنتتان باشید" "برقرار کدن اتّصالی امن" "نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزاره‌های موجودتان هنوز امنند و نیازی نیست نگرانشان باشید." "اکنون چه؟" @@ -67,6 +74,7 @@ "ممکن است فراهم کنندهٔ حسابتان کد زیر را برای تأیید ورود بخواهد." "کد تأییدتان" "تغییر فراهم کنندهٔ حساب" + "یک سرور خصوصی برای کارمندان Element." "ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است." "جایی که گفت‌وگوهایتان خواهند زیست — درست مثل استفاده‌تان از فراهم کنندهٔ رایانامه‌ای برای نگه داشتن رایانامه‌هایتان." "دارید به %1$s وارد می‌شوید" 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 37457a99588..a05dc55216a 100644 --- a/features/login/impl/src/main/res/values-fi/translations.xml +++ b/features/login/impl/src/main/res/values-fi/translations.xml @@ -14,11 +14,14 @@ "Käytä toista palveluntarjoajaa, kuten omaa yksityistä palvelintasi tai työpaikkaasi." "Vaihda palveluntarjoajaa" "Kotipalvelimeen ei saatu yhteyttä. Varmista, että olet syöttänyt osoitteen oikein. Jos osoite on oikein, ota yhteyttä palvelimesi ylläpitäjään." - "Sliding sync ei ole saatavilla well-known tiedostossa olevan ongelman vuoksi: + "Palvelin ei ole saatavilla .well-known tiedostossa olevan ongelman vuoksi: %1$s" - "Tämä palvelin ei tällä hetkellä tue sliding syncia." + "Valitsemasi palveluntarjoaja ei tue sliding syncia. Palvelimen päivitys tarvitaan %1$s -sovelluksen käyttämiseen." + "%1$s ei saa yhdistää %2$s -palvelimeen." + "Tämä sovellus on määritetty sallimaan: %1$s." + "Palveluntarjoaja %1$s ei ole sallittu." "Kotipalvelimen osoite" - "Voit yhdistää vain olemassa olevaan palvelimeen, joka tukee sliding syncia. Kotipalvelimesi ylläpitäjän on otettava se käyttöön. %1$s" + "Anna verkkotunnuksen osoite." "Mikä on palvelimesi osoite?" "Valitse palvelimesi" "Luo tili" @@ -31,6 +34,13 @@ "Matrix on avoin verkko turvallista, hajautettua viestintää varten." "Tervetuloa takaisin!" "Kirjaudu sisään %1$s -palvelimelle" + "Kirjaudu sisään manuaalisesti" + "Kirjaudu sisään %1$s -palvelimelle" + "Kirjaudu sisään QR-koodilla" + "Luo tili" + "Tervetuloa kaikkien aikojen nopeimpaan %1$s -sovellukseen. Ahdettu nopeudella ja yksinkertaisuudella." + "Tervetuloa %1$s -sovellukseen. Ahdettu nopeudella ja yksinkertaisuudella." + "Ole elementissäsi" "Muodostetaan turvallista yhteyttä" "Turvallista yhteyttä uuteen laitteeseen ei voitu muodostaa. Olemassa olevat laitteesi ovat edelleen turvassa, eikä sinun tarvitse huolehtia niistä." "Mitä nyt?" @@ -79,5 +89,6 @@ Yritä kirjautua sisään manuaalisesti tai skannaa QR-koodi toisella laitteella "Matrix on avoin verkko turvallista, hajautettua viestintää varten." "Keskustelusi asuvat täällä — aivan kuten aivan kuten käyttäisit sähköpostipalveluntarjoajaa sähköpostiesi säilyttämiseen." "Olet kirjautumassa sisään %1$s-palvelimelle" + "Valitse palveluntarjoaja" "Olet luomassa tiliä %1$s-palvelimelle" 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 9bc76ef9d71..62d5ebc6af4 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -14,11 +14,14 @@ "Utilisez un autre fournisseur de compte, tel que votre propre serveur privé ou un serveur professionnel." "Changer de fournisseur de compte" "Nous n’avons pas pu atteindre ce serveur d’accueil. Vérifiez que vous avez correctement saisi l’URL du serveur d’accueil. Si l’URL est correcte, contactez l’administrateur de votre serveur d’accueil pour obtenir de l’aide." - "Sliding sync n’est pas disponible en raison d’un problème dans le well-known file : + "Ce fournisseur de compte n’est pas disponible en raison d’un problème dans le fichier .well-known: %1$s" - "Ce serveur ne prend actuellement pas en charge la synchronisation glissante." + "Le fournisseur de compte sélectionné ne prend pas en charge le sliding sync. Une mise à jour du serveur est nécessaire pour pouvoir utiliser %1$s." + "%1$s n’est pas autorisé à se connecter à %2$s." + "Cette application a été configurée pour autoriser: %1$s." + "Le fournisseur de compte %1$s n’est pas autorisé." "URL du serveur d’accueil" - "Vous ne pouvez vous connecter qu’à un serveur existant qui prend en charge le sliding sync. L’administrateur de votre serveur d’accueil devra le configurer. %1$s" + "Saisissez une adresse de domaine." "Quelle est l’adresse de votre serveur ?" "Choisissez votre serveur" "Créer un compte" @@ -31,6 +34,14 @@ "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." "Content de vous revoir !" "Connectez-vous à %1$s" + "Version %1$s" + "Se connecter manuellement" + "Se connecter à %1$s" + "Se connecter avec un QR code" + "Créer un compte" + "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" "Établissement d’une connexion sécurisée" "Aucune connexion sécurisée n’a pu être établie avec la nouvelle session. Vos sessions existantes sont toujours en sécurité et vous n’avez pas à vous en soucier." "Et maintenant ?" @@ -77,5 +88,6 @@ "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails." "Vous êtes sur le point de vous connecter à %1$s" + "Choisissez un fournisseur de compte" "Vous êtes sur le point de créer un compte sur %1$s" 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 d278d3a15ae..f72090d4c5d 100644 --- a/features/login/impl/src/main/res/values-hu/translations.xml +++ b/features/login/impl/src/main/res/values-hu/translations.xml @@ -14,11 +14,14 @@ "Másik fiókszolgáltató, például a saját privát kiszolgáló vagy egy munkahelyi fiók használata." "Fiókszolgáltató módosítása" "Nem sikerült elérni ezt a Matrix-kiszolgálót. Ellenőrizze, hogy helyesen adta-e meg a Matrix-kiszolgáló webcímét. Ha a webcím helyes, akkor további segítségért lépjen kapcsolatba a Matrix-kiszolgáló adminisztrátorával." - "A Sliding sync protokoll a well-known fájl problémája miatt nem érhető el: + "A kiszolgáló a well-known fájl problémája miatt nem érhető el: %1$s" - "A kiszolgáló jelenleg nem támogatja a Sliding sync protokollt." + "A kiválasztott fiókszolgáltató nem támogatja a csúszóablakos szinkronizálást. Az %1$s használatához kiszolgálófrissítés szükséges." + "%1$s nem csatlakozhat ide: %2$s." + "Ezt az alkalmazást úgy konfigurálták, hogy engedélyezi ezt: %1$s." + "A(z) %1$s fiókszolgáltató nem engedélyezett." "Matrix-kiszolgáló webcíme" - "Csak olyan meglévő kiszolgálóhoz csatlakozhat, amely támogatja a Sliding sync protokollt. Ezt a Matrix-kiszolgáló adminisztrátorának kell beállítania. %1$s" + "Adjon meg egy domaincímet." "Mi a kiszolgálója címe?" "Válassza ki a kiszolgálóját" "Fiók létrehozása" @@ -31,6 +34,13 @@ "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" + "Kézi bejelentkezés" + "Bejelentkezés ide: %1$s" + "Bejelentkezés QR-kóddal" + "Fiók létrehozása" + "Ü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" "Biztonságos kapcsolat létesítése" "Nem sikerült biztonságos kapcsolatot létesíteni az új eszközzel. A meglévő eszközei továbbra is biztonságban vannak, és nem kell aggódnia miattuk." "Most mi lesz?" @@ -79,5 +89,6 @@ Próbáljon meg kézileg bejelentkezni, vagy olvassa be a QR-kódot egy másik e "A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz." "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez." "Hamarosan bejelentkezik ebbe: %1$s" + "Válassza ki a fiókszolgáltatót" "Hamarosan létrehoz egy fiókot ezen: %1$s" 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 0ddc32c2129..63d703febd2 100644 --- a/features/login/impl/src/main/res/values-in/translations.xml +++ b/features/login/impl/src/main/res/values-in/translations.xml @@ -14,11 +14,14 @@ "Gunakan penyedia akun yang berbeda, seperti server pribadi Anda sendiri atau akun kerja." "Ubah penyedia akun" "Kami tidak dapat menjangkau server ini. Periksa apakah Anda telah memasukkan URL homeserver dengan benar. Jika URL sudah benar, hubungi administrator homeserver Anda untuk bantuan lebih lanjut." - "Sliding sync tidak tersedia karena adanya masalah dalam berkas .well-known: + "Server tidak tersedia karena ada masalah dalam berkas .well-known: %1$s" - "Server ini saat ini tidak mendukung sinkronisasi geser." + "Penyedia akun yang dipilih tidak mendukung sinkronisasi geser. Peningkatan server diperlukan untuk digunakan %1$s." + "%1$s tidak diizinkan untuk terhubung ke %2$s." + "Aplikasi ini telah diatur untuk mengizinkan: %1$s." + "Penyedia akun %1$s tidak diizinkan." "URL Homeserver" - "Anda hanya dapat terhubung ke server yang ada yang mendukung sinkronisasi geser. Admin homeserver Anda perlu mengaturnya. %1$s" + "Masukkan alamat domain." "Apa alamat server Anda?" "Pilih server Anda" "Buat akun" @@ -31,6 +34,14 @@ "Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi." "Selamat datang kembali!" "Masuk ke %1$s" + "Versi %1$s" + "Masuk secara manual" + "Masuk ke %1$s" + "Masuk dengan kode QR" + "Buat akun" + "Selamat datang di %1$s tercepat yang pernah ada. Berdaya besar untuk kecepatan dan kesederhanaan." + "Selamat datang di %1$s. Berdaya penuh, untuk kecepatan dan kesederhanaan." + "Berada di elemen Anda" "Membuat koneksi aman" "Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka." "Apa sekarang?" @@ -79,5 +90,6 @@ Coba masuk secara manual, atau pindai kode QR dengan perangkat lain." "Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi." "Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda." "Anda akan masuk ke %1$s" + "Pilih penyedia akun" "Anda akan membuat akun di %1$s" 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 5b82829b80b..b88207f9854 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -14,11 +14,14 @@ "Utilizza un provider di account diverso, ad esempio il tuo server privato o un account di lavoro." "Cambia fornitore dell\'account" "Non siamo riusciti a raggiungere questo homeserver. Verifica di aver inserito correttamente l\'URL. Se l\'URL è corretto, contatta l\'amministratore del homeserver per ulteriore assistenza." - "La sliding sync non è disponibile per un problema nel file well-known: + "Il server non è disponibile per un problema nel file well-known: %1$s" - "Questo server attualmente non supporta la sliding sync." + "Il fornitore di account selezionato non supporta la Sliding sync. Per utilizzare %1$s è necessario un aggiornamento del server." + "%1$s non è autorizzato a connettersi a %2$s." + "Questa app è stata configurata per consentire: %1$s." + "Fornitore dell\'account %1$s non consentito." "URL dell\'homeserver" - "Puoi connetterti solo a un server esistente che supporta la sliding sync. L\'amministratore del tuo homeserver dovrà configurarla. %1$s" + "Inserisci un indirizzo di dominio." "Qual è l\'indirizzo del tuo server?" "Seleziona il tuo server" "Crea account" @@ -31,6 +34,14 @@ "Matrix è una rete aperta per comunicazioni sicure e decentralizzate." "Bentornato!" "Accedi a %1$s" + "Versione %1$s" + "Accedi manualmente" + "Accedi a %1$s" + "Accedi con codice QR" + "Crea account" + "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" "Stabilendo la connessione" "Non è stato possibile stabilire una connessione sicura con il nuovo dispositivo. I tuoi dispositivi esistenti sono ancora al sicuro e non devi preoccuparti di loro." "E adesso?" @@ -52,7 +63,7 @@ Prova ad accedere manualmente o scansiona il codice QR con un altro dispositivo." "Codice QR non supportato" - "Il tuo fornitore di account non supporta %1$s ." + "Il tuo fornitore di account non supporta %1$s." "%1$s non supportato" "Pronto per la scansione" "Apri %1$s su un dispositivo desktop" @@ -79,5 +90,6 @@ Prova ad accedere manualmente o scansiona il codice QR con un altro dispositivo. "Matrix è una rete aperta per comunicazioni sicure e decentralizzate." "Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email." "Stai per accedere a %1$s" + "Scegli il fornitore dell\'account" "Stai per creare un account su %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 6c0ce2adab2..f3e46faf199 100644 --- a/features/login/impl/src/main/res/values-ka/translations.xml +++ b/features/login/impl/src/main/res/values-ka/translations.xml @@ -14,19 +14,26 @@ "გამოიყენეთ სხვა ანგარიშის პროვაიდერი, როგორიცაა თქვენი პირადი სერვერი ან სამუშაო ანგარიში." "შეცვალეთ ანგარიშის მომწოდებელი" "ჩვენ ვერ მივაღწიეთ ამ სახლის სერვერს. გთხოვთ, შეამოწმოთ, რომ სწორად შეიყვანეთ სახლის სერვერის URL. თუ URL სწორია, დაუკავშირდით თქვენი სახლის სერვერის ადმინისტრატორს დამატებითი დახმარებისთვის." - "ამჟამად ეს სერვერი მხარს არ უჭერს \"sliding sync\"-ს." + "Sliding sync არ არის ხელმისაწვდომი well-known ფაილში პრობლემის გამო: %1$s" "სახლის სერვერის URL" - "თქვენ შეგიძლიათ დაუკავშირდეთ მხოლოდ იმ სერვერს, რომელიც მხარს უჭერს \"sliding sync\"-ს. თქვენი სახლის სერვერის ადმინისტრატორს დასჭირდება მისი კონფიგურაცია.%1$s" "რა არის თქვენი სერვერის მისამართი?" "აირჩიეთ თქვენი სერვერი" + "ანგარიშის შექმნა" "ეს ანგარიში დეაქტივირებულია." "არასწორი მომხმარებლის სახელი და/ან პაროლი" "მოცემული მომხმარებლის იდენტიფიკატორი არასწორია. დასაშვები ფორმატი: ‘@user:homeserver.org’" + "ეს სერვერი კონფიგურირებულია განახლების გასაღებების გამოსაყენებლად. პაროლზე დაფუძნებული შეცვლისას ისინი მხარდაჭერილი არაა." "მოცემული სახლის სერვერი მხარს არ უჭერს პაროლით ან OIDC-ით შესვლას. გთხოვთ, დაუკავშირდეთ თქვენს ადმინისტრატორს ან აარჩიეთ სხვა სახლის სერვერი." "შეიყვანეთ თქვენი დეტალები" "Matrix არის ღია ქსელი უსაფრთხო, დეცენტრალიზებული კომუნიკაციისთვის." "კეთილი იყოს თქვენი მობრძანება!" "შესვლა %1$s-ში" + "ხელით შესვლა" + "შესვლა QR კოდით" + "ანგარიშის შექმნა" + "კეთილი იყოს თქვენი მობრძანება უსწრაფეს %1$s-ში. დამუხტულია სიჩქარისა და სიმარტივისათვის." + "კეთილი იყოს თქვენი მობრძანება %1$s-ში! დამუხტული სიჩქარისა და სიმარტივისთვის." + "იყავი შენს element-ში" "ხელახლა ცდა" "შეცვალეთ ანგარიშის მომწოდებელი" "კერძო სერვერი Element-ის თანამშრომლებისთვის." diff --git a/features/login/impl/src/main/res/values-lt/translations.xml b/features/login/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..bd53419ce82 --- /dev/null +++ b/features/login/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,36 @@ + + + "Keisti paskyros teikėją" + "Ieškokite bendrovės, bendruomenės arba privataus serverio." + "Rasti paskyros teikėją" + "Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus." + "Ketinate prisijungti prie %s" + "Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus." + "Ketinate sukurti paskyrą teikėjoje %s" + "Kita" + "Naudokite skirtingą paskyros teikėją, pavyzdžiui, savo privatų serverį arba darbo paskyrą." + "Keisti paskyros teikėją" + "Nepavyko pasiekti šio serverio. Patikrinkite, ar teisingai įvedėte serverio URL. Jei URL yra teisingas, susisiekite su serverio administracija dėl tolimesnės pagalbos." + "Serverio URL" + "Koks yra Jūsų serverio adresas?" + "Sukurti paskyrą" + "Ši paskyra buvo išjungta." + "Neteisingas vartotojo vardas ir (arba) slaptažodis" + "Tai nėra tinkamas vartotojo vardas. Reikalingas formatas: \'@vartotojas:serveris.org\'" + "Pasirinktas serveris nepalaiko slaptažodžio ar OIDC prisijungimo. Susisiekite su serverio administracija arba pasirinkite kitą serverį." + "Įveskite savo duomenis" + "Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui." + "Sveiki sugrįžę!" + "Prisijungti prie %1$s" + "Prisijunkite rankiniu būdu" + "Prisijunkite naudodami QR kodą" + "Sukurti paskyrą" + "Sveiki atvykę į %1$s. Įkrautas greitumui ir paprastumui." + "Būkite savo elemente" + "Keisti paskyros teikėją" + "Privatus serveris “Element” darbuotojams." + "Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui." + "Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus." + "Jūs ruošiatės prisijungti prie %1$s" + "Jūs ruošiatės susikurti paskyrą %1$s" + 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 c901908fa59..0054906e653 100644 --- a/features/login/impl/src/main/res/values-nb/translations.xml +++ b/features/login/impl/src/main/res/values-nb/translations.xml @@ -1,26 +1,94 @@ + "Bytt kontotilbyder" "Hjemmeserveradresse" + "Skriv inn et søkeord eller en domeneadresse." "Søk etter et selskap, fellesskap eller privat server." "Finn en kontoleverandør" "Det er her samtalene dine vil ligge - akkurat som du ville brukt en e-postleverandør til å oppbevare e-postene dine." "Du er i ferd med å logge inn på %s" "Det er her samtalene dine vil ligge - akkurat som du ville brukt en e-postleverandør til å oppbevare e-postene dine." "Du er i ferd med å opprette en konto på %s" + "Matrix.org er en stor, gratis server på det offentlige Matrix-nettverket for sikker, desentralisert kommunikasjon, drevet av Matrix.org Foundation." + "Annet" + "Bruk en annen kontotilbyder, for eksempel din egen private server eller en arbeidskonto." + "Bytt kontotilbyder" "Vi kunne ikke nå denne hjemmeserveren. Kontroller at du har skrevet inn hjemmeserverens URL riktig. Hvis URL-en er riktig, kontakt administratoren for hjemmeserveren din for å få mer hjelp." - "Denne serveren støtter for øyeblikket ikke sliding sync." + "Serveren er ikke tilgjengelig på grunn av et problem i den velkjente filen: +%1$s" + "Den valgte kontoleverandøren støtter ikke sliding sync. En oppgradering av serveren er nødvendig for å bruke %1$s." + "%1$s har ikke lov til å koble seg til %2$s." + "Denne appen er konfigurert til å tillate: %1$s." + "Kontoleverandør %1$s er ikke tillatt." "URL til hjemmeserver" + "Skriv inn en domeneadresse." "Hva er adressen til serveren din?" + "Velg din server" + "Opprett konto" "Denne kontoen er deaktivert." "Feil brukernavn og/eller passord" "Dette er ikke en gyldig brukeridentifikator. Forventet format: \'@bruker:homeserver.org\'" + "Denne serveren er konfigurert til å bruke oppdateringstokener. Disse støttes ikke når du bruker passordbasert pålogging." "Den valgte hjemmeserveren støtter ikke passord eller OIDC-pålogging. Ta kontakt med administratoren din eller velg en annen hjemmeserver." "Skriv inn opplysningene dine" "Matrix er et åpent nettverk for sikker, desentralisert kommunikasjon." "Velkommen tilbake!" + "Logg inn på %1$s" + "Logg på manuelt" + "Logg inn på %1$s" + "Logg inn med QR-kode" + "Opprett konto" + "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" + "Etablere en sikker forbindelse" + "En sikker tilkobling kunne ikke opprettes til den nye enheten. Dine eksisterende enheter er fortsatt trygge, og du trenger ikke å bekymre deg for dem." + "Hva nå?" + "Prøv å logge på igjen med en QR-kode i tilfelle dette var et nettverksproblem" + "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" + "Logg på den andre enheten din og prøv igjen, eller bruk en annen enhet som allerede er pålogget." + "Annen enhet er ikke pålogget" + "Påloggingen ble kansellert på den andre enheten." + "Påloggingsforespørsel kansellert" + "Påloggingen ble avvist på den andre enheten." + "Pålogging avslått" + "Påloggingen er utløpt. Vennligst prøv igjen." + "Påloggingen ble ikke fullført i tide" + "Den andre enheten din støtter ikke pålogging på %s med en QR-kode. + +Prøv å logge på manuelt, eller skann QR-koden med en annen enhet." + "QR-kode støttes ikke" + "Kontotilbyderen din støtter ikke %1$s." + "%1$s støttes ikke" + "Klar til å skanne" + "Åpne %1$s på en datamaskin" + "Klikk på avataren din" + "Velg %1$s" + "«Koble til ny enhet»" + "Skann QR-koden med denne enheten" + "Bare tilgjengelig hvis kontotilbyderen din støtter det." + "Åpne %1$s på en annen enhet for å få QR-koden" + "Bruk QR-koden som vises på den andre enheten." + "Prøv igjen" + "Feil QR-kode" + "Gå til kamerainnstillinger" + "Du må gi tillatelse til at %1$s kan bruke enhetens kamera for å fortsette." + "Tillat kameratilgang for å skanne QR-koden" + "Skann QR-koden" + "Begynn på nytt" + "Det oppstod en uventet feil. Prøv igjen." + "Venter på den andre enheten din" + "Kontotilbyderen din kan be om følgende kode for å bekrefte påloggingen." + "Bekreftelseskoden din" + "Bytt kontotilbyder" "En privat server for Element-ansatte." "Matrix er et åpent nettverk for sikker, desentralisert kommunikasjon." "Det er her samtalene dine vil ligge - akkurat som du ville brukt en e-postleverandør til å oppbevare e-postene dine." "Du er i ferd med å logge inn på %1$s" + "Velg kontoleverandør" "Du er i ferd med å opprette en konto på %1$s" 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 4097408d555..1c8515df220 100644 --- a/features/login/impl/src/main/res/values-nl/translations.xml +++ b/features/login/impl/src/main/res/values-nl/translations.xml @@ -16,9 +16,7 @@ "We konden deze homeserver niet bereiken. Controleer of je de homeserver-URL juist hebt ingevoerd. Als de URL juist is, neem dan contact op met de beheerder van je homeserver voor verdere hulp." "Sliding sync is niet beschikbaar vanwege een probleem in het well-known bestand: %1$s" - "Deze server ondersteunt op dit moment geen sliding sync." "Homeserver-URL" - "Je kunt alleen verbinding maken met een bestaande server die sliding sync ondersteunt. De beheerder van de homeserver moet dit configureren. %1$s" "Wat is het adres van je server?" "Selecteer je server" "Account aanmaken" @@ -31,6 +29,12 @@ "Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie." "Welkom terug!" "Inloggen bij %1$s" + "Handmatig inloggen" + "Inloggen met QR-code" + "Account aanmaken" + "Welkom bij de snelste %1$s ooit. Supercharged, voor snelheid en eenvoud." + "Welkom bij %1$s. Supercharged, voor snelheid en eenvoud." + "Wees in je element" "Een beveiligde verbinding tot stand brengen" "Er kon geen beveiligde verbinding worden gemaakt met het nieuwe apparaat. Je bestaande apparaten zijn nog steeds veilig en je hoeft je daarover geen zorgen te maken." "Wat nu?" 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 fc0ad2789ca..64a6e57db2b 100644 --- a/features/login/impl/src/main/res/values-pl/translations.xml +++ b/features/login/impl/src/main/res/values-pl/translations.xml @@ -14,11 +14,12 @@ "Użyj innego dostawcy konta, takiego jak własny serwer lub konta służbowego." "Zmień dostawcę konta" "Nie mogliśmy połączyć się z tym serwerem domowym. Sprawdź, czy adres URL serwera został wprowadzony poprawnie. Jeśli adres URL jest poprawny, skontaktuj się z administratorem serwera w celu uzyskania dalszej pomocy." - "Sliding sync nie jest dostępny z powodu problemu w znanym pliku: + "Serwer nie jest dostępny z powodu problemu pliku .well-known: %1$s" - "Ten serwer obecnie nie obsługuje technologii Sliding Sync." + "Wybrany dostawca konta nie wspiera synchronizacji przesuwnej. Wymagana jest aktualizacja serwera %1$s." + "%1$s nie posiada zezwolenia na dołączenie do %2$s." "URL serwera domowego" - "Możesz połączyć się tylko z serwerem, który obsługuje technologię Sliding Sync. Administrator serwera domowego będzie musiał ją skonfigurować. %1$s" + "Wprowadź adres domeny." "Jaki jest adres Twojego serwera?" "Wybierz swój serwer" "Utwórz konto" @@ -31,6 +32,13 @@ "Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji." "Witaj ponownie!" "Zaloguj się do %1$s" + "Zaloguj się ręcznie" + "Zaloguj się do %1$s" + "Zaloguj się za pomocą kodu QR" + "Utwórz konto" + "Witamy w %1$s. Szybszy i prostszy niż kiedykolwiek." + "Witamy w %1$s. Doładowany, dla szybkości i prostoty." + "Be in your element" "Nawiązanie bezpiecznego połączenia" "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?" 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 5ceebd308e0..0a6b788bd69 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 @@ -14,26 +14,82 @@ "Use um provedor de conta diferente, como seu próprio servidor privado ou uma conta corporativa." "Alterar provedor da conta" "Não conseguimos acessar esse servidor. Verifique se você inseriu a URL do servidor corretamente. Se a URL estiver correta, entre em contato com o administrador do servidor para obter mais ajuda." - "Este servidor atualmente não oferece suporte à tecnologia sliding sync." + "O servidor não está disponível devido à um problema no arquivo .well-known: +%1$s" + "O provedor de conta selecionado não é compatível com a sliding sync. É necessária uma atualização do servidor para que você possa usar o %1$s." + "O %1$s não tem permissão para se conectar a %2$s." + "Este app foi configurado para permitir: %1$s." + "O provedor de conta %1$s não é permitido." "URL do servidor" - "Você só pode se conectar a um servidor existente que ofereça suporte à tecnologia sliding sync. O administrador do seu servidor precisará configurá-lo. %1$s" + "Insira um endereço de domínio." "Qual é o endereço do seu servidor?" "Selecione seu servidor" + "Criar conta" "Essa conta foi desativada." "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 de atualização. Eles não são suportados ao usar login baseado em senha." "O servidor selecionado não suporta senha ou login no OIDC. Entre em contato com o administrador ou escolha outro servidor." "Insira seus dados" "A Matrix é uma rede aberta para comunicação segura e descentralizada." "Bem-vindo de volta!" "Iniciar sessão em %1$s" + "Versão %1$s" + "Iniciar sessão manualmente" + "Entrar em %1$s" + "Iniciar sessão com código QR" + "Criar conta" + "Bem-vindo ao mais rápido %1$s de todos os tempos. Turbinado para velocidade e simplicidade." + "Bem-vindo ao %1$s. Turbinado, para velocidade e simplicidade" + "Esteja no seu elemento" + "Estabelecendo uma conexão segura" + "Não foi possível estabelecer uma conexão segura com o novo dispositivo. Seus dispositivos existentes ainda estão seguros e você não precisa se preocupar com eles." + "E agora?" + "Tente iniciar sessão novamente com um código QR caso este tenha sido um problema de rede" + "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, faça login manualmente" + "Conexão não segura" + "Você será solicitado a inserir os dois dígitos mostrados neste dispositivo." + "Digite o número abaixo em seu outro dispositivo" + "Faça login em seu outro dispositivo e tente novamente, ou use outro dispositivo que já esteja conectado." + "Outro dispositivo não conectado" + "O login foi cancelado no outro dispositivo." + "Solicitação de login cancelada" + "O login foi recusado no outro dispositivo." + "Login recusado" + "O login expirou. Tente novamente." + "O login não foi concluído a tempo" + "Seu outro dispositivo não é compatível com o login em %s com um código QR. + +Tente fazer login manualmente ou escanear o código QR com outro dispositivo." + "Código QR incompatível" + "Seu provedor de conta não é compatível com %1$s." + "%1$s incompatível" + "Pronto para escanear" + "Abrir %1$s em um dispositivo desktop" + "Clique no seu avatar" + "Selecione %1$s" + "\"Vincular novo dispositivo\"" + "Leia o código QR com este dispositivo" + "Disponível somente se o provedor da sua conta for compatível." + "Abra %1$s em outro dispositivo para obter o código QR" + "Use o código QR exibido no outro dispositivo." "Tente novamente" + "Código QR errado" + "Ir para as configurações da câmera" "Você deve permitir ao %1$s usar a câmera do seu dispositivo para continuar." + "Permita o acesso à câmera para escanear o código QR" + "Leia o código QR" + "Comece de novo" + "Ocorreu um erro inesperado. Tente novamente." + "Aguardando seu outro dispositivo" + "Seu provedor de conta pode solicitar o seguinte código para verificar o login." "Seu código de verificação" "Alterar provedor da conta" "Um servidor privado para funcionários do Element." "A Matrix é uma rede aberta para comunicação segura e descentralizada." "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails." "Você está prestes a fazer login em %1$s" + "Escolher um provedor de conta" "Você está prestes a criar uma conta em %1$s" 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 d5489c90f7c..1b60edf1464 100644 --- a/features/login/impl/src/main/res/values-pt/translations.xml +++ b/features/login/impl/src/main/res/values-pt/translations.xml @@ -14,11 +14,14 @@ "Utiliza um operador de conta diferente, como o teu próprio servidor privado ou uma conta de trabalho." "Alterar operador de conta" "Não foi possível comunicar com este servidor. Por favor, verifica se introduziste o seu URL corretamente. Se sim, contacta o administrador para obteres mais ajuda." - "A sincronização deslizante (sliding sync) não está disponível devido a um problema no ficheiro \"well-known\": + "O servidor não está disponível devido a um problema no ficheiro \".well-known\": %1$s" - "Este servidor não suporta sincronização deslizante (sliding sync)." + "O operador de conta selecionado não suporta sincronização deslizante (sliding sync). É necessária uma atualização do servidor para poder usar a %1$s." + "%1$s não se pode ligar a %2$s." + "Esta aplicação foi configurada para permitir: %1$s." + "Operador de conta %1$s não permitido." "URL do servidor" - "Só te podes ligar a um servidor existente que suporte a sincronização deslizante (sliding sync). O administrador do teu servidor terá de a configurar. %1$s" + "Insere um endereço" "Qual é o endereço do teu servidor?" "Seleciona o teu servidor" "Criar conta" @@ -31,6 +34,14 @@ "A Matrix é uma rede aberta de comunicação descentralizada e segura." "Bem-vindo(a) de volta!" "Iniciar sessão em %1$s" + "Versão %1$s" + "Iniciar sessão manualmente" + "Iniciar sessão em %1$s" + "Iniciar sessão com código QR" + "Criar conta" + "Bem-vindo(a) à %1$s mais rápida de sempre. Super rápida e simples." + "Bem-vindo(a) à %1$s. Revitalizado, rápido e simples." + "A liberdade do teu elemento" "A estabelecer uma ligação segura" "Não foi possível estabelecer uma ligação segura com o novo dispositivo. Os teus outros dispositivos continuam seguros, não precisas de te preocupar com eles." "E agora?" @@ -44,8 +55,8 @@ "O outro dispositivo não tem a sessão iniciada" "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 recusado no outro dispositivo." - "Início de sessão cancelado" + "O início de sessão foi rejeitado no outro dispositivo." + "Início de sessão rejeitado" "O início de sessão expirou. Por favor, tenta novamente." "O início de sessão não foi concluído a tempo" "O teu outro dispositivo não suporta o início de sessão na %s com um código QR. @@ -79,5 +90,6 @@ Tenta iniciar a sessão manualmente ou digitaliza o código QR com outro disposi "A Matrix é uma rede aberta de comunicação descentralizada e segura." "É aqui que as tuas conversas vão ficar — tal como num serviço de e-mail." "Irás iniciar sessão em %1$s" + "Escolher operador de conta" "Irás criar uma conta em %1$s" 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 01e0c1a8210..c1355a9491d 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -16,9 +16,7 @@ "Nu am putut accesa acest homeserver. Te rugăm să verifici că ai introdus corect adresa URL a homeserver-ului. Dacă adresa URL este corectă, contactează administratorul homeserver-ului pentru ajutor suplimentar." "Sliding sync nu este disponibil din cauza unei probleme în fișierul well-known: %1$s" - "Momentan acest server nu oferă suport pentru sliding sync." "Adresa URL a homeserver-ului" - "Vă putețo conecta numai la un server existent care oferă suport pentru sliding sync. Administratorul homeserver-ului dumneavoastră va trebui să îl configureze. %1$s" "Care este adresa serverului dumneavoastră?" "Selectați serverul dumneavoastra" "Creați un cont" @@ -31,6 +29,12 @@ "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." "Bine ați revenit!" "Conectați-vă la %1$s" + "Conectați-vă manual" + "Conectați-vă cu un cod QR" + "Creați un cont" + "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" "Se stabilește o conexiune securizată" "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?" 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 40a958e7eff..17aaaa4530a 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -14,11 +14,14 @@ "Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись." "Сменить поставщика учетной записи" "Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью." - "Sliding sync недоступен из-за проблемы в известном файле: + "Сервер недоступен из-за проблемы в файле .well-known: %1$s" - "К сожалению данный сервер не поддерживает sliding sync." + "Выбранный провайдер аккаунтов не поддерживает Sliding sync. Для использования %1$s необходимо обновление сервера." + "%1$s отказано в подключении к %2$s." + "Это приложение настроено таким образом, чтобы разрешить: %1$s." + "Поставщик учетной записи %1$s не разрешен." "URL-адрес домашнего сервера" - "Вы можете подключиться только к существующему серверу, поддерживающему sliding sync. Администратору домашнего сервера потребуется настроить его. %1$s" + "Введите адрес домена." "Какой адрес у вашего сервера?" "Выберите свой сервер" "Создать учетную запись" @@ -31,6 +34,13 @@ "Matrix — это открытая сеть для безопасной децентрализованной связи." "Рады видеть вас снова!" "Войти в %1$s" + "Войти вручную" + "Войти в %1$s" + "Войти QR-кодом" + "Создать учетную запись" + "Добро пожаловать в самый быстрый клиент %1$s. Ориентирован на скорость и простоту." + "Добро пожаловать в %1$s. Ориентирован на скорость и простоту." + "Чувствуйте себя как дома с Element" "Установление безопасного соединения" "Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них." "Что теперь?" @@ -79,5 +89,6 @@ "Matrix — это открытая сеть для безопасной децентрализованной связи." "Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем." "Вы собираетесь войти в %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 d4a57173605..e9ce127552f 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -14,11 +14,14 @@ "Použite iného poskytovateľa účtu, ako napríklad vlastný súkromný server alebo pracovný účet." "Zmeniť poskytovateľa účtu" "Nemohli sme sa spojiť s týmto domovským serverom. Skontrolujte prosím, či ste zadali URL adresu domovského servera správne. Ak je adresa URL správna, kontaktujte svoj domovský server pre ďalšiu pomoc." - "Posuvná synchronizácia nie je k dispozícii z dôvodu problému v známom súbore: + "Server nie je k dispozícii kvôli problému v známom súbore: %1$s" - "Tento server momentálne nepodporuje kĺzavú synchronizáciu." + "Vybraný poskytovateľ účtu nepodporuje kĺzavú synchronizáciu. Na používanie aplikácie %1$s je potrebná aktualizácia servera," + "%1$s nemá dovolené pripojiť sa k %2$s." + "Táto aplikácia bola nastavená tak, aby povoľovala: %1$s." + "Poskytovateľ účtu %1$s nie je povolený." "Adresa URL domovského servera" - "Môžete sa pripojiť iba k existujúcemu serveru, ktorý podporuje kĺzavú synchronizáciu. Správca domovského servera ju bude musieť nakonfigurovať. %1$s" + "Zadajte adresu domény." "Aká je adresa vášho servera?" "Vyberte svoj server" "Vytvoriť účet" @@ -31,6 +34,14 @@ "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu." "Vitajte späť!" "Prihlásiť sa do %1$s" + "Verzia %1$s" + "Prihlásiť sa manuálne" + "Prihlásiť sa do %1$s" + "Prihlásiť sa pomocou QR kódu" + "Vytvoriť účet" + "Vitajte v najrýchlejšom %1$s vôbec. Nadupaný pre rýchlosť a jednoduchosť." + "Vitajte v %1$s. Nadupaný, pre rýchlosť a jednoduchosť." + "Buďte vo svojom elemente" "Nadväzovanie bezpečného spojenia" "K novému zariadeniu sa nepodarilo vytvoriť bezpečné pripojenie. Vaše existujúce zariadenia sú stále v bezpečí a nemusíte sa o ne obávať." "Čo teraz?" @@ -79,5 +90,6 @@ Skúste sa prihlásiť manuálne alebo naskenujte QR kód pomocou iného zariade "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu." "Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov." "Chystáte sa prihlásiť do %1$s" + "Vyberte poskytovateľa účtu" "Chystáte sa vytvoriť účet na %1$s" 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 b63ab011839..711e899ca6e 100644 --- a/features/login/impl/src/main/res/values-sv/translations.xml +++ b/features/login/impl/src/main/res/values-sv/translations.xml @@ -14,11 +14,14 @@ "Använd en annan kontoleverantör, till exempel din egen privata server eller ett jobbkonto." "Byt kontoleverantör" "Vi kunde inte nå den här hemservern. Kontrollera att du har angett hemserverns URL korrekt. Om URL:en är korrekt kontaktar du administratören för hemservern för ytterligare hjälp." - "Sliding Sync är inte tillgängligt på grund av ett problem i well-known-filen: + "Sliding Sync är inte tillgängligt på grund av ett problem i .well-known-filen: %1$s" - "Den här servern stöder för närvarande inte sliding sync." + "Den valda kontoleverantören stöder inte sliding sync. En uppgradering till servern behövs för att använda %1$s." + "%1$s får inte ansluta till %2$s." + "Den här appen har konfigurerats för att tillåta: %1$s." + "Kontoleverantör %1$s är inte tillåten." "Hemserverns URL" - "Du kan bara ansluta till en befintlig server som stöder sliding sync. Din hemserveradministratör måste konfigurera det. %1$s" + "Ange en domänadress." "Vad är adressen till din server?" "Välj din server" "Skapa konto" @@ -31,6 +34,13 @@ "Matrix är ett öppet nätverk för säker, decentraliserad kommunikation." "Välkommen tillbaka!" "Logga in på %1$s" + "Logga in manuellt" + "Logga in på %1$s" + "Logga in med QR-kod" + "Skapa konto" + "Välkommen till den snabbaste %1$s någonsin. Superladdad för snabbhet och enkelhet." + "Välkommen till %1$s. Superladdad, för snabbhet och enkelhet." + "Var i ditt rätta element" "Upprättar en säker anslutning" "En säker anslutning kunde inte göras till den nya enheten. Dina befintliga enheter är fortfarande säkra och du behöver inte oroa dig för dem." "Nu då?" @@ -79,5 +89,6 @@ Prova att logga in manuellt eller skanna QR-koden med en annan enhet." "Matrix är ett öppet nätverk för säker, decentraliserad kommunikation." "Det är här dina konversationer kommer att sparas - precis som du skulle använda en e-postleverantör för att spara dina e-brev." "Du är på väg att logga in på %1$s" + "Välj kontoleverantör" "Du är på väg att skapa ett konto på %1$s" diff --git a/features/login/impl/src/main/res/values-tr/translations.xml b/features/login/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..1a18fba1bc7 --- /dev/null +++ b/features/login/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,87 @@ + + + "Hesap sağlayıcısını değiştir" + "Ana sunucu adresi" + "Bir arama terimi veya alan adı adresi girin." + "Bir şirket, topluluk veya özel sunucu arayın." + "Bir hesap sağlayıcısı bulun" + "Konuşmalarınızın saklanacağı yer burasıdır - tıpkı e-postalarınızı saklamak için bir e-posta sağlayıcısı kullandığınız gibi." + "Oturum açmak üzeresiniz %s" + "Konuşmalarınızın saklanacağı yer burasıdır - tıpkı e-postalarınızı saklamak için bir e-posta sağlayıcısı kullandığınız gibi." + "%s üzerinde bir hesap oluşturmak üzeresiniz" + "Matrix.org, Matrix.org Vakfı tarafından yönetilen, güvenli, merkezi olmayan iletişim için halka açık Matrix ağında büyük, ücretsiz bir sunucudur." + "Diğer" + "Kendi özel sunucunuz veya iş hesabınız gibi farklı bir hesap sağlayıcı kullanın." + "Hesap sağlayıcısını değiştir" + "Bu ana sunucuya ulaşamadık. Lütfen ana sunucu URL\'sini doğru girip girmediğinizi kontrol edin. URL doğruysa, daha fazla yardım için ana sunucu yöneticinize başvurun." + "Sliding sync, iyi bilinen dosyadaki bir sorun nedeniyle kullanılamıyor: +%1$s" + "Ana sunucu URL\'si" + "Sunucunuzun adresi nedir?" + "Sunucunuzu seçin" + "Hesap oluştur" + "Hesap devre dışı bırakıldı." + "Yanlış kullanıcı adı ve/veya şifre" + "Bu geçerli bir kullanıcı tanımlayıcısı değil. Kullanılması gereken biçim: \'@user:homeserver.org\'" + "Bu sunucu, yenileme belirteçlerini kullanacak şekilde yapılandırılmıştır. Parola tabanlı oturum açma kullanılırken bunlar desteklenmez." + "Seçilen ana sunucu parola veya OIDC oturum açmayı desteklemiyor. Lütfen yöneticinizle iletişime geçin veya başka bir ana sunucu seçin." + "Bilgilerinizi girin" + "Matrix, güvenli, merkezi olmayan iletişim için açık bir ağdır." + "Tekrar hoş geldiniz!" + "%1$s adresinde oturum aç" + "Manuel olarak oturum aç" + "QR kodu ile giriş yap" + "Hesap oluştur" + "Şimdiye kadarki en hızlı %1$s hoş geldiniz. Hız ve basitlik için güçlendirildi." + "%1$s\'e hoş geldiniz. Hız ve basitlik için süper şarjlı." + "Kendi elementinizde olun" + "Güvenli bir bağlantı kuruluyor" + "Yeni cihaza güvenli bir bağlantı kurulamadı. Mevcut cihazlarınız hala güvende ve onlar için endişelenmenize gerek yok." + "Şimdi ne olacak?" + "Bunun bir ağ sorunu olması ihtimaline karşı bir QR koduyla tekrar oturum açmayı deneyin" + "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" + "Diğer cihazınızda oturum açın ve ardından tekrar deneyin veya zaten oturum açmış başka bir cihaz kullanın." + "Diğer cihaz oturum açmamış" + "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." + "Oturum açma reddedildi" + "Oturum açma süresi doldu. Lütfen tekrar deneyin." + "Oturum açma işlemi zamanında tamamlanmadı" + "Diğer cihazınız %s QR koduyla oturum açmayı desteklemiyor. + +Manuel olarak oturum açmayı deneyin veya QR kodunu başka bir cihazla tarayın." + "QR kodu desteklenmiyor" + "Hesap sağlayıcınız %1$s desteklemiyor." + "%1$s desteklenmiyor" + "Taramaya hazır" + "Bir masaüstü cihazda %1$s açın" + "Avatarınıza tıklayın" + "Seç %1$s" + "\"Yeni cihaz bağla\"" + "QR kodunu bu cihazla tarayın" + "Yalnızca hesap sağlayıcınız destekliyorsa kullanılabilir." + "QR kodunu almak için %1$s uygulamasını başka bir cihazda açın" + "Diğer cihazda gösterilen QR kodunu kullan." + "Tekrar deneyin" + "Yanlış QR kodu" + "Kamera ayarlarına git" + "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" + "QR kodunu tara" + "Yeniden Başla" + "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin." + "Diğer cihazınız bekleniyor" + "Hesap sağlayıcınız, oturum açmayı doğrulamak için aşağıdaki kodu isteyebilir." + "Doğrulama kodunuz" + "Hesap sağlayıcısını değiştir" + "Element çalışanları için özel bir sunucu." + "Matrix, güvenli, merkezi olmayan iletişim için açık bir ağdır." + "Konuşmalarınızın saklanacağı yer burasıdır - tıpkı e-postalarınızı saklamak için bir e-posta sağlayıcısı kullandığınız gibi." + "%1$s sunucusunda oturum açmak üzeresiniz" + "%1$s sunucusunda bir hesap oluşturmak üzeresiniz" + 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 0fa0be98683..47e0933d05a 100644 --- a/features/login/impl/src/main/res/values-uk/translations.xml +++ b/features/login/impl/src/main/res/values-uk/translations.xml @@ -14,11 +14,14 @@ "Використати іншого провайдера облікових записів, наприклад, власний приватний сервер або робочий обліковий запис." "Змінити провайдера облікового запису" "Не вдалося під\'єднатися до цього домашнього сервера. Перевірте правильність введеної URL-адреси домашнього сервера. Якщо URL-адреса правильна, зверніться по додаткову допомогу до адміністратора домашнього сервера." - "Sliding sync недоступний через проблему у файлі well-known: + "Сервер недоступний через помилку у файлі well-known: %1$s" - "Наразі цей сервер не підтримує sliding sync." + "Вибраний постачальник облікових записів не підтримує sliding sync. Щоб користуватися %1$s необхідно оновити сервер." + "%1$s не дозволено під\'єднуватися до %2$s." + "Цей застосунок налаштовано так, щоб дозволити: %1$s." + "Постачальник облікового запису %1$s не дозволений." "URL-адреса домашнього сервера" - "Ви можете під\'єднатися лише до наявного сервера, який підтримує sliding sync. Адміністратор вашого домашнього сервера повинен буде налаштувати його. %1$s" + "Введіть адресу домену." "Яка адреса вашого сервера?" "Виберіть свій сервер" "Створити обліковий запис" @@ -31,6 +34,13 @@ "Matrix — це відкрита мережа для безпечної, децентралізованої комунікації." "З поверненням!" "Увійти в %1$s" + "Увійти вручну" + "Увійти в %1$s" + "Увійти за допомогою QR-коду" + "Створити обліковий запис" + "Ласкаво просимо до найшвидшого %1$s. Заряджений для швидкості та простоти." + "Ласкаво просимо до %1$s. Заряджений, для швидкості та простоти." + "Будьте у своєму element" "Встановлення безпечного з\'єднання" "Не вдалося встановити безпечне з\'єднання з новим пристроєм. Ваші наявні пристрої досі в безпеці, і вам не потрібно про них турбуватися." "Що тепер?" @@ -79,5 +89,6 @@ "Matrix — це відкрита мережа для безпечної, децентралізованої комунікації." "Тут розміщуватимуться ваші розмови — так само як у поштовій скриньці для зберігання своїх електронних листів." "Ви збираєтесь увійти в %1$s" + "Вибір постачальника облікового запису" "Ви збираєтеся створити обліковий запис на %1$s" diff --git a/features/login/impl/src/main/res/values-ur/translations.xml b/features/login/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..7b1216415e9 --- /dev/null +++ b/features/login/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,86 @@ + + + "اکاؤنٹ فراہم کنندہ بدلیں" + "منزلی خادم پتہ" + "تلاش کی اصطلاح یا عنوانِ مجال درج کریں۔" + "کسی شرکت، برادری، یا نجی خادم کیلئے تلاش کریں۔" + "ایک کھاتہ فراہم کنندہ ڈھونڈیں" + "یہ وہ جگہ ہے جہاں آپ کی گفتگوئیں زندہ رہیں گی — بالکل اسی طرح جیسے آپ اپنی برقی خطوط رکھنے کے لیے برقی ڈاک فراہم کنندہ کا استعمال کرتے ہوں گے۔" + "آپ %s میں داخل ہونے والے ہیں" + "یہ وہ جگہ ہے جہاں آپ کی گفتگوئیں زندہ رہیں گی — بالکل اسی طرح جیسے آپ اپنی برقی خطوط رکھنے کے لیے برقی ڈاک فراہم کنندہ کا استعمال کرتے ہوں گے۔" + "آپ %s پر ایک کھاتہ تخلیق کرنے والے ہیں" + "Matrix.org Matrix.org فاؤنڈیشن کے ذریعہ محفوظ، غیر مرکزی مواصلات کے لئے عوامی میٹرکس شبکہ پر ایک بڑا، مفت خادم ہے۔" + "دیگر" + "ایک مختلف کھاتہ فراہم کنندہ استعمال کریں، جیسے آپ کا اپنا نجی خادم یا کام کا کھاتہ۔" + "اکاؤنٹ فراہم کنندہ بدلیں" + "ہم اس منزلی خادم تک نہیں پہنچ سکے۔ برائے مہربانی پڑتال کریں کہ آپ نے ہوم سرور کا عنوان صحیح طریقے سے درج کیا ہے۔ اگر عنوان درست ہے تو مزید مدد کے لیے اپنے منزلی خادم کے منتظم سے رابطہ کریں۔" + "مشہور فائل میں کسی مسئلے کی وجہ سے سلائیڈنگ سنک دستیاب نہیں ہے: +%1$s" + "منزلی خادم عنوان" + "آپکے خادم کا پتہ کیا ہے؟" + "اپنا خادم منتخب کریں" + "کھاتہ تخلیق کریں" + "یہ کھاتہ غیر فعال کر دیا گیا ہے۔" + "غلط صارف نام اور/یا لفظ عبور" + "یہ صالح صارف شناسه نہیں ہے۔ متوقع شکل: @صارف:منزلی خادم" + "یہ خادم تازگی کی رموزِ ممیز استعمال کرنے کے لئے تشکیل دیا گیا ہے۔ لفظ عبور پر مبنی دخول استعمال کرتے ہوئے ان کی حمایت نہیں کی جاتی۔" + "منتخب منزلی خادم کلمۂ عبوری یا OIDC دخول کا تعاون نہیں کرتا۔ برائے مہربانی اپنے منتظم سے رابطہ کریں یا کوئی اور منزلی خادم چنیں۔" + "اپنی تفصیلات درج کریں" + "میٹرکس محفوظ، غیر مرکزی مواصلت کے لئے ایک کھلا شبکہ ہے۔" + "واپس خوش آمدید!" + "%1$s میں داخل ہوں" + "دستی طور پر داخل ہوں" + "کیو آر (QR) رمز کیساتھ داخل ہوں" + "کھاتہ تخلیق کریں" + "اب تک کی تیز ترین %1$s میں خوش آمدید۔ رفتار اور سادگی کے لئے مشحون" + "%1$s پر خوش آمدید۔ شحن فائق شدہ، رفتار اور سادگی کیلئے۔" + "اپنے عنصر میں رہیں" + "محفوظ اتصال قائم کر رہا ہے" + "نئے آلے سے محفوظ اتصال نہیں بنایا جا سکا۔ آپ کے موجودہ آلات اب بھی محفوظ ہیں اور آپ کو ان کے بارے میں فکر کرنے کی ضرورت نہیں ہے۔" + "اب کیا؟" + "اگر یہ شبکہ کا مسئلہ تھا تو کیو آر رمز کے ساتھ دوبارہ داخل ہونے کی کوشش کریں۔" + "اگر آپ کو بھی یہی مسئلہ درپیش ہو، تو کوئی دوسرا وائی فائی شبکہ آزمائیں یا وائی فائی کے بجائے اپنے محمول بیانات استعمال کریں۔" + "اگر یہ کام نہ کرے، تو دستی طور پر داخل ہوں" + "اتصال محفوظ نہیں" + "آپ سے اس آلے پر دکھائے گئے دو ہندسوں کو درج کرنے کو کہا جائے گا۔" + "اپنے دوسرے آلے پر درج ذیل نمبر درج کریں" + "اپنے دوسرے آلے میں داخل ہوں اور پھر دوبارہ کوشش کریں، یا کوئی دوسرا آلہ استعمال کریں جو پہلے سے دخول شدہ ہے۔" + "دوسرا آلہ دخول شدہ نہیں" + "دوسرے آلے پر دخول منسوخ کر دیا گیا تھا۔" + "دخول کی درخواست منسوخ" + "دوسرے آلہ پر دخول کو مسترد کر دیا گیا تھا۔" + "دخول مسترد کیا گیا" + "دخول کی میعاد ختم۔ برائے مہربانی دوبارہ کوشش کریں۔" + "دخول وقت پر مکمل نہیں ہوا تھا" + "آپ کا دوسرا آلہ کیو آر رمز کے ساتھ %s میں دخول کا تعاون نہیں کرتا۔ + +دستی طور پر داخل ہونے کی کوشش کریں ، یا کسی دوسرے آلے سے کیو آر رمز مسح ضوئی کریں۔" + "کر رمز غیر تعاون یافتہ" + "آپ کا کھاتہ فراہم کنندہ %1$s کا تعاون نہیں کرتا۔" + "%1$s تعاون یافتہ نہیں" + "مسح ضوئی کیلئے تیار" + "برمیز آلے پر %1$s کھولیں" + "اپنے اوتار پر دبائیں" + "%1$s منتخب کریں" + "”نیا آلہ مربوط کریں“" + "اس آلے کے ساتھ کیو آر رمز مسح ضوئی کریں" + "کیو آر رمز حاصل کرنے کے لئے کسی دوسرے آلے پر %1$s کھولیں" + "دوسرے آلے پر دکھایا گیا کیو آر رمز استعمال کریں۔" + "دوبارہ کوشش کریں" + "غلط کیو آر رمز" + "تصویرگر کی ترتیبات پر جائیں" + "جاری رکھنے کے لیے آپ %1$s کو اپنے آلے کا تصویرگر استعمال کرنے کی اجازت دینے کی ضرورت ہے۔" + "کیو آر رمز کو مسح ضوئی کرنے کے لئے تصویرگر تک رسائی کی اجازت دیں" + "کیو آر رمز مسح ضوئی کریں" + "از سر نو شروع کریں" + "ایک غیر متوقع نقص واقع ہوا۔ برائے مہربانی دوبارہ کوشش کریں۔" + "آپکے دوسرے آلے کا منتظر" + "آپ کا کھاتہ فراہم کنندہ دخول کی توثیق کے لیے درج ذیل رمز کا مطالبہ کر سکتا ہے۔" + "آپکا توثیقی رمز" + "اکاؤنٹ فراہم کنندہ بدلیں" + "ایلیمنٹ کے ملازمین کیلئے ایک نجی خادم۔" + "میٹرکس محفوظ، غیر مرکزی مواصلت کے لئے ایک کھلا شبکہ ہے۔" + "یہ وہ جگہ ہے جہاں آپ کی گفتگوئیں زندہ رہیں گی — بالکل اسی طرح جیسے آپ اپنی برقی خطوط رکھنے کے لیے برقی ڈاک فراہم کنندہ کا استعمال کرتے ہوں گے۔" + "آپ %1$s میں داخل ہونے والے ہیں" + "آپ %1$s پر ایک کھاتہ بنانے والے ہیں" + 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 db16e17b093..25fc695452a 100644 --- a/features/login/impl/src/main/res/values-uz/translations.xml +++ b/features/login/impl/src/main/res/values-uz/translations.xml @@ -14,11 +14,10 @@ "Shaxsiy serveringiz yoki ishchi hisob qaydnomangiz kabi boshqa hisob provayderidan foydalaning." "Hisob provayderini o\'zgartiring" "Bu uy serveriga kira olmadik. Iltimos, uy serverining URL manzilini to\'ri kiritganingizni tekshiring. Agar URL toʻgʻri boʻlsa, qoʻshimcha yordam olish uchun uy serveri administratoriga murojaat qiling." - "Hozirda bu server siljish sinxronlashni qo‘llab-quvvatlamaydi." "Uy serverining URL manzili" - "Siz faqat siljish sinxronlashni qo\'llab-quvvatlaydigan mavjud serverga ulanishingiz mumkin. Uy serveringiz administratori uni sozlashi kerak.%1$s" "Serveringizning manzili nima?" "Serveringizni tanlang" + "Hisob yaratish" "Bu hisob o‘chirilgan." "Notog\'ri foydalanuvchi nomi va/yoki parol" "Bu haqiqiy foydalanuvchi identifikatori emas. Kutilayotgan format: \'@user:homeserver.org\'" @@ -27,6 +26,12 @@ "Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir." "Qaytib kelganingizdan xursandmiz!" "Kirish%1$s" + "Qo\'lda tizimga kiring" + "QR kod bilan tizimga kiring" + "Hisob yaratish" + "Eng tezkor %1$sga xush kelibsiz. Tezlik va oddylik uchun super zaryadlangan." + "%1$sga Xush kelibsiz. Tezlik va oddylik uchun o\'ta zaryadlangan." + "Elementingizda bo\'ling" "Hisob provayderini o\'zgartiring" "Element xodimlari uchun shaxsiy server." "Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir." 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 a87d1c20949..f23089779ed 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 @@ -14,11 +14,11 @@ "使用不同的帳戶提供者,例如您自己的伺服器或工作帳號。" "更改帳號提供者" "我們無法連線至此家伺服器。請檢查您是否已正確輸入家伺服器 URL。若 URL 正確,請聯絡您家伺服器的管理員以取得進一步協助。" - "因為 well-known 檔案的問題,無法使用 sliding sync: + "因為 well-known 檔案的問題,伺服器不可用: %1$s" - "此伺服器目前不支援滑動同步(sliding sync)。" + "選定的帳號提供者不支援 sliding sync。必須升級伺服器才能使用 %1$s。" "家伺服器 URL" - "您僅能連線至支援 sliding sync 的既有伺服器。您的家伺服器管理員必須設定它。%1$s" + "輸入網域位址。" "您的伺服器地址?" "選擇您的伺服器" "建立帳號" @@ -31,6 +31,12 @@ "Matrix 是一個開放網路,為了安全且去中心化的通訊而生。" "歡迎回來!" "登入 %1$s" + "手動登入" + "使用 QR code 登入" + "建立帳號" + "歡迎使用有史以來最快的 %1$s。速度超快,操作簡便。" + "歡迎使用 %1$s。速度超快且簡單。" + "Be in your element" "建立安全連線" "無法與新裝置建立安全連線。您現有的裝置仍然安全,您不必擔心它們。" "現在怎麼辦?" 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 c23bb10f9f0..44d5d48574f 100644 --- a/features/login/impl/src/main/res/values-zh/translations.xml +++ b/features/login/impl/src/main/res/values-zh/translations.xml @@ -16,9 +16,7 @@ "我们无法访问此服务器。请检查您输入的服务器网址是否正确。如果 URL 正确,请联系您的服务器管理员寻求进一步帮助。" "由于 Well Known 文件中的问题,Sliding Sync 不可用: %1$s" - "该服务器目前不支持 Sliding Sync。" "服务器网址" - "您只能连接到支持 Sliding Sync 的现有服务器。您的服务器管理员需要对其进行配置。%1$s" "您的服务器地址是什么?" "选择服务器" "创建账户" @@ -31,6 +29,12 @@ "Matrix 是一个用于安全、去中心化通信的开放网络。" "欢迎回来!" "登录到 %1$s" + "手动登录" + "使用二维码登录" + "创建账户" + "欢迎使用 %1$s,快而简约的消息应用。" + "欢迎使用 %1$s,速度与简洁的极致。" + "融入您的 Element" "建立安全连接" "无法与新设备建立安全连接。您现有的设备仍然安全,无需担心。" "现在怎么办?" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index be01bd56d5e..3f8beeec4f0 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -14,11 +14,14 @@ "Use a different account provider, such as your own private server or a work account." "Change account provider" "We couldn\'t reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help." - "Sliding sync isn\'t available due to an issue in the well-known file: + "Server isn\'t available due to an issue in the .well-known file: %1$s" - "This server currently doesn’t support sliding sync." + "The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$s." + "%1$s is not allowed to connect to %2$s." + "This app has been configured to allow: %1$s." + "Account provider %1$s not allowed." "Homeserver URL" - "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s" + "Enter a domain address." "What is the address of your server?" "Select your server" "Create account" @@ -31,6 +34,14 @@ "Matrix is an open network for secure, decentralised communication." "Welcome back!" "Sign in to %1$s" + "Version %1$s" + "Sign in manually" + "Sign in to %1$s" + "Sign in with QR code" + "Create account" + "Welcome to our new app. Supercharged for speed and simplicity." + "Welcome to our new app. Supercharged for speed and simplicity." + "The Revivalists" "Establishing a secure connection" "A secure connection could not be made to the new device. Your existing devices are still safe and you don\'t need to worry about them." "What now?" @@ -79,5 +90,6 @@ Try signing in manually, or scan the QR code with another device." "Matrix is an open network for secure, decentralised communication." "This is where your conversations will live — just like you would use an email provider to keep your emails." "You’re about to sign in to %1$s" + "Choose account provider" "You’re about to create an account on %1$s" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolverTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolverTest.kt new file mode 100644 index 00000000000..c22074f8d82 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolverTest.kt @@ -0,0 +1,81 @@ +/* + * 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.login.impl + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.api.LoginParams +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DefaultLoginIntentResolverTest { + @Test + fun `nominal case`() { + val sut = DefaultLoginIntentResolver() + val uriString = "https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org" + assertThat(sut.parse(uriString)).isEqualTo( + LoginParams( + accountProvider = "example.org", + loginHint = "mxid:@alice:example.org", + ) + ) + } + + @Test + fun `extra unknown param`() { + val sut = DefaultLoginIntentResolver() + val uriString = "https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org&extra=uknown" + assertThat(sut.parse(uriString)).isEqualTo( + LoginParams( + accountProvider = "example.org", + loginHint = "mxid:@alice:example.org", + ) + ) + } + + @Test + fun `no account provider`() { + val sut = DefaultLoginIntentResolver() + val uriString = "https://mobile.element.io/element/?login_hint=mxid:@alice:example.org" + assertThat(sut.parse(uriString)).isNull() + } + + @Test + fun `no path`() { + val sut = DefaultLoginIntentResolver() + val uriString = "https://mobile.element.io?account_provider=example.org&login_hint=mxid:@alice:example.org" + assertThat(sut.parse(uriString)).isNull() + } + + @Test + fun `wrong path`() { + val sut = DefaultLoginIntentResolver() + val uriString = "https://mobile.element.io/wrong?account_provider=example.org&login_hint=mxid:@alice:example.org" + assertThat(sut.parse(uriString)).isNull() + } + + @Test + fun `wrong host`() { + val sut = DefaultLoginIntentResolver() + val uriString = "https://wrong.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org" + assertThat(sut.parse(uriString)).isNull() + } + + @Test + fun `no login_hint param`() { + val sut = DefaultLoginIntentResolver() + val uriString = "https://mobile.element.io/element/?account_provider=example.org" + assertThat(sut.parse(uriString)).isEqualTo( + LoginParams( + accountProvider = "example.org", + loginHint = null, + ) + ) + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt new file mode 100644 index 00000000000..3d84a1da415 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt @@ -0,0 +1,109 @@ +/* + * 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.login.impl.accountprovider + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class AccountProviderDataSourceTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val sut = AccountProviderDataSource(FakeEnterpriseService()) + sut.flow.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + AccountProvider( + url = AuthenticationConfig.MATRIX_ORG_URL, + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = false, + ) + ) + } + } + + @Test + fun `present - initial state - matrix org`() = runTest { + val sut = AccountProviderDataSource( + FakeEnterpriseService( + defaultHomeserverListResult = { listOf(AuthenticationConfig.MATRIX_ORG_URL) } + ) + ) + sut.flow.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + AccountProvider( + url = AuthenticationConfig.MATRIX_ORG_URL, + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = false, + ) + ) + } + } + + @Test + fun `present - ensure that default homeserver is not star char`() = runTest { + val sut = AccountProviderDataSource( + FakeEnterpriseService( + defaultHomeserverListResult = { listOf(EnterpriseService.ANY_ACCOUNT_PROVIDER, AuthenticationConfig.MATRIX_ORG_URL) } + ) + ) + sut.flow.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + AccountProvider( + url = AuthenticationConfig.MATRIX_ORG_URL, + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = false, + ) + ) + } + } + + @Test + fun `present - user change and reset`() = runTest { + val sut = AccountProviderDataSource(FakeEnterpriseService()) + sut.flow.test { + val initialState = awaitItem() + assertThat(initialState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) + sut.userSelection(AccountProvider(url = "https://example.com")) + val changedState = awaitItem() + assertThat(changedState).isEqualTo( + AccountProvider( + url = "https://example.com", + title = "example.com", + subtitle = null, + isPublic = false, + isMatrixOrg = false, + isValid = false, + ) + ) + sut.reset() + val resetState = awaitItem() + assertThat(resetState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) + } + } +} 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 2dbde8633e7..17c4cc0ae76 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 @@ -7,17 +7,20 @@ package io.element.android.features.login.impl.changeserver -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.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -28,13 +31,7 @@ class ChangeServerPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = ChangeServerPresenter( - FakeMatrixAuthenticationService(), - AccountProviderDataSource() - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createPresenter().test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) } @@ -43,13 +40,12 @@ class ChangeServerPresenterTest { @Test fun `present - change server ok`() = runTest { val authenticationService = FakeMatrixAuthenticationService() - val presenter = ChangeServerPresenter( - authenticationService, - AccountProviderDataSource() - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createPresenter( + authenticationService = authenticationService, + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = { true }, + ), + ).test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) authenticationService.givenHomeserver(A_HOMESERVER) @@ -63,14 +59,11 @@ class ChangeServerPresenterTest { @Test fun `present - change server error`() = runTest { - val authenticationService = FakeMatrixAuthenticationService() - val presenter = ChangeServerPresenter( - authenticationService, - AccountProviderDataSource() - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createPresenter( + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = { true }, + ), + ).test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) @@ -84,4 +77,42 @@ class ChangeServerPresenterTest { assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Uninitialized) } } + + @Test + fun `present - change server not allowed error`() = runTest { + val isAllowedToConnectToHomeserverResult = lambdaRecorder { false } + createPresenter( + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult, + defaultHomeserverListResult = { listOf("element.io") }, + ), + ).test { + val initialState = awaitItem() + assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) + val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL) + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider)) + val loadingState = awaitItem() + assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java) + val failureState = awaitItem() + assertThat( + (failureState.changeServerAction.errorOrNull() as ChangeServerError.UnauthorizedAccountProvider).unauthorisedAccountProviderTitle + ).isEqualTo(anAccountProvider.title) + assertThat( + (failureState.changeServerAction.errorOrNull() as ChangeServerError.UnauthorizedAccountProvider).authorisedAccountProviderTitles + ).containsExactly("element.io") + isAllowedToConnectToHomeserverResult.assertions() + .isCalledOnce() + .with(value(A_HOMESERVER_URL)) + } + } + + private fun createPresenter( + authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), + accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + enterpriseService: EnterpriseService = FakeEnterpriseService(), + ) = ChangeServerPresenter( + authenticationService = authenticationService, + accountProviderDataSource = accountProviderDataSource, + enterpriseService = enterpriseService, + ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt index 94155c6ac6d..2741bbd62ab 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt @@ -17,7 +17,7 @@ class ErrorFormatterTest { // region loginError @Test fun `loginError - invalid unknown error returns unknown error message`() { - val error = Throwable("Some unknown error") + val error = RuntimeException("Some unknown error") assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt index df85aa801a3..89abc6ddeff 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -11,8 +11,12 @@ 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.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.changeserver.aChangeServerState +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -25,7 +29,10 @@ class ChangeAccountProviderPresenterTest { @Test fun `present - initial state`() = runTest { val presenter = ChangeAccountProviderPresenter( - changeServerPresenter = { aChangeServerState() } + changeServerPresenter = { aChangeServerState() }, + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { emptyList() } + ), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -43,6 +50,75 @@ class ChangeAccountProviderPresenterTest { ) ) ) + assertThat(initialState.canSearchForAccountProviders).isTrue() + } + } + + @Test + fun `present - fixed list of account providers`() = runTest { + val presenter = ChangeAccountProviderPresenter( + changeServerPresenter = { aChangeServerState() }, + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { + listOf(AN_ACCOUNT_PROVIDER, AN_ACCOUNT_PROVIDER_2) + } + ), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.accountProviders).isEqualTo( + listOf( + AccountProvider( + url = "https://matrix.org", + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = true, + ), + AccountProvider( + url = "https://element.io", + title = "element.io", + subtitle = null, + isPublic = false, + isMatrixOrg = false, + isValid = true, + ) + ) + ) + assertThat(initialState.canSearchForAccountProviders).isFalse() + } + } + + @Test + fun `present - opened list of account providers`() = runTest { + val presenter = ChangeAccountProviderPresenter( + changeServerPresenter = { aChangeServerState() }, + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { + listOf(AN_ACCOUNT_PROVIDER, EnterpriseService.ANY_ACCOUNT_PROVIDER) + } + ), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.accountProviders).isEqualTo( + listOf( + AccountProvider( + url = "https://matrix.org", + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = true, + ) + ) + ) + assertThat(initialState.canSearchForAccountProviders).isTrue() } } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt new file mode 100644 index 00000000000..5bad8a36383 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt @@ -0,0 +1,167 @@ +/* + * 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.login.impl.screens.chooseaccountprovider + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_3 +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class ChooseAccountProviderPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + companion object { + private const val ACCOUNT_PROVIDER_FROM_CONFIG_1 = AN_ACCOUNT_PROVIDER_2 + private const val ACCOUNT_PROVIDER_FROM_CONFIG_2 = AN_ACCOUNT_PROVIDER_3 + val accountProvider1 = AccountProvider( + url = ACCOUNT_PROVIDER_FROM_CONFIG_1.ensureProtocol(), + subtitle = null, + isPublic = false, + isMatrixOrg = false, + isValid = true, + ) + val accountProvider2 = AccountProvider( + url = ACCOUNT_PROVIDER_FROM_CONFIG_2.ensureProtocol(), + subtitle = null, + isPublic = false, + isMatrixOrg = false, + isValid = true, + ) + } + + @Test + fun `present - ensure initial conditions`() { + assertThat( + setOf( + ACCOUNT_PROVIDER_FROM_CONFIG_1, + ACCOUNT_PROVIDER_FROM_CONFIG_2, + ).size + ).isEqualTo(2) + } + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter( + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + ), + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.accountProviders).containsExactly( + accountProvider1, + accountProvider2, + ) + assertThat(initialState.selectedAccountProvider).isNull() + } + } + + @Test + fun `present - Continue when no account provider is selected has no effect`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + val presenter = createPresenter( + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + ), + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + awaitItem().also { + assertThat(it.selectedAccountProvider).isNull() + it.eventSink(ChooseAccountProviderEvents.Continue) + expectNoEvents() + } + } + } + + @Test + fun `present - select account provider and continue - error then clear error`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + val presenter = createPresenter( + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + ), + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + awaitItem().also { + assertThat(it.selectedAccountProvider).isNull() + it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider1)) + } + awaitItem().also { + assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1) + authenticationService.givenChangeServerError(AN_EXCEPTION) + it.eventSink(ChooseAccountProviderEvents.Continue) + skipItems(1) // Loading + + // Check an error was returned + val submittedState = awaitItem() + assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + + // Assert the error is then cleared + submittedState.eventSink(ChooseAccountProviderEvents.ClearError) + val clearedState = awaitItem() + assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + } + } + } + + @Test + fun `present - default account provider - select account provider during login has no effect`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + val presenter = createPresenter( + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + ), + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + awaitItem().also { + assertThat(it.selectedAccountProvider).isNull() + it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider1)) + } + awaitItem().also { + assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1) + it.eventSink(ChooseAccountProviderEvents.Continue) + } + awaitItem().also { + assertThat(it.loginMode.isLoading()).isTrue() + it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2)) + } + expectNoEvents() + } + } +} + +private fun createPresenter( + enterpriseService: EnterpriseService = FakeEnterpriseService(), + loginHelper: LoginHelper = createLoginHelper(), +) = ChooseAccountProviderPresenter( + enterpriseService = enterpriseService, + loginHelper = loginHelper, +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt new file mode 100644 index 00000000000..1acac2713a3 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt @@ -0,0 +1,50 @@ +/* + * 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.login.impl.screens.chooseaccountprovider + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncData +import org.junit.Test + +class ChooseAccountProviderStateTest { + @Test + fun `submitEnabled returns false when there is no selectedAccountProvider`() { + val sut = aChooseAccountProviderState( + selectedAccountProvider = null, + ) + assertThat(sut.submitEnabled).isFalse() + } + + @Test + fun `submitEnabled returns true when there is a selectedAccountProvider`() { + val sut = aChooseAccountProviderState( + selectedAccountProvider = anAccountProvider(), + ) + assertThat(sut.submitEnabled).isTrue() + } + + @Test + fun `submitEnabled returns false when there is a selectedAccountProvider but there is an error`() { + val sut = aChooseAccountProviderState( + selectedAccountProvider = anAccountProvider(), + loginMode = AsyncData.Failure(Throwable("Error")), + ) + assertThat(sut.submitEnabled).isFalse() + } + + @Test + fun `submitEnabled returns false when there is a selectedAccountProvider but the result is successful`() { + val sut = aChooseAccountProviderState( + selectedAccountProvider = anAccountProvider(), + loginMode = AsyncData.Success(LoginMode.PasswordLogin), + ) + assertThat(sut.submitEnabled).isFalse() + } +} 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 new file mode 100644 index 00000000000..a044874eed0 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt @@ -0,0 +1,102 @@ +/* + * 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.login.impl.screens.chooseaccountprovider + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +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.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 +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.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`() { + val eventSink = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setChooseAccountProviderView( + state = aChooseAccountProviderState( + eventSink = eventSink, + ), + onBackClick = it, + ) + rule.pressBack() + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `selecting an account provider emits the the expected event`() { + val eventSink = EventsRecorder() + rule.setChooseAccountProviderView( + state = aChooseAccountProviderState( + accountProviders = listOf( + ChooseAccountProviderPresenterTest.accountProvider1, + ChooseAccountProviderPresenterTest.accountProvider2, + ), + selectedAccountProvider = anAccountProvider(), + eventSink = eventSink, + ), + ) + 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`() { + val eventSink = EventsRecorder() + rule.setChooseAccountProviderView( + state = aChooseAccountProviderState( + loginMode = AsyncData.Failure(AN_EXCEPTION), + eventSink = eventSink, + ), + ) + rule.clickOn(CommonStrings.action_ok) + eventSink.assertSingle(ChooseAccountProviderEvents.ClearError) + } + + private fun AndroidComposeTestRule.setChooseAccountProviderView( + state: ChooseAccountProviderState, + onBackClick: () -> Unit = EnsureNeverCalled(), + onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), + onNeedLoginPassword: () -> Unit = EnsureNeverCalled(), + onLearnMoreClick: () -> Unit = EnsureNeverCalled(), + onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(), + ) { + setContent { + ChooseAccountProviderView( + state = state, + onBackClick = onBackClick, + 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 3ff604a1c56..cbf61f46787 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 @@ -11,20 +11,24 @@ 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.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported -import io.element.android.features.login.impl.util.defaultAccountProvider +import io.element.android.features.login.impl.screens.onboarding.createLoginHelper import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.libraries.architecture.AsyncData 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.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC -import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.impl.customtab.DefaultOidcActionFlow +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.waitForPredicate import kotlinx.coroutines.test.runTest @@ -44,8 +48,8 @@ class ConfirmAccountProviderPresenterTest { val initialState = awaitItem() assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.submitEnabled).isTrue() - assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) - assertThat(initialState.loginFlow).isEqualTo(AsyncData.Uninitialized) + assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) + assertThat(initialState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -63,11 +67,11 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.PasswordLogin) + assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin) } } @@ -85,18 +89,18 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) + assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) } } @Test fun `present - oidc - cancel with failure`() = runTest { val authenticationService = FakeMatrixAuthenticationService() - val defaultOidcActionFlow = DefaultOidcActionFlow() + val defaultOidcActionFlow = FakeOidcActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, defaultOidcActionFlow = defaultOidcActionFlow, @@ -109,22 +113,22 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) - authenticationService.givenOidcCancelError(A_THROWABLE) + assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) + authenticationService.givenOidcCancelError(AN_EXCEPTION) defaultOidcActionFlow.post(OidcAction.GoBack) val cancelFailureState = awaitItem() - assertThat(cancelFailureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java) + assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @Test fun `present - oidc - cancel with success`() = runTest { val authenticationService = FakeMatrixAuthenticationService() - val defaultOidcActionFlow = DefaultOidcActionFlow() + val defaultOidcActionFlow = FakeOidcActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, defaultOidcActionFlow = defaultOidcActionFlow, @@ -137,21 +141,21 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) + assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) defaultOidcActionFlow.post(OidcAction.GoBack) val cancelFinalState = awaitItem() - assertThat(cancelFinalState.loginFlow).isInstanceOf(AsyncData.Uninitialized::class.java) + assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @Test fun `present - oidc - success with failure`() = runTest { val authenticationService = FakeMatrixAuthenticationService() - val defaultOidcActionFlow = DefaultOidcActionFlow() + val defaultOidcActionFlow = FakeOidcActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, defaultOidcActionFlow = defaultOidcActionFlow, @@ -164,24 +168,24 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) - authenticationService.givenLoginError(A_THROWABLE) + assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) + authenticationService.givenLoginError(AN_EXCEPTION) defaultOidcActionFlow.post(OidcAction.Success("aUrl")) val cancelLoadingState = awaitItem() - assertThat(cancelLoadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) + assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val cancelFailureState = awaitItem() - assertThat(cancelFailureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java) + assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @Test fun `present - oidc - success with success`() = runTest { val authenticationService = FakeMatrixAuthenticationService() - val defaultOidcActionFlow = DefaultOidcActionFlow() + val defaultOidcActionFlow = FakeOidcActionFlow() val defaultLoginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) } @@ -198,15 +202,15 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) + assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() defaultOidcActionFlow.post(OidcAction.Success("aUrl")) val successSuccessState = awaitItem() - assertThat(successSuccessState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) + assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java) waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value } } } @@ -221,12 +225,12 @@ class ConfirmAccountProviderPresenterTest { presenter.present() }.test { val initialState = awaitItem() - authenticationService.givenChangeServerError(Throwable()) + authenticationService.givenChangeServerError(RuntimeException()) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val failureState = awaitItem() assertThat(failureState.submitEnabled).isFalse() - assertThat(failureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java) + assertThat(failureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -242,19 +246,19 @@ class ConfirmAccountProviderPresenterTest { val initialState = awaitItem() // Submit will return an error - authenticationService.givenChangeServerError(A_THROWABLE) + authenticationService.givenChangeServerError(AN_EXCEPTION) initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginFlow).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -277,11 +281,11 @@ class ConfirmAccountProviderPresenterTest { skipItems(1) // Loading // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginFlow.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) + assertThat(submittedState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) // Assert the error is then cleared submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginFlow).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -300,8 +304,8 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) + assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) } } @@ -322,8 +326,8 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) + assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) } } @@ -344,23 +348,25 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.AccountCreationFlow(aUrl)) + assertThat(submittedState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl)) } } private fun createConfirmAccountProviderPresenter( params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false), - accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(), + accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), - defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(), + defaultOidcActionFlow: OidcActionFlow = FakeOidcActionFlow(), defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), ) = ConfirmAccountProviderPresenter( params = params, accountProviderDataSource = accountProviderDataSource, - authenticationService = matrixAuthenticationService, - oidcActionFlow = defaultOidcActionFlow, - defaultLoginUserStory = defaultLoginUserStory, - webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever + loginHelper = createLoginHelper( + authenticationService = matrixAuthenticationService, + oidcActionFlow = defaultOidcActionFlow, + defaultLoginUserStory = defaultLoginUserStory, + webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, + ), ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt index ef8cfc86d08..d97a021fccd 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt @@ -16,10 +16,14 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.external.ExternalSession +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -88,12 +92,16 @@ class CreateAccountPresenterTest { defaultLoginUserStory.setLoginFlowIsDone(false) assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() val lambda = lambdaRecorder { _ -> anExternalSession() } + val sessionVerificationService = FakeSessionVerificationService() + val client = FakeMatrixClient(sessionVerificationService = sessionVerificationService) + val clientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }) val presenter = createPresenter( authenticationService = FakeMatrixAuthenticationService( importCreatedSessionLambda = { Result.success(A_SESSION_ID) } ), messageParser = FakeMessageParser(lambda), defaultLoginUserStory = defaultLoginUserStory, + clientProvider = clientProvider, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -101,6 +109,7 @@ class CreateAccountPresenterTest { val initialState = awaitItem() initialState.eventSink(CreateAccountEvents.OnMessageReceived("aMessage")) assertThat(awaitItem().createAction.isLoading()).isTrue() + sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) assertThat(awaitItem().createAction.dataOrNull()).isEqualTo(A_SESSION_ID) } lambda.assertions().isCalledOnce().with(value("aMessage")) @@ -135,11 +144,13 @@ class CreateAccountPresenterTest { defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), messageParser: MessageParser = FakeMessageParser(), buildMeta: BuildMeta = aBuildMeta(), + clientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), ) = CreateAccountPresenter( url = url, authenticationService = authenticationService, defaultLoginUserStory = defaultLoginUserStory, messageParser = messageParser, buildMeta = buildMeta, + clientProvider = clientProvider, ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/DefaultMessageParserTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/DefaultMessageParserTest.kt index e118570f4e6..a1fbbc0fe20 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/DefaultMessageParserTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/DefaultMessageParserTest.kt @@ -8,8 +8,9 @@ package io.element.android.features.login.impl.screens.createaccount import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.libraries.matrix.api.auth.external.ExternalSession import kotlinx.serialization.SerializationException import org.junit.Assert.assertThrows @@ -27,9 +28,7 @@ class DefaultMessageParserTest { @Test fun `DefaultMessageParser is able to parse correct message`() { - val sut = DefaultMessageParser( - AccountProviderDataSource() - ) + val sut = createDefaultMessageParser() assertThat(sut.parse(validMessage)).isEqualTo( anExternalSession( homeserverUrl = "home_server", @@ -39,9 +38,7 @@ class DefaultMessageParserTest { @Test fun `DefaultMessageParser should throw Exception in case of error`() { - val sut = DefaultMessageParser( - AccountProviderDataSource() - ) + val sut = createDefaultMessageParser() // kotlinx.serialization.json.internal.JsonDecodingException assertThrows(SerializationException::class.java) { sut.parse("invalid json") } // missing userId @@ -60,16 +57,20 @@ class DefaultMessageParserTest { @Test fun `DefaultMessageParser should be successful even is homeserver url is missing`() { - val sut = DefaultMessageParser( - AccountProviderDataSource() - ) + val sut = createDefaultMessageParser() // missing homeServer assertThat(sut.parse(validMessage.replace(""""home_server": "home_server",""", ""))).isEqualTo( anExternalSession( - homeserverUrl = defaultAccountProvider.url, + homeserverUrl = AuthenticationConfig.MATRIX_ORG_URL, ) ) } + + private fun createDefaultMessageParser(): DefaultMessageParser { + return DefaultMessageParser( + AccountProviderDataSource(FakeEnterpriseService()) + ) + } } internal fun anExternalSession( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt index cced3fece5f..2413e09243c 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt @@ -7,22 +7,21 @@ package io.element.android.features.login.impl.screens.loginpassword -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.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -33,19 +32,9 @@ class LoginPasswordPresenterTest { @Test fun `present - initial state`() = runTest { - val authenticationService = FakeMatrixAuthenticationService() - val accountProviderDataSource = AccountProviderDataSource() - val loginUserStory = DefaultLoginUserStory() - val presenter = LoginPasswordPresenter( - authenticationService, - accountProviderDataSource, - loginUserStory, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createLoginPasswordPresenter().test { val initialState = awaitItem() - assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) + assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) assertThat(initialState.formState).isEqualTo(LoginFormState.Default) assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.submitEnabled).isFalse() @@ -55,17 +44,10 @@ class LoginPasswordPresenterTest { @Test fun `present - enter login and password`() = runTest { val authenticationService = FakeMatrixAuthenticationService() - val accountProviderDataSource = AccountProviderDataSource() - val loginUserStory = DefaultLoginUserStory() - val presenter = LoginPasswordPresenter( - authenticationService, - accountProviderDataSource, - loginUserStory, - ) authenticationService.givenHomeserver(A_HOMESERVER) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createLoginPasswordPresenter( + authenticationService = authenticationService, + ).test { val initialState = awaitItem() initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) val loginState = awaitItem() @@ -81,17 +63,12 @@ class LoginPasswordPresenterTest { @Test fun `present - submit`() = runTest { val authenticationService = FakeMatrixAuthenticationService() - val accountProviderDataSource = AccountProviderDataSource() - val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) } - val presenter = LoginPasswordPresenter( - authenticationService, - accountProviderDataSource, - loginUserStory, - ) authenticationService.givenHomeserver(A_HOMESERVER) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) } + createLoginPasswordPresenter( + authenticationService = authenticationService, + defaultLoginUserStory = loginUserStory, + ).test { assertThat(loginUserStory.loginFlowIsDone.value).isFalse() val initialState = awaitItem() initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) @@ -110,61 +87,57 @@ class LoginPasswordPresenterTest { @Test fun `present - submit with error`() = runTest { val authenticationService = FakeMatrixAuthenticationService() - val accountProviderDataSource = AccountProviderDataSource() - val loginUserStory = DefaultLoginUserStory() - val presenter = LoginPasswordPresenter( - authenticationService, - accountProviderDataSource, - loginUserStory, - ) authenticationService.givenHomeserver(A_HOMESERVER) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createLoginPasswordPresenter( + authenticationService = authenticationService, + ).test { val initialState = awaitItem() initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) skipItems(1) val loginAndPasswordState = awaitItem() - authenticationService.givenLoginError(A_THROWABLE) + authenticationService.givenLoginError(AN_EXCEPTION) loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) val submitState = awaitItem() assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java) val loggedInState = awaitItem() - assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure(A_THROWABLE)) + assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure(AN_EXCEPTION)) } } @Test fun `present - clear error`() = runTest { val authenticationService = FakeMatrixAuthenticationService() - val accountProviderDataSource = AccountProviderDataSource() - val loginUserStory = DefaultLoginUserStory() - val presenter = LoginPasswordPresenter( - authenticationService, - accountProviderDataSource, - loginUserStory, - ) authenticationService.givenHomeserver(A_HOMESERVER) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + createLoginPasswordPresenter( + authenticationService = authenticationService, + ).test { val initialState = awaitItem() initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) skipItems(1) val loginAndPasswordState = awaitItem() - authenticationService.givenLoginError(A_THROWABLE) + authenticationService.givenLoginError(AN_EXCEPTION) loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) val submitState = awaitItem() assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java) val loggedInState = awaitItem() // Check an error was returned - assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure(A_THROWABLE)) + assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure(AN_EXCEPTION)) // Assert the error is then cleared loggedInState.eventSink(LoginPasswordEvents.ClearError) val clearedState = awaitItem() assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized) } } + + private fun createLoginPasswordPresenter( + authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), + accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory() + ): LoginPasswordPresenter = LoginPasswordPresenter( + authenticationService = authenticationService, + accountProviderDataSource = accountProviderDataSource, + defaultLoginUserStory = defaultLoginUserStory, + ) } 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 new file mode 100644 index 00000000000..99deb8b7bb2 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt @@ -0,0 +1,193 @@ +/* + * 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.login.impl.screens.loginpassword + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.matrix.test.A_PASSWORD +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class LoginPasswordViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invoke back callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder + ), + onBackClick = callback, + ) + rule.pressBack() + } + } + + @Test + fun `changing login invokes the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder, + ), + ) + 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`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder, + ), + ) + 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`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(A_USER_NAME), + eventSink = eventsRecorder, + ), + ) + val a11yClear = rule.activity.getString(CommonStrings.action_clear) + rule.onNodeWithContentDescription(a11yClear).performClick() + eventsRecorder.assertSingle( + LoginPasswordEvents.SetLogin("") + ) + } + + @Test + fun `changing password invokes the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder, + ), + ) + 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`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(password = A_PASSWORD), + eventSink = eventsRecorder, + ), + ) + rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) + // Show 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 = 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`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(password = A_PASSWORD), + eventSink = eventsRecorder, + ), + ) + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsNotEnabled() + } + + @Test + fun `when password is empty, continue button is not enabled`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(login = A_USER_NAME), + eventSink = eventsRecorder, + ), + ) + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsNotEnabled() + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on Continue sends expected event`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD), + eventSink = eventsRecorder, + ), + ) + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsEnabled() + rule.clickOn(CommonStrings.action_continue) + eventsRecorder.assertSingle( + LoginPasswordEvents.Submit + ) + } +} + +private fun AndroidComposeTestRule.setLoginPasswordView( + state: LoginPasswordState, + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + LoginPasswordView( + state = state, + onBackClick = onBackClick, + ) + } +} 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 new file mode 100644 index 00000000000..3e595284276 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -0,0 +1,259 @@ +/* + * 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. + */ + +package io.element.android.features.login.impl.screens.onboarding + +import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.OnBoardingConfig +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.features.login.impl.DefaultLoginUserStory +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever +import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever +import io.element.android.libraries.architecture.AsyncData +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.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 +import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_3 +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import io.element.android.libraries.matrix.test.A_LOGIN_HINT +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.oidc.api.OidcActionFlow +import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class OnBoardingPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + companion object { + private const val ACCOUNT_PROVIDER_FROM_LINK = AN_ACCOUNT_PROVIDER + private const val ACCOUNT_PROVIDER_FROM_CONFIG = AN_ACCOUNT_PROVIDER_2 + private const val ACCOUNT_PROVIDER_FROM_CONFIG_2 = AN_ACCOUNT_PROVIDER_3 + } + + @Test + fun `present - ensure initial conditions`() { + assertThat( + setOf( + ACCOUNT_PROVIDER_FROM_LINK, + ACCOUNT_PROVIDER_FROM_CONFIG, + ACCOUNT_PROVIDER_FROM_CONFIG_2, + ).size + ).isEqualTo(3) + } + + @Test + fun `present - initial state`() = runTest { + val buildMeta = aBuildMeta( + applicationName = "A", + productionApplicationName = "B", + desktopApplicationName = "C", + ) + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.QrCodeLogin.key to true), + buildMeta = buildMeta, + ) + val presenter = createPresenter( + buildMeta = buildMeta, + featureFlagService = featureFlagService, + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) }, + ), + rageshakeFeatureAvailability = { true }, + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.defaultAccountProvider).isNull() + assertThat(initialState.canLoginWithQrCode).isFalse() + assertThat(initialState.productionApplicationName).isEqualTo("B") + assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) + assertThat(initialState.canReportBug).isFalse() + assertThat(awaitItem().canLoginWithQrCode).isTrue() + } + } + + @Test + fun `present - clicking on version 7 times has no effect if rageshake not available`() = runTest { + val presenter = createPresenter( + rageshakeFeatureAvailability = { false }, + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.canReportBug).isFalse() + repeat(7) { + state.eventSink(OnBoardingEvents.OnVersionClick) + } + } + expectNoEvents() + } + } + + @Test + fun `present - clicking on version 7 times will reveal the report a problem button`() = runTest { + val presenter = createPresenter() + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.canReportBug).isFalse() + repeat(7) { + state.eventSink(OnBoardingEvents.OnVersionClick) + } + } + assertThat(awaitItem().canReportBug).isTrue() + } + } + + @Test + fun `present - opening the app using link with allowed account provider, and the app does not force account provider`() = runTest { + val presenter = createPresenter( + params = OnBoardingNode.Params( + accountProvider = ACCOUNT_PROVIDER_FROM_LINK, + loginHint = null, + ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.QrCodeLogin.key to true), + ), + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) }, + isAllowedToConnectToHomeserverResult = { true }, + ), + ) + presenter.test { + skipItems(3) + awaitItem().also { + assertThat(it.defaultAccountProvider).isEqualTo(ACCOUNT_PROVIDER_FROM_LINK) + assertThat(it.canLoginWithQrCode).isFalse() + assertThat(it.canCreateAccount).isFalse() + } + } + } + + @Test + fun `present - opening the app using link with not allowed account provider, and the app does not force account provider`() = runTest { + val presenter = createPresenter( + params = OnBoardingNode.Params( + accountProvider = ACCOUNT_PROVIDER_FROM_LINK, + loginHint = null, + ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.QrCodeLogin.key to true), + ), + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, + isAllowedToConnectToHomeserverResult = { false }, + ), + ) + presenter.test { + skipItems(1) + awaitItem().also { + assertThat(it.defaultAccountProvider).isNull() + assertThat(it.canLoginWithQrCode).isTrue() + assertThat(it.canCreateAccount).isFalse() + } + } + } + + @Test + fun `present - opening the app using link, and the app forces account provider`() = runTest { + val presenter = createPresenter( + params = OnBoardingNode.Params( + accountProvider = ACCOUNT_PROVIDER_FROM_LINK, + loginHint = null, + ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.QrCodeLogin.key to true), + ), + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG) }, + ) + ) + presenter.test { + skipItems(1) + awaitItem().also { + assertThat(it.defaultAccountProvider).isEqualTo(ACCOUNT_PROVIDER_FROM_CONFIG) + assertThat(it.canLoginWithQrCode).isTrue() + assertThat(it.canCreateAccount).isFalse() + } + } + } + + @Test + fun `present - default account provider - login and clear error`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + val presenter = createPresenter( + params = OnBoardingNode.Params( + accountProvider = A_HOMESERVER_URL, + loginHint = A_LOGIN_HINT, + ), + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = { true }, + ), + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + skipItems(3) + awaitItem().also { + assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL) + authenticationService.givenChangeServerError(AN_EXCEPTION) + it.eventSink(OnBoardingEvents.OnSignIn(A_HOMESERVER_URL)) + skipItems(1) // Loading + + // Check an error was returned + val submittedState = awaitItem() + assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + + // Assert the error is then cleared + submittedState.eventSink(OnBoardingEvents.ClearError) + val clearedState = awaitItem() + assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + } + } + } +} + +private fun createPresenter( + params: OnBoardingNode.Params = OnBoardingNode.Params(null, null), + buildMeta: BuildMeta = aBuildMeta(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), + enterpriseService: EnterpriseService = FakeEnterpriseService(), + rageshakeFeatureAvailability: () -> Boolean = { true }, + loginHelper: LoginHelper = createLoginHelper(), +) = OnBoardingPresenter( + params = params, + buildMeta = buildMeta, + featureFlagService = featureFlagService, + enterpriseService = enterpriseService, + rageshakeFeatureAvailability = rageshakeFeatureAvailability, + loginHelper = loginHelper, +) + +fun createLoginHelper( + oidcActionFlow: OidcActionFlow = FakeOidcActionFlow(), + authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), + defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), + webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), +): LoginHelper = LoginHelper( + oidcActionFlow = oidcActionFlow, + authenticationService = authenticationService, + defaultLoginUserStory = defaultLoginUserStory, + 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 new file mode 100644 index 00000000000..8ac42b4c936 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -0,0 +1,261 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.login.impl.screens.onboarding + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.test.AN_EXCEPTION +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 org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class OnboardingViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `when can create account - clicking on create account calls the expected callback`() { + val eventSink = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + canCreateAccount = true, + eventSink = eventSink, + ), + onCreateAccount = callback, + ) + rule.clickOn(R.string.screen_onboarding_sign_up) + } + } + + @Test + 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 -> + rule.setOnboardingView( + state = anOnBoardingState( + canLoginWithQrCode = true, + eventSink = eventSink, + ), + onSignInWithQrCode = callback, + ) + rule.clickOn(R.string.screen_onboarding_sign_in_with_qr_code) + } + } + + @Test + fun `when can login with QR code - clicking on sign in manually calls the expected callback - can search account provider`() { + `when can login with QR code - clicking on sign in manually calls the expected callback`( + mustChooseAccountProvider = false, + ) + } + + @Test + fun `when can login with QR code - clicking on sign in manually calls the expected callback - cannot search account provider`() { + `when can login with QR code - clicking on sign in manually calls the expected callback`( + mustChooseAccountProvider = true, + ) + } + + private fun `when can login with QR code - clicking on sign in manually calls the expected callback`( + mustChooseAccountProvider: Boolean, + ) { + val eventSink = EventsRecorder(expectEvents = false) + ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + canLoginWithQrCode = true, + mustChooseAccountProvider = mustChooseAccountProvider, + eventSink = eventSink, + ), + onSignIn = callback, + ) + rule.clickOn(R.string.screen_onboarding_sign_in_manually) + } + } + + @Test + fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - can search account provider`() { + `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( + mustChooseAccountProvider = false, + ) + } + + @Test + fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - cannot search account provider`() { + `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( + mustChooseAccountProvider = true, + ) + } + + private fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( + mustChooseAccountProvider: Boolean, + ) { + val eventSink = EventsRecorder(expectEvents = false) + ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + canLoginWithQrCode = false, + canCreateAccount = false, + mustChooseAccountProvider = mustChooseAccountProvider, + eventSink = eventSink, + ), + onSignIn = callback, + ) + rule.clickOn(CommonStrings.action_continue) + } + } + + @Test + fun `when sign in to pre defined account provider - clicking on button emits the expected event`() { + val eventSink = EventsRecorder() + rule.setOnboardingView( + state = anOnBoardingState( + defaultAccountProvider = "element.io", + eventSink = eventSink, + ), + ) + val buttonText = rule.activity.getString(R.string.screen_onboarding_sign_in_to, "element.io") + rule.onNodeWithText(buttonText).performClick() + eventSink.assertSingle(OnBoardingEvents.OnSignIn("element.io")) + } + + @Test + fun `when error is displayed - closing the dialog emits the expected event`() { + val eventSink = EventsRecorder() + rule.setOnboardingView( + state = anOnBoardingState( + defaultAccountProvider = "element.io", + loginMode = AsyncData.Failure(AN_EXCEPTION), + eventSink = eventSink, + ), + ) + rule.clickOn(CommonStrings.action_ok) + eventSink.assertSingle(OnBoardingEvents.ClearError) + } + + @Test + fun `clicking on report a problem calls the sign in callback`() { + val eventSink = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + canReportBug = true, + eventSink = eventSink, + ), + onReportProblem = callback, + ) + val text = rule.activity.getString(CommonStrings.common_report_a_problem) + rule.onNodeWithText(text).assertExists() + rule.clickOn(CommonStrings.common_report_a_problem) + } + } + + @Test + fun `cannot report a problem when the feature is disabled`() { + val eventSink = EventsRecorder(expectEvents = false) + rule.setOnboardingView( + state = anOnBoardingState( + canReportBug = false, + eventSink = eventSink, + ), + ) + 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`() { + val eventSink = EventsRecorder() + ensureCalledOnce { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + loginMode = AsyncData.Success(LoginMode.PasswordLogin), + eventSink = eventSink, + ), + onNeedLoginPassword = callback, + ) + } + eventSink.assertSingle(OnBoardingEvents.ClearError) + } + + @Test + fun `when success Oidc - the expected callback is invoked and the event is received`() { + val eventSink = EventsRecorder() + val oidcDetails = OidcDetails("aUrl") + ensureCalledOnceWithParam(oidcDetails) { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + loginMode = AsyncData.Success(LoginMode.Oidc(oidcDetails)), + eventSink = eventSink, + ), + onOidcDetails = callback, + ) + } + eventSink.assertSingle(OnBoardingEvents.ClearError) + } + + @Test + fun `when success AccountCreation - the expected callback is invoked and the event is received`() { + val eventSink = EventsRecorder() + val oidcDetails = OidcDetails("aUrl") + ensureCalledOnceWithParam(oidcDetails.url) { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), + eventSink = eventSink, + ), + onCreateAccountContinue = callback, + ) + } + eventSink.assertSingle(OnBoardingEvents.ClearError) + } + + private fun AndroidComposeTestRule.setOnboardingView( + state: OnBoardingState, + onSignInWithQrCode: () -> Unit = EnsureNeverCalled(), + onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(), + onCreateAccount: () -> Unit = EnsureNeverCalled(), + onReportProblem: () -> Unit = EnsureNeverCalled(), + onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), + onNeedLoginPassword: () -> Unit = EnsureNeverCalled(), + onLearnMoreClick: () -> Unit = EnsureNeverCalled(), + onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(), + ) { + setContent { + OnBoardingView( + state = state, + onSignInWithQrCode = onSignInWithQrCode, + onSignIn = onSignIn, + onCreateAccount = onCreateAccount, + onReportProblem = onReportProblem, + 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/scan/QrCodeScanPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt index 294f9c48c21..ccf2fe49888 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt @@ -11,12 +11,17 @@ 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.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException +import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginDataFactory import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -38,10 +43,22 @@ class QrCodeScanPresenterTest { @Test fun `present - scanned QR code successfully`() = runTest { - val presenter = createQrCodeScanPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory( + parseQrCodeLoginDataResult = { + Result.success( + FakeMatrixQrCodeLoginData( + serverNameResult = { "example.com" } + ) + ) + } + ) + val presenter = createQrCodeScanPresenter( + qrCodeLoginDataFactory = qrCodeLoginDataFactory, + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = { true }, + ) + ) + presenter.test { val initialState = awaitItem() initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf())) assertThat(awaitItem().isScanning).isFalse() @@ -50,6 +67,38 @@ class QrCodeScanPresenterTest { } } + @Test + fun `present - scanned QR code successfully, but homeserver not allowed`() = runTest { + val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory( + parseQrCodeLoginDataResult = { + Result.success( + FakeMatrixQrCodeLoginData( + serverNameResult = { "example.com" } + ) + ) + } + ) + val presenter = createQrCodeScanPresenter( + qrCodeLoginDataFactory = qrCodeLoginDataFactory, + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = { false }, + defaultHomeserverListResult = { listOf("element.io") }, + ) + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf())) + assertThat(awaitItem().isScanning).isFalse() + assertThat(awaitItem().authenticationAction.isLoading()).isTrue() + awaitItem().also { state -> + assertThat((state.authenticationAction.errorOrNull() as UnauthorizedAccountProviderException).unauthorisedAccountProviderTitle) + .isEqualTo("example.com") + assertThat((state.authenticationAction.errorOrNull() as UnauthorizedAccountProviderException).authorisedAccountProviderTitles) + .containsExactly("element.io") + } + } + } + @Test fun `present - scanned QR code failed and can be retried`() = runTest { val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory( @@ -103,9 +152,11 @@ class QrCodeScanPresenterTest { qrCodeLoginDataFactory: FakeMatrixQrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), qrCodeLoginManager: FakeQrCodeLoginManager = FakeQrCodeLoginManager(), + enterpriseService: EnterpriseService = FakeEnterpriseService(), ) = QrCodeScanPresenter( qrCodeLoginDataFactory = qrCodeLoginDataFactory, qrCodeLoginManager = qrCodeLoginManager, coroutineDispatchers = coroutineDispatchers, + enterpriseService = enterpriseService, ) } diff --git a/features/login/test/build.gradle.kts b/features/login/test/build.gradle.kts new file mode 100644 index 00000000000..d786420def4 --- /dev/null +++ b/features/login/test/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.login.test" +} + +dependencies { + implementation(projects.features.login.api) + implementation(projects.tests.testutils) +} diff --git a/features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginIntentResolver.kt b/features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginIntentResolver.kt new file mode 100644 index 00000000000..bcdbb816210 --- /dev/null +++ b/features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginIntentResolver.kt @@ -0,0 +1,20 @@ +/* + * 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.login.test + +import io.element.android.features.login.api.LoginIntentResolver +import io.element.android.features.login.api.LoginParams +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeLoginIntentResolver( + private val parseResult: (String) -> LoginParams? = { lambdaError() } +) : LoginIntentResolver { + override fun parse(uriString: String): LoginParams? { + return parseResult(uriString) + } +} diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt index 5d60d642fd2..17f67813ca4 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt @@ -16,7 +16,6 @@ interface LogoutEntryPoint : FeatureEntryPoint { fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder interface NodeBuilder { - fun onSuccessfulLogoutPendingAction(action: () -> Unit): NodeBuilder fun callback(callback: Callback): NodeBuilder fun build(): Node } diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutUseCase.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutUseCase.kt index 32058bd1540..6f265ec88c5 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutUseCase.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutUseCase.kt @@ -14,10 +14,8 @@ interface LogoutUseCase { /** * Log out the current user and then perform any needed cleanup tasks. * @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway. - * @return an optional URL. When the URL is there, it should be presented to the user after logout for - * Relying Party (RP) initiated logout on their account page. */ - suspend fun logout(ignoreSdkError: Boolean): String? + suspend fun logout(ignoreSdkError: Boolean) interface Factory { fun create(sessionId: String): LogoutUseCase diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt index 1e28d2d7fce..4e040b30bd1 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt @@ -11,6 +11,6 @@ import io.element.android.libraries.architecture.AsyncAction data class DirectLogoutState( val canDoDirectSignOut: Boolean, - val logoutAction: AsyncAction, + val logoutAction: AsyncAction, val eventSink: (DirectLogoutEvents) -> Unit, ) diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt index a02b1fc6198..993ad3cf0c8 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt @@ -17,13 +17,13 @@ open class DirectLogoutStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, + logoutAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (DirectLogoutEvents) -> Unit = {}, ) = DirectLogoutState( canDoDirectSignOut = canDoDirectSignOut, diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt index be5c2d4d523..4d6b6df3d23 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt @@ -11,8 +11,5 @@ import androidx.compose.runtime.Composable interface DirectLogoutView { @Composable - fun Render( - state: DirectLogoutState, - onSuccessLogout: (logoutUrlResult: String?) -> Unit - ) + fun Render(state: DirectLogoutState) } diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/util/Util.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/util/Util.kt deleted file mode 100644 index 87dff8e4f01..00000000000 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/util/Util.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.logout.api.util - -import android.app.Activity -import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab -import timber.log.Timber - -fun onSuccessLogout( - activity: Activity, - darkTheme: Boolean, - url: String?, -) { - Timber.d("Success logout with result url: $url") - url?.let { - activity.openUrlInChromeCustomTab(null, darkTheme, it) - } -} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt index 9abcd1304a3..0c651eb0774 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt @@ -27,15 +27,6 @@ class DefaultLogoutEntryPoint @Inject constructor() : LogoutEntryPoint { return this } - override fun onSuccessfulLogoutPendingAction(action: () -> Unit): LogoutEntryPoint.NodeBuilder { - plugins += object : LogoutNode.SuccessfulLogoutPendingAction, Plugin { - override fun onSuccessfulLogoutPendingAction() { - action() - } - } - return this - } - override fun build(): Node { return parentNode.createNode(buildContext, plugins) } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCase.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCase.kt index 8ede6ddcfa3..59906b1d742 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCase.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCase.kt @@ -19,9 +19,9 @@ class DefaultLogoutUseCase @Inject constructor( private val authenticationService: MatrixAuthenticationService, private val matrixClientProvider: MatrixClientProvider, ) : LogoutUseCase { - override suspend fun logout(ignoreSdkError: Boolean): String? { + override suspend fun logout(ignoreSdkError: Boolean) { val currentSession = authenticationService.getLatestSessionId() - return if (currentSession != null) { + if (currentSession != null) { matrixClientProvider.getOrRestore(currentSession) .getOrThrow() .logout(userInitiated = true, ignoreSdkError = true) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt index 140982f0e8c..382ef75b622 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt @@ -7,10 +7,8 @@ package io.element.android.features.logout.impl -import android.app.Activity 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 @@ -18,9 +16,7 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.compound.theme.ElementTheme import io.element.android.features.logout.api.LogoutEntryPoint -import io.element.android.features.logout.api.util.onSuccessLogout import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @@ -33,24 +29,12 @@ class LogoutNode @AssistedInject constructor( plugins().forEach { it.onChangeRecoveryKeyClick() } } - interface SuccessfulLogoutPendingAction : Plugin { - fun onSuccessfulLogoutPendingAction() - } - - private val customOnSuccessfulLogoutPendingAction = plugins().firstOrNull() - @Composable override fun View(modifier: Modifier) { val state = presenter.present() - val activity = LocalContext.current as Activity - val isDark = ElementTheme.isLightTheme.not() LogoutView( state = state, onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick, - onSuccessLogout = { - customOnSuccessfulLogoutPendingAction?.onSuccessfulLogoutPendingAction() - onSuccessLogout(activity, isDark, it) - }, onBackClick = ::navigateUp, modifier = modifier, ) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt index 99efefcedf7..3d1cacd89ca 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt @@ -15,6 +15,7 @@ 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 io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -25,6 +26,7 @@ import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject @@ -35,7 +37,7 @@ class LogoutPresenter @Inject constructor( @Composable override fun present(): LogoutState { val localCoroutineScope = rememberCoroutineScope() - val logoutAction: MutableState> = remember { + val logoutAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } @@ -44,6 +46,16 @@ class LogoutPresenter @Inject constructor( } .collectAsState(initial = BackupUploadState.Unknown) + var waitingForALongTime by remember { mutableStateOf(false) } + LaunchedEffect(backupUploadState) { + if (backupUploadState is BackupUploadState.Waiting) { + delay(2_000) + waitingForALongTime = true + } else { + waitingForALongTime = false + } + } + val isLastDevice by encryptionService.isLastDevice.collectAsState() val backupState by encryptionService.backupStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() @@ -79,6 +91,7 @@ class LogoutPresenter @Inject constructor( doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(), recoveryState = recoveryState, backupUploadState = backupUploadState, + waitingForALongTime = waitingForALongTime, logoutAction = logoutAction.value, eventSink = ::handleEvents ) @@ -91,7 +104,7 @@ class LogoutPresenter @Inject constructor( } private fun CoroutineScope.logout( - logoutAction: MutableState>, + logoutAction: MutableState>, ignoreSdkError: Boolean, ) = launch { suspend { diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt index 3e889c0962c..584d7a0ac93 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt @@ -18,6 +18,7 @@ data class LogoutState( val doesBackupExistOnServer: Boolean, val recoveryState: RecoveryState, val backupUploadState: BackupUploadState, - val logoutAction: AsyncAction, + val waitingForALongTime: Boolean, + val logoutAction: AsyncAction, val eventSink: (LogoutEvents) -> Unit, ) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt index 456ec6ce164..a55a9afb96f 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt @@ -29,6 +29,15 @@ open class LogoutStateProvider : PreviewParameterProvider { aLogoutState(isLastDevice = true, recoveryState = RecoveryState.DISABLED), // Last session no backup aLogoutState(isLastDevice = true, backupState = BackupState.UNKNOWN, doesBackupExistOnServer = false), + aLogoutState( + isLastDevice = false, + backupUploadState = BackupUploadState.Waiting, + ), + aLogoutState( + isLastDevice = false, + backupUploadState = BackupUploadState.Waiting, + waitingForALongTime = true, + ), ) } @@ -38,7 +47,8 @@ fun aLogoutState( doesBackupExistOnServer: Boolean = true, recoveryState: RecoveryState = RecoveryState.ENABLED, backupUploadState: BackupUploadState = BackupUploadState.Unknown, - logoutAction: AsyncAction = AsyncAction.Uninitialized, + waitingForALongTime: Boolean = false, + logoutAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (LogoutEvents) -> Unit = {}, ) = LogoutState( isLastDevice = isLastDevice, @@ -46,6 +56,7 @@ fun aLogoutState( doesBackupExistOnServer = doesBackupExistOnServer, recoveryState = recoveryState, backupUploadState = backupUploadState, + waitingForALongTime = waitingForALongTime, logoutAction = logoutAction, eventSink = eventSink, ) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt index 2f54294ea83..321879b0096 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt @@ -45,7 +45,6 @@ fun LogoutView( state: LogoutState, onChangeRecoveryKeyClick: () -> Unit, onBackClick: () -> Unit, - onSuccessLogout: (logoutUrlResult: String?) -> Unit, modifier: Modifier = Modifier, ) { val eventSink = state.eventSink @@ -80,9 +79,6 @@ fun LogoutView( onDismissDialog = { eventSink(LogoutEvents.CloseDialogs) }, - onSuccessLogout = { - onSuccessLogout(it) - }, ) } @@ -147,24 +143,41 @@ private fun ColumnScope.Buttons( @Composable private fun Content( state: LogoutState, + modifier: Modifier = Modifier, ) { - if (state.backupUploadState is BackupUploadState.Uploading) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 60.dp, start = 20.dp, end = 20.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - progress = { state.backupUploadState.backedUpCount.toFloat() / state.backupUploadState.totalCount.toFloat() }, - trackColor = ElementTheme.colors.progressIndicatorTrackColor, - ) - Text( - modifier = Modifier.align(Alignment.End), - text = "${state.backupUploadState.backedUpCount} / ${state.backupUploadState.totalCount}", - style = ElementTheme.typography.fontBodySmRegular, - ) + Column( + modifier = modifier + .fillMaxWidth() + .padding(top = 60.dp, start = 20.dp, end = 20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + when (state.backupUploadState) { + is BackupUploadState.Uploading -> { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + progress = { state.backupUploadState.backedUpCount.toFloat() / state.backupUploadState.totalCount.toFloat() }, + trackColor = ElementTheme.colors.progressIndicatorTrackColor, + ) + Text( + modifier = Modifier.align(Alignment.End), + text = "${state.backupUploadState.backedUpCount} / ${state.backupUploadState.totalCount}", + style = ElementTheme.typography.fontBodySmRegular, + ) + } + BackupUploadState.Waiting -> { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + trackColor = ElementTheme.colors.progressIndicatorTrackColor, + ) + if (state.waitingForALongTime) { + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = stringResource(CommonStrings.common_please_check_internet_connection), + style = ElementTheme.typography.fontBodySmRegular, + ) + } + } + else -> Unit } } } @@ -177,7 +190,6 @@ internal fun LogoutViewPreview( LogoutView( state, onChangeRecoveryKeyClick = {}, - onSuccessLogout = {}, onBackClick = {}, ) } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt index 3ec8ed3c397..e1cf8aa656a 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt @@ -23,10 +23,7 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView { @Composable - override fun Render( - state: DirectLogoutState, - onSuccessLogout: (logoutUrlResult: String?) -> Unit, - ) { + override fun Render(state: DirectLogoutState) { val eventSink = state.eventSink LogoutActionDialog( state.logoutAction, @@ -39,9 +36,6 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView { onDismissDialog = { eventSink(DirectLogoutEvents.CloseDialogs) }, - onSuccessLogout = { - onSuccessLogout(it) - }, ) } } @@ -51,8 +45,5 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView { internal fun DefaultDirectLogoutViewPreview( @PreviewParameter(DirectLogoutStateProvider::class) state: DirectLogoutState, ) = ElementPreview { - DefaultDirectLogoutView().Render( - state = state, - onSuccessLogout = {}, - ) + DefaultDirectLogoutView().Render(state = state) } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt index 63d1484655a..4694b6dc2fb 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt @@ -35,7 +35,7 @@ class DirectLogoutPresenter @Inject constructor( override fun present(): DirectLogoutState { val localCoroutineScope = rememberCoroutineScope() - val logoutAction: MutableState> = remember { + val logoutAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } @@ -70,7 +70,7 @@ class DirectLogoutPresenter @Inject constructor( } private fun CoroutineScope.logout( - logoutAction: MutableState>, + logoutAction: MutableState>, ignoreSdkError: Boolean, ) = launch { suspend { diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt index 9998a1c930e..4b8e23ab4ed 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt @@ -8,9 +8,6 @@ package io.element.android.features.logout.impl.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.res.stringResource import io.element.android.features.logout.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -20,11 +17,10 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun LogoutActionDialog( - state: AsyncAction, + state: AsyncAction, onConfirmClick: () -> Unit, onForceLogoutClick: () -> Unit, onDismissDialog: () -> Unit, - onSuccessLogout: (String?) -> Unit, ) { when (state) { AsyncAction.Uninitialized -> @@ -44,11 +40,6 @@ fun LogoutActionDialog( onRetry = onForceLogoutClick, onDismiss = onDismissDialog, ) - is AsyncAction.Success -> { - val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout) - LaunchedEffect(state) { - latestOnSuccessLogout(state.data) - } - } + is AsyncAction.Success -> Unit } } diff --git a/features/logout/impl/src/main/res/values-cy/translations.xml b/features/logout/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..d476bb69c94 --- /dev/null +++ b/features/logout/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,18 @@ + + + "Ydych chi\'n siŵr eich bod am allgofnodi?" + "Allgofnodi" + "Allgofnodi" + "Yn allgofnodi…" + "Rydych chi ar fin allgofnodi o\'ch sesiwn ddiwethaf. Os byddwch yn allgofnodi nawr, byddwch yn colli mynediad i\'ch negeseuon wedi\'u hamgryptio." + "Rydych chi wedi diffodd copïo wrth gefn" + "Roedd eich allweddi yn dal i gael eu copïo wrth gefn pan aethoch all-lein. Ailgysylltwch fel bod modd gwneud copi wrth gefn o\'ch allweddi cyn allgofnodi." + "Mae eich allweddi yn dal i gael eu copïo wrth gefn" + "Arhoswch iddo gael ei gwblhau cyn allgofnodi." + "Mae eich allweddi yn dal i gael eu copïo wrth gefn" + "Allgofnodi" + "Rydych chi ar fin allgofnodi o\'ch sesiwn ddiwethaf. Os byddwch yn allgofnodi nawr, byddwch yn colli mynediad i\'ch negeseuon wedi\'u hamgryptio." + "Adfer heb ei osod" + "Rydych chi ar fin allgofnodi o\'ch sesiwn ddiwethaf. Os ydych chi\'n allgofnodi nawr, efallai y byddwch chi\'n colli mynediad i\'ch negeseuon wedi\'u hamgryptio." + "Ydych chi wedi cadw\'ch allwedd adfer?" + diff --git a/features/logout/impl/src/main/res/values-da/translations.xml b/features/logout/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..bbf6a49399f --- /dev/null +++ b/features/logout/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,18 @@ + + + "Er du sikker på, at du vil logge ud?" + "Log ud" + "Log ud" + "Logger ud…" + "Du er ved at logge ud af din sidste session. Hvis du logger ud nu, mister du adgangen til dine krypterede meddelelser." + "Du har slået sikkerhedskopiering fra" + "Dine nøgler blev stadig sikkerhedskopieret, da du gik offline. Opret forbindelse igen, så dine nøgler kan sikkerhedskopieres, før du logger ud." + "Dine nøgler bliver stadig sikkerhedskopieret" + "Vent på, at dette er fuldført, før du logger ud." + "Dine nøgler bliver stadig sikkerhedskopieret" + "Log ud" + "Du er ved at logge ud af din sidste session. Hvis du logger af nu, mister du adgangen til dine krypterede meddelelser." + "Gendannelse er ikke konfigureret" + "Du er ved at logge ud af din sidste session. Hvis du logger af nu, kan du miste adgangen til dine krypterede meddelelser." + "Har du gemt din gendannelsesnøgle?" + 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 eeec111177a..a9a74a965dc 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 dich abmelden willst?" + "Möchten Sie sich wirklich abmelden?" "Abmelden" "Abmelden" "Abmelden…" - "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten." - "Du hast das Backup deaktiviert." - "Deine Schlüssel wurden noch gesichert, als du offline gegangen bist. Stelle die Verbindung wieder her, damit deine Schlüssel gesichert werden können, bevor du dich abmeldest." + "Sie sind dabei, sich von Ihrer letzten Sitzung abzumelden. Wenn Sie sich jetzt abmelden, verlieren Sie den Zugriff auf Ihre verschlüsselten Nachrichten." + "Sie haben das Backup deaktiviert" + "Ihre Schlüssel wurden noch gesichert, als Sie offline gingen. Stellen Sie die Verbindung wieder her, damit Ihre Schlüssel fertig gesichert werden können, bevor Sie sich abmelden." "Deine Schlüssel werden noch gesichert" - "Bitte warte, bis der Vorgang abgeschlossen ist, bevor du dich abmeldest." + "Bitte warten Sie, bis der Vorgang abgeschlossen ist, bevor Sie sich abmelden." "Deine Schlüssel werden noch gesichert" "Abmelden" - "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten." + "Sie sind dabei, sich von Ihrer letzten Sitzung abzumelden. Wenn Sie sich jetzt abmelden, verlieren Sie den Zugriff auf Ihre 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?" + "Sie sind dabei, sich von Ihrer letzten Sitzung abzumelden. Wenn Sie sich jetzt abmelden, verlieren Sie möglicherweise den Zugriff auf Ihre verschlüsselten Nachrichten." + "Haben Sie Ihren Wiederherstellungsschlüssel gespeichert?" diff --git a/features/logout/impl/src/main/res/values-eu/translations.xml b/features/logout/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..9d25ef2ba63 --- /dev/null +++ b/features/logout/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,12 @@ + + + "Ziur saioa amaitu nahi duzula?" + "Amaitu saioa" + "Amaitu saioa" + "Saioa amaitzen…" + "Babeskopia desaktibatu duzu" + "Itxaron eragiketa amaitu arte saioa amaitu baino lehen." + "Amaitu saioa" + "Berreskuratzea ez da konfiguratu" + "Gorde al duzu berreskuratze-gakoa?" + 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 46cfa54539c..4bf6be1b895 100644 --- a/features/logout/impl/src/main/res/values-fa/translations.xml +++ b/features/logout/impl/src/main/res/values-fa/translations.xml @@ -9,6 +9,7 @@ "لطفاً پیش از خروج منتظر پایانش شوید." "کلیدهایتان هنوز در حال پشتیبان گیریند" "خروج" + "شما در آستانه خروج از آخرین جلسه خود هستید. اگر اکنون از سیستم خارج شوید، دسترسی به پیام های رمزگذاری شده تان را از دست خواهید داد." "بازگردانی برپا نشده" "کلید بازیابیتان را ذخیره کرده‌اید؟" diff --git a/features/logout/impl/src/main/res/values-lt/translations.xml b/features/logout/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..27bfab20070 --- /dev/null +++ b/features/logout/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,5 @@ + + + "Ar tikrai norite atsijungti?" + "Atsijungiama…" + diff --git a/features/logout/impl/src/main/res/values-nb/translations.xml b/features/logout/impl/src/main/res/values-nb/translations.xml index 4e02108299c..0e4a7352390 100644 --- a/features/logout/impl/src/main/res/values-nb/translations.xml +++ b/features/logout/impl/src/main/res/values-nb/translations.xml @@ -4,5 +4,15 @@ "Logg ut" "Logg ut" "Logger ut…" + "Du er i ferd med å logge av din siste sesjon. Hvis du logger av nå, mister du tilgangen til de krypterte meldingene dine." + "Du har slått av sikkerhetskopiering" + "Nøklene dine ble fortsatt sikkerhetskopiert da du koblet fra. Koble til igjen, slik at nøklene dine kan sikkerhetskopieres før du logger av." + "Nøklene dine blir fortsatt sikkerhetskopiert" + "Vent til dette er fullført før du logger av." + "Nøklene dine blir fortsatt sikkerhetskopiert" "Logg ut" + "Du er i ferd med å logge ut av din siste sesjon. Hvis du logger ut nå, mister du tilgangen til de krypterte meldingene dine." + "Gjenoppretting ikke konfigurert" + "Du er i ferd med å logge av din siste sesjon. Hvis du logger av nå, kan du miste tilgangen til de krypterte meldingene dine." + "Har du lagret gjenopprettingsnøkkelen din?" diff --git a/features/logout/impl/src/main/res/values-pt-rBR/translations.xml b/features/logout/impl/src/main/res/values-pt-rBR/translations.xml index d5787232bfa..b12a2aa470d 100644 --- a/features/logout/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/logout/impl/src/main/res/values-pt-rBR/translations.xml @@ -4,10 +4,15 @@ "Sair" "Sair" "Saindo…" + "Você está prestes a sair da sua última sessão. Se você sair agora, perderá o acesso às suas mensagens criptografadas." "Você desativou o backup" + "O backup das suas chaves ainda estava sendo feito quando você ficou off-line. Reconecte-se para que você possa fazer o backup de suas chaves antes de sair." "O backup das suas chaves ainda está em andamento" + "Aguarde até que isso seja concluído antes de sair." "O backup das suas chaves ainda está em andamento" "Sair" + "Você está prestes a sair da sua última sessão. Se você sair agora, perderá o acesso às suas mensagens criptografadas." "A recuperação não está configurada" + "Você está prestes a sair da sua última sessão. Se você sair agora, poderá perder o acesso às suas mensagens criptografadas." "Você salvou sua chave de recuperação?" diff --git a/features/logout/impl/src/main/res/values-tr/translations.xml b/features/logout/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..6e1ac9cb609 --- /dev/null +++ b/features/logout/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,18 @@ + + + "Çıkış yapmak istediğinize emin misiniz?" + "Oturumu kapat" + "Oturumu kapat" + "Oturum kapatılıyor…" + "Son oturumunuzdan çıkmak üzeresiniz. Şimdi çıkış yaparsanız, şifrelenmiş mesajlarınıza erişiminizi kaybedersiniz." + "Yedeklemeyi kapattınız" + "Çevrimdışı olduğunuzda anahtarlarınız hala yedekleniyordu. Oturumu kapatmadan önce anahtarlarınızın yedeklenebilmesi için yeniden bağlanın." + "Anahtarlarınız hala yedekleniyor" + "Lütfen oturumu kapatmadan önce bunun tamamlanmasını bekleyin." + "Anahtarlarınız hala yedekleniyor" + "Oturumu kapat" + "Son oturumunuzdan çıkmak üzeresiniz. Şimdi çıkış yaparsanız şifrelenmiş mesajlarınıza erişiminizi kaybedersiniz." + "Kurtarma ayarlanmadı" + "Son oturumunuzdan çıkmak üzeresiniz. Şimdi oturumu kapatırsanız şifrelenmiş mesajlarınıza erişiminizi kaybedebilirsiniz." + "Kurtarma anahtarınızı kaydettiniz mi?" + diff --git a/features/logout/impl/src/main/res/values-ur/translations.xml b/features/logout/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..37c8c57be54 --- /dev/null +++ b/features/logout/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,18 @@ + + + "کیا آپ واقعی خارج ہونا چاہتے ہیں؟" + "خارج ہوں" + "خارج ہوں" + "خارج ہورہاہے…" + "آپ اپنے آخری جلسے سے خارج ہونے والے ہیں۔ اگر آپ ابھی خارج ہوجاتے ہیں تو آپ اپنے مرموز کردہ پیغامات تک رسائی سے محروم ہو جائیں گے۔" + "آپنے پشتارہ بند کردیا ہے" + "جب آپ پرے خط تھے تب بھی آپ کی کلیدوں کا پشتارہ کیا جا رہا تھا۔ دوبارہ جڑیں تاکہ خارج ہونے سے پہلے آپ کی کلیدوں کا پشتارہ کیا جا سکے۔" + "آپکی کلیدوں کا ابھی بھی پشتارہ کیا جا رہا ہے۔" + "برائے مہربانی خارج ہونے سے پہلے اسکے مکمل ہونے کا انتظار کریں" + "آپکی کلیدوں کا ابھی بھی پشتارہ کیا جا رہا ہے۔" + "خارج ہوں" + "آپ اپنے آخری جلسے سے خارج ہونے والے ہیں۔ اگر آپ ابھی خارج ہوجاتے ہیں تو آپ اپنے مرموز کردہ پیغامات تک رسائی سے محروم ہو جائیں گے۔" + "بازیابی غیر مرتب" + "آپ اپنے آخری جلسے سے خارج ہونے والے ہیں۔ اگر آپ ابھی خارج ہوجاتے ہیں تو ہوسکتا ہے کہ آپ اپنے مرموز کردہ پیغامات تک رسائی سے محروم ہو جائیں گے۔" + "کیا آپ نے اپنی بازیابی کی کلید محفوظ کی ہے؟" + diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt index d054556f87e..3a309ab7b93 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState -import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.tests.testutils.WarmUpRule @@ -44,6 +44,7 @@ class LogoutPresenterTest { assertThat(initialState.doesBackupExistOnServer).isTrue() assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN) assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) + assertThat(initialState.waitingForALongTime).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } } @@ -66,6 +67,34 @@ class LogoutPresenterTest { } } + @Test + fun `present - initial state - waiting a long time`() = runTest { + val encryptionService = FakeEncryptionService() + encryptionService.givenWaitForBackupUploadSteadyStateFlow( + flow { + emit(BackupUploadState.Waiting) + delay(3_000) + } + ) + val presenter = createLogoutPresenter( + encryptionService = encryptionService + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.waitingForALongTime).isFalse() + assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) + val waitingState = awaitItem() + assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting) + assertThat(initialState.waitingForALongTime).isFalse() + skipItems(1) + val waitingALongTimeState = awaitItem() + assertThat(waitingALongTimeState.backupUploadState).isEqualTo(BackupUploadState.Waiting) + assertThat(waitingALongTimeState.waitingForALongTime).isTrue() + } + } + @Test fun `present - initial state - backing up`() = runTest { val encryptionService = FakeEncryptionService() @@ -136,7 +165,7 @@ class LogoutPresenterTest { fun `present - logout with error then cancel`() = runTest { val matrixClient = FakeMatrixClient().apply { logoutLambda = { _, _ -> - throw A_THROWABLE + throw AN_EXCEPTION } } val presenter = createLogoutPresenter( @@ -153,7 +182,7 @@ class LogoutPresenterTest { val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val errorState = awaitItem() - assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE)) + assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) errorState.eventSink.invoke(LogoutEvents.CloseDialogs) val finalState = awaitItem() assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized) @@ -165,9 +194,7 @@ class LogoutPresenterTest { val matrixClient = FakeMatrixClient().apply { logoutLambda = { ignoreSdkError, _ -> if (!ignoreSdkError) { - throw A_THROWABLE - } else { - null + throw AN_EXCEPTION } } } @@ -185,7 +212,7 @@ class LogoutPresenterTest { val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val errorState = awaitItem() - assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE)) + assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) errorState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = true)) val loadingState2 = awaitItem() assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) 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 4d078f0b960..00e294f7d1b 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 @@ -15,11 +15,9 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.testtags.TestTags 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 io.element.android.tests.testutils.pressTag import org.junit.Rule @@ -96,21 +94,6 @@ class LogoutViewTest { eventsRecorder.assertSingle(LogoutEvents.CloseDialogs) } - @Test - fun `success logout invoke onSuccessLogout`() { - val data = "data" - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnceWithParam(data) { callback -> - rule.setLogoutView( - aLogoutState( - logoutAction = AsyncAction.Success(data), - eventSink = eventsRecorder - ), - onSuccessLogout = callback, - ) - } - } - @Test fun `last session setting button invoke onChangeRecoveryKeyClicked`() { val eventsRecorder = EventsRecorder(expectEvents = false) @@ -131,14 +114,12 @@ private fun AndroidComposeTestRule.setLogou state: LogoutState, onChangeRecoveryKeyClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(), - onSuccessLogout: (logoutUrlResult: String?) -> Unit = EnsureNeverCalledWithParam() ) { setContent { LogoutView( state = state, onChangeRecoveryKeyClick = onChangeRecoveryKeyClick, onBackClick = onBackClick, - onSuccessLogout = onSuccessLogout, ) } } 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 24e00e2998c..3581f682fa9 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 @@ -16,10 +16,8 @@ import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn -import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBackKey import org.junit.Ignore import org.junit.Rule @@ -96,45 +94,12 @@ class DefaultDirectLogoutViewTest { rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) } - - @Test - fun `success logout invoke expected callback and sends expected Event`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnceWithParam(null) { callback -> - rule.setDefaultDirectLogoutView( - state = aDirectLogoutState( - logoutAction = AsyncAction.Success(null), - eventSink = eventsRecorder, - ), - onSuccessLogout = callback - ) - } - } - - @Test - fun `success logout invoke expected callback and sends expected Event with data`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - val data = "data" - ensureCalledOnceWithParam(data) { callback -> - rule.setDefaultDirectLogoutView( - state = aDirectLogoutState( - logoutAction = AsyncAction.Success(data), - eventSink = eventsRecorder, - ), - onSuccessLogout = callback - ) - } - } } private fun AndroidComposeTestRule.setDefaultDirectLogoutView( state: DirectLogoutState, - onSuccessLogout: (String?) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { - DefaultDirectLogoutView().Render( - state, - onSuccessLogout = onSuccessLogout, - ) + DefaultDirectLogoutView().Render(state) } } diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt index 158ce0c0ea7..d5dff82c5ca 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt @@ -17,7 +17,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService -import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.tests.testutils.WarmUpRule @@ -117,7 +117,7 @@ class DirectLogoutPresenterTest { fun `present - logout with error then cancel`() = runTest { val matrixClient = FakeMatrixClient().apply { logoutLambda = { _, _ -> - throw A_THROWABLE + throw AN_EXCEPTION } } val presenter = createDirectLogoutPresenter( @@ -134,7 +134,7 @@ class DirectLogoutPresenterTest { val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val errorState = awaitItem() - assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE)) + assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) errorState.eventSink.invoke(DirectLogoutEvents.CloseDialogs) val finalState = awaitItem() assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized) @@ -146,9 +146,7 @@ class DirectLogoutPresenterTest { val matrixClient = FakeMatrixClient().apply { logoutLambda = { ignoreSdkError, _ -> if (!ignoreSdkError) { - throw A_THROWABLE - } else { - null + throw AN_EXCEPTION } } } @@ -166,7 +164,7 @@ class DirectLogoutPresenterTest { val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val errorState = awaitItem() - assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE)) + assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) errorState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = true)) val loadingState2 = awaitItem() assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) diff --git a/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt b/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt index dadccf1b26d..e71266b596b 100644 --- a/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt +++ b/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt @@ -12,9 +12,9 @@ import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask class FakeLogoutUseCase( - var logoutLambda: (Boolean) -> String? = { lambdaError() } + var logoutLambda: (Boolean) -> Unit = { lambdaError() } ) : LogoutUseCase { - override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask { + override suspend fun logout(ignoreSdkError: Boolean) = simulateLongTask { logoutLambda(ignoreSdkError) } } diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/HtmlConverterProvider.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/HtmlConverterProvider.kt index 5071613e581..607283792f0 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/HtmlConverterProvider.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/HtmlConverterProvider.kt @@ -8,12 +8,11 @@ package io.element.android.features.messages.api.timeline import androidx.compose.runtime.Composable -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.wysiwyg.utils.HtmlConverter interface HtmlConverterProvider { @Composable - fun Update(currentUserId: UserId) + fun Update() fun provide(): HtmlConverter } diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 94edaf5c9f8..00d8e3f7655 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation(libs.telephoto.zoomableimage) implementation(libs.matrix.emojibase.bindings) implementation(projects.features.knockrequests.api) + implementation(projects.features.roommembermoderation.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -75,6 +76,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) + testImplementation(projects.features.location.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.messages.test) testImplementation(projects.services.analytics.test) @@ -92,7 +94,6 @@ dependencies { testImplementation(libs.test.mockk) testImplementation(libs.test.robolectric) testImplementation(projects.features.poll.test) - testImplementation(projects.features.poll.impl) testImplementation(libs.androidx.compose.ui.test.junit) testImplementation(projects.libraries.eventformatter.test) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt deleted file mode 100644 index 21b8d033813..00000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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. - */ - -@file:OptIn(ExperimentalMaterial3Api::class) - -package io.element.android.features.messages.impl - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SheetValue -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.Measurable -import androidx.compose.ui.layout.SubcomposeLayout -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min -import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold -import io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberStandardBottomSheetState -import kotlin.math.roundToInt - -/** - * A [BottomSheetScaffold] that allows the sheet to be expanded the screen height - * of the sheet contents. - * - * @param content The main content. - * @param sheetContent The sheet content. - * @param sheetDragHandle The drag handle for the sheet. - * @param sheetSwipeEnabled Whether the sheet can be swiped. This value is ignored and swipe is disabled if the sheet content overflows. - * @param sheetShape The shape of the sheet. - * @param sheetTonalElevation The tonal elevation of the sheet. - * @param sheetShadowElevation The shadow elevation of the sheet. - * @param modifier The modifier for the layout. - * @param sheetContentKey The key for the sheet content. If the key changes, the sheet will be remeasured. - */ -@Suppress( - "ContentTrailingLambda", - // False positive - "MultipleEmitters", -) -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun ExpandableBottomSheetScaffold( - content: @Composable (padding: PaddingValues) -> Unit, - // False positive, it's not being reused - @Suppress("ContentSlotReused") - sheetContent: @Composable (subcomposing: Boolean) -> Unit, - sheetDragHandle: @Composable () -> Unit, - sheetSwipeEnabled: Boolean, - sheetShape: Shape, - sheetTonalElevation: Dp, - sheetShadowElevation: Dp, - modifier: Modifier = Modifier, - sheetContentKey: Int? = null, -) { - val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberStandardBottomSheetState( - initialValue = SheetValue.PartiallyExpanded, - skipHiddenState = true, - ) - ) - - // If the content overflows, we disable swipe to prevent the sheet from intercepting - // scroll events of the sheet content. - var contentOverflows by remember { mutableStateOf(false) } - val sheetSwipeEnabledIfPossible by remember(contentOverflows, sheetSwipeEnabled) { - derivedStateOf { - sheetSwipeEnabled && !contentOverflows - } - } - - LaunchedEffect(sheetSwipeEnabledIfPossible) { - if (!sheetSwipeEnabledIfPossible) { - scaffoldState.bottomSheetState.partialExpand() - } - } - - @Composable - fun Scaffold( - sheetContent: @Composable () -> Unit, - dragHandle: @Composable () -> Unit, - peekHeight: Dp, - ) { - BottomSheetScaffold( - modifier = Modifier, - scaffoldState = scaffoldState, - sheetPeekHeight = peekHeight, - sheetSwipeEnabled = sheetSwipeEnabledIfPossible, - sheetDragHandle = dragHandle, - sheetShape = sheetShape, - content = content, - sheetContent = { sheetContent() }, - sheetTonalElevation = sheetTonalElevation, - sheetShadowElevation = sheetShadowElevation, - ) - } - - SubcomposeLayout( - modifier = modifier, - measurePolicy = { constraints: Constraints -> - val sheetContentSub = subcompose(Slot.SheetContent(sheetContentKey)) { sheetContent(true) }.map { - it.measure(Constraints(maxWidth = constraints.maxWidth)) - }.first() - val dragHandleSub = subcompose(Slot.DragHandle) { sheetDragHandle() }.map { - it.measure(Constraints(maxWidth = constraints.maxWidth)) - }.firstOrNull() - val dragHandleHeight = dragHandleSub?.height?.toDp() ?: 0.dp - - val maxHeight = constraints.maxHeight.toDp() - val contentHeight = sheetContentSub.height.toDp() + dragHandleHeight - - contentOverflows = contentHeight > maxHeight - - val peekHeight = min( - // prevent the sheet from expanding beyond the screen - maxHeight, - contentHeight - ) - - val scaffoldPlaceables = subcompose(Slot.Scaffold) { - Scaffold({ - Layout( - modifier = Modifier.fillMaxHeight(), - measurePolicy = { measurables, constraints -> - val constraintHeight = constraints.maxHeight - val offset = scaffoldState.bottomSheetState.getIntOffset() ?: 0 - val height = Integer.max(0, constraintHeight - offset) - val top = measurables[0].measure( - constraints.copy( - minHeight = height, - maxHeight = height - ) - ) - layout(constraints.maxWidth, constraints.maxHeight) { - top.place(x = 0, y = 0) - } - }, - content = { sheetContent(false) } - ) - }, sheetDragHandle, peekHeight) - }.map { measurable: Measurable -> - measurable.measure(constraints) - } - val scaffoldPlaceable = scaffoldPlaceables.first() - layout(constraints.maxWidth, constraints.maxHeight) { - scaffoldPlaceable.place(0, 0) - } - } - ) -} - -private fun CustomSheetState.getIntOffset(): Int? = try { - requireOffset().roundToInt() -} catch (e: IllegalStateException) { - null -} - -private sealed interface Slot { - data class SheetContent(val key: Int?) : Slot - data object DragHandle : Slot - data object Scaffold : Slot -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt index 8d4d597502c..2e035b62995 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt @@ -10,11 +10,13 @@ package io.element.android.features.messages.impl import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId +import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface MessagesEvents { data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvents data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents + data class OnUserClicked(val user: MatrixUser) : MessagesEvents data object Dismiss : MessagesEvents } 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 6b84423d44f..8f97b6bc195 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 @@ -28,6 +28,7 @@ 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.location.api.Location +import io.element.android.features.location.api.LocationService import io.element.android.features.location.api.SendLocationEntryPoint import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint @@ -45,7 +46,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent 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.duration @@ -57,6 +57,7 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.overlay.Overlay import io.element.android.libraries.architecture.overlay.operation.hide import io.element.android.libraries.architecture.overlay.operation.show +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.dateformatter.api.toHumanReadableDuration @@ -68,21 +69,24 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.permalink.PermalinkData -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.alias.matches import io.element.android.libraries.matrix.api.room.joinedRoomMembers +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo -import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache +import io.element.android.libraries.matrix.ui.messages.RoomNamesCache import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint -import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme +import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanUpdater import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme +import io.element.android.libraries.textcomposer.mentions.MentionSpanUpdater import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) @@ -96,13 +100,17 @@ class MessagesFlowNode @AssistedInject constructor( private val elementCallEntryPoint: ElementCallEntryPoint, private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val analyticsService: AnalyticsService, - private val room: MatrixRoom, + private val locationService: LocationService, + private val room: BaseRoom, private val roomMemberProfilesCache: RoomMemberProfilesCache, + private val roomNamesCache: RoomNamesCache, + private val mentionSpanUpdater: MentionSpanUpdater, private val mentionSpanTheme: MentionSpanTheme, private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider, private val timelineController: TimelineController, private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint, private val dateFormatter: DateFormatter, + private val coroutineDispatchers: CoroutineDispatchers, ) : BaseFlowNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialTarget.toNavTarget(), @@ -170,13 +178,29 @@ class MessagesFlowNode @AssistedInject constructor( timelineController.close() } ) + setupCacheUpdaters() + + pinnedEventsTimelineProvider.launchIn(lifecycleScope) + } + + private fun setupCacheUpdaters() { room.membersStateFlow .onEach { membersState -> - roomMemberProfilesCache.replace(membersState.joinedRoomMembers()) + withContext(coroutineDispatchers.computation) { + roomMemberProfilesCache.replace(membersState.joinedRoomMembers()) + } } .launchIn(lifecycleScope) - pinnedEventsTimelineProvider.launchIn(lifecycleScope) + matrixClient.roomListService + .allRooms + .summaries + .onEach { + withContext(coroutineDispatchers.computation) { + roomNamesCache.replace(it) + } + } + .launchIn(lifecycleScope) } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -187,8 +211,11 @@ class MessagesFlowNode @AssistedInject constructor( callbacks.forEach { it.onRoomDetailsClick() } } - override fun onEventClick(event: TimelineItem.Event): Boolean { - return processEventClick(event) + override fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean { + return processEventClick( + timelineMode = if (isLive) Timeline.Mode.LIVE else Timeline.Mode.FOCUSED_ON_EVENT, + event = event, + ) } override fun onPreviewAttachments(attachments: ImmutableList) { @@ -316,7 +343,10 @@ class MessagesFlowNode @AssistedInject constructor( NavTarget.PinnedMessagesList -> { val callback = object : PinnedMessagesListNode.Callback { override fun onEventClick(event: TimelineItem.Event) { - processEventClick(event) + processEventClick( + timelineMode = Timeline.Mode.PINNED_EVENTS, + event = event, + ) } override fun onUserDataClick(userId: UserId) { @@ -358,33 +388,23 @@ class MessagesFlowNode @AssistedInject constructor( callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) } } - private fun processEventClick(event: TimelineItem.Event): Boolean { + private fun processEventClick( + timelineMode: Timeline.Mode, + event: TimelineItem.Event, + ): Boolean { val navTarget = when (event.content) { is TimelineItemImageContent -> { buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode), event = event, content = event.content, mediaSource = event.content.mediaSource, thumbnailSource = event.content.thumbnailSource, ) } - is TimelineItemStickerContent -> { - /* Sticker may have an empty url and no thumbnail - if encrypted on certain bridges */ - event.content.preferredMediaSource?.let { preferredMediaSource -> - buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, - event = event, - content = event.content, - mediaSource = preferredMediaSource, - thumbnailSource = event.content.thumbnailSource, - ) - } - } is TimelineItemVideoContent -> { buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode), event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -393,7 +413,7 @@ class MessagesFlowNode @AssistedInject constructor( } is TimelineItemFileContent -> { buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode), event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -402,7 +422,7 @@ class MessagesFlowNode @AssistedInject constructor( } is TimelineItemAudioContent -> { buildMediaViewerNavTarget( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode), event = event, content = event.content, mediaSource = event.content.mediaSource, @@ -413,7 +433,7 @@ class MessagesFlowNode @AssistedInject constructor( NavTarget.LocationViewer( location = event.content.location, description = event.content.description, - ) + ).takeIf { locationService.isServiceAvailable() } } else -> null } @@ -467,10 +487,9 @@ class MessagesFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - mentionSpanTheme.updateStyles(currentUserId = room.sessionId) + mentionSpanTheme.updateStyles() CompositionLocalProvider( - LocalRoomMemberProfilesCache provides roomMemberProfilesCache, - LocalMentionSpanTheme provides mentionSpanTheme, + LocalMentionSpanUpdater provides mentionSpanUpdater ) { BackstackWithOverlayBox(modifier) } 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 b045d10199c..c66ec1ee518 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 @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl import io.element.android.features.messages.impl.attachments.Attachment 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.timeline.item.TimelineItemDebugInfo import kotlinx.collections.immutable.ImmutableList @@ -19,4 +20,5 @@ interface MessagesNavigator { fun onReportContentClick(eventId: EventId, senderId: UserId) fun onEditPollClick(eventId: EventId) fun onPreviewAttachment(attachments: ImmutableList) + fun onNavigateToRoom(roomId: RoomId) } 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 2ab3bbbc862..84d20fbf944 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 @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl import android.app.Activity import android.content.Context +import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -17,7 +18,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.Lifecycle import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext @@ -39,31 +39,44 @@ import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab +import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.androidutils.system.toast import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom 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.core.toRoomIdOrAlias 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.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.alias.matches 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.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch @ContributesNode(RoomScope::class) class MessagesNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val room: MatrixRoom, + @ApplicationContext private val context: Context, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, + private val room: BaseRoom, private val analyticsService: AnalyticsService, messageComposerPresenterFactory: MessageComposerPresenter.Factory, timelinePresenterFactory: TimelinePresenter.Factory, @@ -72,7 +85,8 @@ class MessagesNode @AssistedInject constructor( private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val mediaPlayer: MediaPlayer, private val permalinkParser: PermalinkParser, - private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer + private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer, + private val roomMemberModerationRenderer: RoomMemberModerationRenderer, ) : Node(buildContext, plugins = plugins), MessagesNavigator { private val presenter = presenterFactory.create( navigator = this, @@ -88,7 +102,7 @@ class MessagesNode @AssistedInject constructor( interface Callback : Plugin { fun onRoomDetailsClick() - fun onEventClick(event: TimelineItem.Event): Boolean + fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean fun onPreviewAttachments(attachments: ImmutableList) fun onUserDataClick(userId: UserId) fun onPermalinkClick(data: PermalinkData) @@ -107,7 +121,7 @@ class MessagesNode @AssistedInject constructor( super.onBuilt() lifecycle.subscribe( onCreate = { - analyticsService.capture(room.toAnalyticsViewRoom()) + sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) } }, onDestroy = { mediaPlayer.close() @@ -119,12 +133,12 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onRoomDetailsClick() } } - private fun onEventClick(event: TimelineItem.Event): Boolean { + private fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean { // Note: cannot use `callbacks.all { it.onEventClick(event) }` because: // - if callbacks is empty, it will return true and we want to return false. // - if a callback returns false, the other callback will not be invoked. return callbacks.takeIf { it.isNotEmpty() } - ?.map { it.onEventClick(event) } + ?.map { it.onEventClick(isLive, event) } ?.all { it } .orFalse() } @@ -138,6 +152,7 @@ class MessagesNode @AssistedInject constructor( darkTheme: Boolean, url: String, eventSink: (TimelineEvents) -> Unit, + customTab: Boolean ) { when (val permalink = permalinkParser.parse(url)) { is PermalinkData.UserLink -> { @@ -146,9 +161,15 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onUserDataClick(permalink.userId) } } is PermalinkData.RoomLink -> { - handleRoomLinkClick(activity, permalink, eventSink) + handleRoomLinkClick(permalink, eventSink) + } + is PermalinkData.FallbackLink -> { + if (customTab) { + activity.openUrlInChromeCustomTab(null, darkTheme, url) + } else { + activity.openUrlInExternalApp(url) + } } - is PermalinkData.FallbackLink, is PermalinkData.RoomEmailInviteLink -> { activity.openUrlInChromeCustomTab(null, darkTheme, url) } @@ -156,7 +177,6 @@ class MessagesNode @AssistedInject constructor( } private fun handleRoomLinkClick( - context: Context, roomLink: PermalinkData.RoomLink, eventSink: (TimelineEvents) -> Unit, ) { @@ -166,7 +186,7 @@ class MessagesNode @AssistedInject constructor( eventSink(TimelineEvents.FocusOnEvent(eventId)) } else { // Click on the same room, ignore - context.toast("Already viewing this room!") + displaySameRoomToast() } } else { callbacks.forEach { it.onPermalinkClick(roomLink) } @@ -193,6 +213,15 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onPreviewAttachments(attachments) } } + override fun onNavigateToRoom(roomId: RoomId) { + if (roomId == room.roomId) { + displaySameRoomToast() + } else { + val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias()) + callbacks.forEach { it.onPermalinkClick(permalinkData) } + } + } + private fun onViewAllPinnedMessagesClick() { callbacks.forEach { it.onViewAllPinnedEvents() } } @@ -213,9 +242,13 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onViewKnockRequests() } } + private fun displaySameRoomToast() { + context.toast(CommonStrings.screen_room_permalink_same_room_android) + } + @Composable override fun View(modifier: Modifier) { - val activity = LocalContext.current as Activity + val activity = requireNotNull(LocalActivity.current) val isDark = ElementTheme.isLightTheme.not() CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, @@ -233,18 +266,28 @@ class MessagesNode @AssistedInject constructor( onRoomDetailsClick = this::onRoomDetailsClick, onEventContentClick = this::onEventClick, onUserDataClick = this::onUserDataClick, - onLinkClick = { url -> onLinkClick(activity, isDark, url, state.timelineState.eventSink) }, + onLinkClick = { url, customTab -> onLinkClick(activity, isDark, url, state.timelineState.eventSink, customTab) }, onSendLocationClick = this::onSendLocationClick, onCreatePollClick = this::onCreatePollClick, onJoinCallClick = this::onJoinCallClick, onViewAllPinnedMessagesClick = this::onViewAllPinnedMessagesClick, + modifier = modifier, knockRequestsBannerView = { knockRequestsBannerRenderer.View( modifier = Modifier, onViewRequestsClick = this::onViewKnockRequestsClick ) }, - modifier = modifier, + ) + roomMemberModerationRenderer.Render( + state = state.roomMemberModerationState, + onSelectAction = { action, target -> + when (action) { + is ModerationAction.DisplayProfile -> onUserDataClick(target.userId) + else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) + } + }, + modifier = Modifier, ) var focusedEventId by rememberSaveable { 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 547f5f6161d..3390b55d5e0 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 @@ -21,6 +21,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.LifecycleResumeEffect import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -31,6 +32,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState +import io.element.android.features.messages.impl.link.LinkState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState @@ -48,10 +50,13 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -60,21 +65,23 @@ 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.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.permalink.PermalinkParser -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +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.RoomInfo +import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage import io.element.android.libraries.matrix.api.sync.SyncService -import io.element.android.libraries.matrix.api.sync.isOnline 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 +import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService @@ -86,18 +93,20 @@ import timber.log.Timber class MessagesPresenter @AssistedInject constructor( @Assisted private val navigator: MessagesNavigator, - private val room: MatrixRoom, + private val room: JoinedRoom, @Assisted private val composerPresenter: Presenter, private val voiceMessageComposerPresenter: Presenter, @Assisted private val timelinePresenter: Presenter, private val timelineProtectionPresenter: Presenter, private val identityChangeStatePresenter: Presenter, + private val linkPresenter: Presenter, @Assisted private val actionListPresenter: Presenter, private val customReactionPresenter: Presenter, private val reactionSummaryPresenter: Presenter, private val readReceiptBottomSheetPresenter: Presenter, private val pinnedMessagesBannerPresenter: Presenter, private val roomCallStatePresenter: Presenter, + private val roomMemberModerationPresenter: Presenter, private val syncService: SyncService, private val snackbarDispatcher: SnackbarDispatcher, private val dispatchers: CoroutineDispatchers, @@ -108,6 +117,7 @@ class MessagesPresenter @AssistedInject constructor( private val timelineController: TimelineController, private val permalinkParser: PermalinkParser, private val analyticsService: AnalyticsService, + private val encryptionService: EncryptionService, ) : Presenter { @AssistedFactory interface Factory { @@ -121,9 +131,9 @@ class MessagesPresenter @AssistedInject constructor( @Composable override fun present(): MessagesState { - htmlConverterProvider.Update(currentUserId = room.sessionId) + htmlConverterProvider.Update() - val roomInfo by room.roomInfoFlow.collectAsState(null) + val roomInfo by room.roomInfoFlow.collectAsState() val localCoroutineScope = rememberCoroutineScope() val composerState = composerPresenter.present() val voiceMessageComposerState = voiceMessageComposerPresenter.present() @@ -131,46 +141,49 @@ class MessagesPresenter @AssistedInject constructor( val timelineProtectionState = timelineProtectionPresenter.present() val identityChangeState = identityChangeStatePresenter.present() val actionListState = actionListPresenter.present() + val linkState = linkPresenter.present() val customReactionState = customReactionPresenter.present() val reactionSummaryState = reactionSummaryPresenter.present() val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present() val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present() val roomCallState = roomCallStatePresenter.present() - + val roomMemberModerationState = roomMemberModerationPresenter.present() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userEventPermissions by userEventPermissions(syncUpdateFlow.value) - val roomName: AsyncData by remember { - derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized } - } - val roomAvatar: AsyncData by remember { - derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized } + val roomAvatar by remember { + derivedStateOf { roomInfo.avatarData() } } val heroes by remember { - derivedStateOf { roomInfo?.heroes().orEmpty().toPersistentList() } + derivedStateOf { roomInfo.heroes().toPersistentList() } } var hasDismissedInviteDialog by rememberSaveable { mutableStateOf(false) } - LaunchedEffect(Unit) { // Remove the unread flag on entering but don't send read receipts // as those will be handled by the timeline. withContext(dispatchers.io) { room.setUnreadFlag(isUnread = false) + + // If for some reason the encryption state is unknown, fetch it + if (roomInfo.isEncrypted == null) { + room.getUpdatedIsEncrypted() + } } } val inviteProgress = remember { mutableStateOf>(AsyncData.Uninitialized) } var showReinvitePrompt by remember { mutableStateOf(false) } - LaunchedEffect(hasDismissedInviteDialog, composerState.textEditorState.hasFocus(), syncUpdateFlow.value) { + val composerHasFocus by remember { derivedStateOf { composerState.textEditorState.hasFocus() } } + LaunchedEffect(hasDismissedInviteDialog, composerHasFocus, roomInfo) { withContext(dispatchers.io) { - showReinvitePrompt = !hasDismissedInviteDialog && composerState.textEditorState.hasFocus() && room.isDm && room.activeMemberCount == 1L + showReinvitePrompt = !hasDismissedInviteDialog && composerHasFocus && roomInfo.isDm && roomInfo.activeMembersCount == 1L } } - val isOnline by syncService.isOnline().collectAsState() + val isOnline by syncService.isOnline.collectAsState() val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() @@ -179,6 +192,25 @@ class MessagesPresenter @AssistedInject constructor( enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages) } + var dmUserVerificationState by remember { mutableStateOf(null) } + + val membersState by room.membersStateFlow.collectAsState() + val dmRoomMember by room.getDirectRoomMember(membersState) + val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges + + LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) { + if (roomInfo.isEncrypted == true) { + val dmRoomMemberId = dmRoomMember?.userId + localCoroutineScope.launch { + dmRoomMemberId?.let { userId -> + dmUserVerificationState = roomMemberIdentityStateChanges.find { it.identityRoomMember.userId == userId }?.identityState + ?: encryptionService.getUserIdentity(userId).getOrNull() + } + } + } + onPauseOrDispose {} + } + fun handleEvents(event: MessagesEvents) { when (event) { is MessagesEvents.HandleAction -> { @@ -202,12 +234,15 @@ class MessagesPresenter @AssistedInject constructor( } } is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear) + is MessagesEvents.OnUserClicked -> { + roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user)) + } } } return MessagesState( roomId = room.roomId, - roomName = roomName, + roomName = roomInfo.name, roomAvatar = roomAvatar, heroes = heroes, composerState = composerState, @@ -216,6 +251,7 @@ class MessagesPresenter @AssistedInject constructor( timelineState = timelineState, timelineProtectionState = timelineProtectionState, identityChangeState = identityChangeState, + linkState = linkState, actionListState = actionListState, customReactionState = customReactionState, reactionSummaryState = reactionSummaryState, @@ -229,6 +265,9 @@ class MessagesPresenter @AssistedInject constructor( appName = buildMeta.applicationName, roomCallState = roomCallState, pinnedMessagesBannerState = pinnedMessagesBannerState, + dmUserVerificationState = dmUserVerificationState, + roomMemberModerationState = roomMemberModerationState, + successorRoom = roomInfo.successorRoom, eventSink = { handleEvents(it) } ) } @@ -246,16 +285,16 @@ class MessagesPresenter @AssistedInject constructor( } } - private fun MatrixRoomInfo.avatarData(): AvatarData { + private fun RoomInfo.avatarData(): AvatarData { return AvatarData( id = id.value, name = name, - url = avatarUrl ?: room.avatarUrl, + url = avatarUrl, size = AvatarSize.TimelineRoom ) } - private fun MatrixRoomInfo.heroes(): List { + private fun RoomInfo.heroes(): List { return heroes.map { user -> user.getAvatarData(size = AvatarSize.TimelineRoom) } @@ -347,10 +386,10 @@ class MessagesPresenter @AssistedInject constructor( private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState>) = launch(dispatchers.io) { inviteProgress.value = AsyncData.Loading() - runCatching { + runCatchingExceptions { val memberList = when (val memberState = room.membersStateFlow.value) { - is MatrixRoomMembersState.Ready -> memberState.roomMembers - is MatrixRoomMembersState.Error -> memberState.prevRoomMembers.orEmpty() + is RoomMembersState.Ready -> memberState.roomMembers + is RoomMembersState.Error -> memberState.prevRoomMembers.orEmpty() else -> emptyList() } 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 5db91b30459..bedce147dcc 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 @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState +import io.element.android.features.messages.impl.link.LinkState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineState @@ -19,17 +20,20 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import kotlinx.collections.immutable.ImmutableList @Immutable data class MessagesState( val roomId: RoomId, - val roomName: AsyncData, - val roomAvatar: AsyncData, + val roomName: String?, + val roomAvatar: AvatarData, val heroes: ImmutableList, val userEventPermissions: UserEventPermissions, val composerState: MessageComposerState, @@ -37,6 +41,7 @@ data class MessagesState( val timelineState: TimelineState, val timelineProtectionState: TimelineProtectionState, val identityChangeState: IdentityChangeState, + val linkState: LinkState, val actionListState: ActionListState, val customReactionState: CustomReactionState, val reactionSummaryState: ReactionSummaryState, @@ -50,5 +55,10 @@ data class MessagesState( val roomCallState: RoomCallState, val appName: String, val pinnedMessagesBannerState: PinnedMessagesBannerState, + val dmUserVerificationState: IdentityState?, + val roomMemberModerationState: RoomMemberModerationState, + val successorRoom: SuccessorRoom?, val eventSink: (MessagesEvents) -> 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 308efd8eac0..8fa539dd948 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 @@ -12,6 +12,8 @@ import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState +import io.element.android.features.messages.impl.link.LinkState +import io.element.android.features.messages.impl.link.aLinkState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState @@ -35,10 +37,14 @@ import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMe import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.features.roomcall.api.anOngoingCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState 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.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.aTextEditorStateRich import kotlinx.collections.immutable.persistentListOf @@ -52,10 +58,7 @@ open class MessagesStateProvider : PreviewParameterProvider { aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)), aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)), aMessagesState(showReinvitePrompt = true), - aMessagesState( - roomName = AsyncData.Uninitialized, - roomAvatar = AsyncData.Uninitialized, - ), + aMessagesState(roomName = null), aMessagesState(composerState = aMessageComposerState(showTextFormatting = true)), aMessagesState( enableVoiceMessages = true, @@ -80,12 +83,15 @@ open class MessagesStateProvider : PreviewParameterProvider { currentPinnedMessageIndex = 0, ), ), + aMessagesState(roomName = "A DM with a very looong name", dmUserVerificationState = IdentityState.Verified), + aMessagesState(roomName = "A DM with a very looong name", dmUserVerificationState = IdentityState.VerificationViolation), + aMessagesState(successorRoom = SuccessorRoom(RoomId("!id:domain"), null)), ) } fun aMessagesState( - roomName: AsyncData = AsyncData.Success("Room name"), - roomAvatar: AsyncData = AsyncData.Success(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), @@ -100,6 +106,7 @@ fun aMessagesState( ), timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), identityChangeState: IdentityChangeState = anIdentityChangeState(), + linkState: LinkState = aLinkState(), readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(), actionListState: ActionListState = anActionListState(), customReactionState: CustomReactionState = aCustomReactionState(), @@ -109,6 +116,9 @@ fun aMessagesState( enableVoiceMessages: Boolean = true, roomCallState: RoomCallState = aStandByCallState(), pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(), + dmUserVerificationState: IdentityState? = null, + roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(), + successorRoom: SuccessorRoom? = null, eventSink: (MessagesEvents) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), @@ -120,6 +130,7 @@ fun aMessagesState( voiceMessageComposerState = voiceMessageComposerState, timelineProtectionState = timelineProtectionState, identityChangeState = identityChangeState, + linkState = linkState, timelineState = timelineState, readReceiptBottomSheetState = readReceiptBottomSheetState, actionListState = actionListState, @@ -134,9 +145,21 @@ fun aMessagesState( roomCallState = roomCallState, appName = "Element", pinnedMessagesBannerState = pinnedMessagesBannerState, + dmUserVerificationState = dmUserVerificationState, + roomMemberModerationState = roomMemberModerationState, + successorRoom = successorRoom, eventSink = eventSink, ) +fun aRoomMemberModerationState( + canKick: Boolean = false, + canBan: Boolean = false, +) = object : RoomMemberModerationState { + override val canKick: Boolean = canKick + override val canBan: Boolean = canBan + override val eventSink: (RoomMemberModerationEvents) -> Unit = {} +} + fun aUserEventPermissions( canRedactOwn: Boolean = false, canRedactOther: Boolean = false, 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 02ad1cf87f2..7c52cc9bab3 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 @@ -26,35 +26,36 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableIntStateOf -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.geometry.Offset +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource +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.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.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStateView +import io.element.android.features.messages.impl.link.LinkEvents +import io.element.android.features.messages.impl.link.LinkView import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet +import io.element.android.features.messages.impl.messagecomposer.DisabledComposerView import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsPickerView @@ -78,15 +79,19 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.androidutils.ui.hideKeyboard -import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule +import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule +import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayout +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.CompositeAvatar +import io.element.android.libraries.designsystem.components.avatar.AvatarType 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.rememberExpandableBottomSheetLayoutState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.toAnnotatedString 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.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -96,12 +101,16 @@ import io.element.android.libraries.designsystem.utils.OnLifecycleEvent 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.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.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.wysiwyg.link.Link import kotlinx.collections.immutable.ImmutableList import timber.log.Timber -import kotlin.random.Random import kotlin.time.Duration.Companion.milliseconds @Composable @@ -109,9 +118,9 @@ fun MessagesView( state: MessagesState, onBackClick: () -> Unit, onRoomDetailsClick: () -> Unit, - onEventContentClick: (event: TimelineItem.Event) -> Boolean, + onEventContentClick: (isLive: Boolean, event: TimelineItem.Event) -> Boolean, onUserDataClick: (UserId) -> Unit, - onLinkClick: (String) -> Unit, + onLinkClick: (String, Boolean) -> Unit, onSendLocationClick: () -> Unit, onCreatePollClick: () -> Unit, onJoinCallClick: () -> Unit, @@ -140,7 +149,7 @@ fun MessagesView( fun onContentClick(event: TimelineItem.Event) { Timber.v("onMessageClick= ${event.id}") - val hideKeyboard = onEventContentClick(event) + val hideKeyboard = onEventContentClick(state.timelineState.isLive, event) if (hideKeyboard) { localView.hideKeyboard() } @@ -175,56 +184,117 @@ fun MessagesView( state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event)) } - Scaffold( - modifier = modifier, - contentWindowInsets = WindowInsets.statusBars, - topBar = { - Column { - ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) - MessagesViewTopBar( - roomName = state.roomName.dataOrNull(), - roomAvatar = state.roomAvatar.dataOrNull(), - heroes = state.heroes, - roomCallState = state.roomCallState, - onBackClick = { hidingKeyboard { onBackClick() } }, - onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } }, - onJoinCallClick = onJoinCallClick, - ) - } - }, - content = { padding -> - MessagesViewContent( - state = state, - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding), - onContentClick = ::onContentClick, - onMessageLongClick = ::onMessageLongClick, - onUserDataClick = { hidingKeyboard { onUserDataClick(it) } }, - onLinkClick = onLinkClick, - onReactionClick = ::onEmojiReactionClick, - onReactionLongClick = ::onEmojiReactionLongClick, - onMoreReactionsClick = ::onMoreReactionsClick, - onReadReceiptClick = { event -> - state.readReceiptBottomSheetState.eventSink(ReadReceiptBottomSheetEvents.EventSelected(event)) + val expandableState = rememberExpandableBottomSheetLayoutState() + ExpandableBottomSheetLayout( + modifier = modifier + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + content = { + Scaffold( + contentWindowInsets = WindowInsets.statusBars, + topBar = { + Column { + ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) + MessagesViewTopBar( + roomName = state.roomName, + roomAvatar = state.roomAvatar, + isTombstoned = state.isTombstoned, + heroes = state.heroes, + roomCallState = state.roomCallState, + dmUserIdentityState = state.dmUserVerificationState, + onBackClick = { hidingKeyboard { onBackClick() } }, + onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } }, + onJoinCallClick = onJoinCallClick, + ) + } }, - onSendLocationClick = onSendLocationClick, - onCreatePollClick = onCreatePollClick, - onSwipeToReply = { targetEvent -> - state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent)) + content = { padding -> + Box( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + ) { + MessagesViewContent( + state = state, + onContentClick = ::onContentClick, + onMessageLongClick = ::onMessageLongClick, + onUserDataClick = { + hidingKeyboard { + state.eventSink(MessagesEvents.OnUserClicked(it)) + } + }, + onLinkClick = { link, customTab -> + if (customTab) { + onLinkClick(link.url, true) + // Do not check those links, they are internal link only + } else { + state.linkState.eventSink(LinkEvents.OnLinkClick(link)) + } + }, + onReactionClick = ::onEmojiReactionClick, + onReactionLongClick = ::onEmojiReactionLongClick, + onMoreReactionsClick = ::onMoreReactionsClick, + onReadReceiptClick = { event -> + state.readReceiptBottomSheetState.eventSink(ReadReceiptBottomSheetEvents.EventSelected(event)) + }, + onSendLocationClick = onSendLocationClick, + onCreatePollClick = onCreatePollClick, + onSwipeToReply = { targetEvent -> + state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent)) + }, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, + onJoinCallClick = onJoinCallClick, + onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, + knockRequestsBannerView = knockRequestsBannerView, + ) + + SuggestionsPickerView( + modifier = Modifier + .shadow(10.dp) + .background(ElementTheme.colors.bgCanvasDefault) + .align(Alignment.BottomStart) + .heightIn(max = 230.dp), + roomId = state.roomId, + roomName = state.roomName, + roomAvatarData = state.roomAvatar, + suggestions = state.composerState.suggestions, + onSelectSuggestion = { + state.composerState.eventSink(MessageComposerEvents.InsertSuggestion(it)) + } + ) + } + }, + snackbarHost = { + SnackbarHost( + snackbarHostState, + modifier = Modifier.navigationBarsPadding() + ) }, - forceJumpToBottomVisibility = forceJumpToBottomVisibility, - onJoinCallClick = onJoinCallClick, - onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, - knockRequestsBannerView = knockRequestsBannerView, ) }, - snackbarHost = { - SnackbarHost( - snackbarHostState, - modifier = Modifier.navigationBarsPadding() + bottomSheetContent = { + MessagesViewComposerBottomSheetContents( + state = state, + onLinkClick = { url, customTab -> onLinkClick(url, customTab) }, + onRoomSuccessorClick = { roomId -> + state.timelineState.eventSink(TimelineEvents.NavigateToRoom(roomId = roomId)) + }, ) }, + sheetDragHandle = if (state.composerState.showTextFormatting) { + @Composable { BottomSheetDragHandle() } + } else { + @Composable {} + }, + isSwipeGestureEnabled = state.composerState.showTextFormatting, + state = expandableState, + sheetShape = if (state.composerState.showTextFormatting || state.composerState.suggestions.isNotEmpty()) { + MaterialTheme.shapes.large + } else { + RectangleShape + }, + maxBottomSheetContentHeight = 360.dp, ) ActionListView( @@ -252,6 +322,12 @@ fun MessagesView( onUserDataClick = onUserDataClick, ) ReinviteDialog(state = state) + LinkView( + onLinkValid = { link -> + onLinkClick(link.url, false) + }, + state = state.linkState, + ) } @Composable @@ -272,8 +348,8 @@ private fun ReinviteDialog(state: MessagesState) { private fun MessagesViewContent( state: MessagesState, onContentClick: (TimelineItem.Event) -> Unit, - onUserDataClick: (UserId) -> Unit, - onLinkClick: (String) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, + onLinkClick: (Link, Boolean) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit, @@ -290,9 +366,9 @@ private fun MessagesViewContent( ) { Box( modifier = modifier - .fillMaxSize() - .navigationBarsPadding() - .imePadding(), + .fillMaxSize() + .navigationBarsPadding() + .imePadding(), ) { AttachmentsBottomSheet( state = state.composerState, @@ -318,123 +394,85 @@ private fun MessagesViewContent( ) } - // This key is used to force the sheet to be remeasured when the content changes. - // Any state change that should trigger a height size should be added to the list of remembered values here. - val sheetResizeContentKey = remember { mutableIntStateOf(0) } - LaunchedEffect( - state.composerState.textEditorState.lineCount, - state.composerState.showTextFormatting, - ) { - sheetResizeContentKey.intValue = Random.nextInt() - } - - ExpandableBottomSheetScaffold( - sheetDragHandle = if (state.composerState.showTextFormatting) { - @Composable { BottomSheetDragHandle() } - } else { - @Composable {} - }, - sheetSwipeEnabled = state.composerState.showTextFormatting, - sheetShape = if (state.composerState.showTextFormatting || state.composerState.suggestions.isNotEmpty()) { - MaterialTheme.shapes.large - } else { - RectangleShape - }, - content = { paddingValues -> - Box(modifier = Modifier.padding(paddingValues)) { - val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior() - TimelineView( - state = state.timelineState, - timelineProtectionState = state.timelineProtectionState, - onUserDataClick = onUserDataClick, - onLinkClick = onLinkClick, - onContentClick = onContentClick, - onMessageLongClick = onMessageLongClick, - onSwipeToReply = onSwipeToReply, - onReactionClick = onReactionClick, - onReactionLongClick = onReactionLongClick, - onMoreReactionsClick = onMoreReactionsClick, - onReadReceiptClick = onReadReceiptClick, - forceJumpToBottomVisibility = forceJumpToBottomVisibility, - onJoinCallClick = onJoinCallClick, - nestedScrollConnection = scrollBehavior.nestedScrollConnection, + Box { + val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberScrollBehavior( + pinnedMessagesCount = (state.pinnedMessagesBannerState as? PinnedMessagesBannerState.Visible)?.pinnedMessagesCount() ?: 0, + ) + TimelineView( + state = state.timelineState, + timelineProtectionState = state.timelineProtectionState, + onUserDataClick = onUserDataClick, + onLinkClick = { link -> onLinkClick(link, false) }, + onContentClick = onContentClick, + onMessageLongClick = onMessageLongClick, + onSwipeToReply = onSwipeToReply, + onReactionClick = onReactionClick, + onReactionLongClick = onReactionLongClick, + onMoreReactionsClick = onMoreReactionsClick, + onReadReceiptClick = onReadReceiptClick, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, + onJoinCallClick = onJoinCallClick, + nestedScrollConnection = scrollBehavior.nestedScrollConnection, + ) + AnimatedVisibility( + visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + fun focusOnPinnedEvent(eventId: EventId) { + state.timelineState.eventSink( + TimelineEvents.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds) ) - AnimatedVisibility( - visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - fun focusOnPinnedEvent(eventId: EventId) { - state.timelineState.eventSink( - TimelineEvents.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds) - ) - } - PinnedMessagesBannerView( - state = state.pinnedMessagesBannerState, - onClick = ::focusOnPinnedEvent, - onViewAllClick = onViewAllPinnedMessagesClick, - ) - } - knockRequestsBannerView() } - }, - sheetContent = { subcomposing: Boolean -> - MessagesViewComposerBottomSheetContents( - subcomposing = subcomposing, - state = state, - onLinkClick = onLinkClick, + PinnedMessagesBannerView( + state = state.pinnedMessagesBannerState, + onClick = ::focusOnPinnedEvent, + onViewAllClick = onViewAllPinnedMessagesClick, ) - }, - sheetContentKey = sheetResizeContentKey.intValue, - sheetTonalElevation = 0.dp, - sheetShadowElevation = if (state.composerState.suggestions.isNotEmpty()) 16.dp else 0.dp, - ) + } + knockRequestsBannerView() + } } } @Composable private fun MessagesViewComposerBottomSheetContents( - subcomposing: Boolean, state: MessagesState, - onLinkClick: (String) -> Unit, + onRoomSuccessorClick: (RoomId) -> Unit, + onLinkClick: (String, Boolean) -> Unit, ) { - if (state.userEventPermissions.canSendMessage) { - Column(modifier = Modifier.fillMaxWidth()) { - SuggestionsPickerView( - modifier = Modifier - .heightIn(max = 230.dp) - // Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions - .nestedScroll(object : NestedScrollConnection { - override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { - return available - } - }), - roomId = state.roomId, - roomName = state.roomName.dataOrNull(), - roomAvatarData = state.roomAvatar.dataOrNull(), - suggestions = state.composerState.suggestions, - onSelectSuggestion = { - state.composerState.eventSink(MessageComposerEvents.InsertSuggestion(it)) + when { + state.successorRoom != null -> { + SuccessorRoomBanner(roomSuccessor = state.successorRoom, onRoomSuccessorClick = onRoomSuccessorClick) + } + state.userEventPermissions.canSendMessage -> { + Column(modifier = Modifier.fillMaxWidth()) { + // Do not show the identity change if user is composing a Rich message or is seeing suggestion(s). + if (state.composerState.suggestions.isEmpty() && + state.composerState.textEditorState is TextEditorState.Markdown) { + IdentityChangeStateView( + state = state.identityChangeState, + onLinkClick = onLinkClick, + ) + } + val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull { + it.identityState == IdentityState.VerificationViolation + } + if (verificationViolation != null) { + DisabledComposerView(modifier = Modifier.fillMaxWidth()) + } else { + MessageComposerView( + state = state.composerState, + voiceMessageState = state.voiceMessageComposerState, + enableVoiceMessages = state.enableVoiceMessages, + modifier = Modifier.fillMaxWidth(), + ) } - ) - // Do not show the identity change if user is composing a Rich message or is seeing suggestion(s). - if (state.composerState.suggestions.isEmpty() && - state.composerState.textEditorState is TextEditorState.Markdown) { - IdentityChangeStateView( - state = state.identityChangeState, - onLinkClick = onLinkClick, - ) } - MessageComposerView( - state = state.composerState, - voiceMessageState = state.voiceMessageComposerState, - subcomposing = subcomposing, - enableVoiceMessages = state.enableVoiceMessages, - modifier = Modifier.fillMaxWidth(), - ) } - } else { - CantSendMessageBanner() + else -> { + CantSendMessageBanner() + } } } @@ -442,9 +480,11 @@ private fun MessagesViewComposerBottomSheetContents( @Composable private fun MessagesViewTopBar( roomName: String?, - roomAvatar: AvatarData?, + roomAvatar: AvatarData, + isTombstoned: Boolean, heroes: ImmutableList, roomCallState: RoomCallState, + dmUserIdentityState: IdentityState?, onRoomDetailsClick: () -> Unit, onJoinCallClick: () -> Unit, onBackClick: () -> Unit, @@ -455,21 +495,39 @@ private fun MessagesViewTopBar( }, title = { val roundedCornerShape = RoundedCornerShape(8.dp) - val titleModifier = Modifier + Row( + modifier = Modifier .clip(roundedCornerShape) - .clickable { onRoomDetailsClick() } - if (roomName != null && roomAvatar != null) { + .clickable { onRoomDetailsClick() }, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val titleModifier = Modifier.weight(1f, fill = false) RoomAvatarAndNameRow( roomName = roomName, roomAvatar = roomAvatar, + isTombstoned = isTombstoned, heroes = heroes, modifier = titleModifier ) - } else { - IconTitlePlaceholdersRowMolecule( - iconSize = AvatarSize.TimelineRoom.dp, - modifier = titleModifier - ) + + when (dmUserIdentityState) { + IdentityState.Verified -> { + Icon( + imageVector = CompoundIcons.Verified(), + tint = ElementTheme.colors.iconSuccessPrimary, + contentDescription = null, + ) + } + IdentityState.VerificationViolation -> { + Icon( + imageVector = CompoundIcons.ErrorSolid(), + tint = ElementTheme.colors.iconCriticalPrimary, + contentDescription = null, + ) + } + else -> Unit + } } }, actions = { @@ -485,23 +543,32 @@ private fun MessagesViewTopBar( @Composable private fun RoomAvatarAndNameRow( - roomName: String, + roomName: String?, roomAvatar: AvatarData, heroes: ImmutableList, + isTombstoned: Boolean, modifier: Modifier = Modifier ) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically ) { - CompositeAvatar( + Avatar( avatarData = roomAvatar, - heroes = heroes, + avatarType = AvatarType.Room( + heroes = heroes, + isTombstoned = isTombstoned, + ), ) Text( - modifier = Modifier.padding(horizontal = 8.dp), - text = roomName, + modifier = Modifier + .padding(horizontal = 8.dp) + .semantics { + heading() + }, + text = roomName ?: stringResource(CommonStrings.common_no_room_name), style = ElementTheme.typography.fontBodyLgMedium, + fontStyle = FontStyle.Italic.takeIf { roomName == null }, maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -512,15 +579,15 @@ private fun RoomAvatarAndNameRow( private fun CantSendMessageBanner() { Row( modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.secondary) - .padding(16.dp), + .fillMaxWidth() + .background(ElementTheme.colors.bgSubtleSecondary) + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Text( text = stringResource(id = R.string.screen_room_timeline_no_permission_to_post), - color = MaterialTheme.colorScheme.onSecondary, + color = ElementTheme.colors.textSecondary, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, fontStyle = FontStyle.Italic, @@ -528,6 +595,22 @@ private fun CantSendMessageBanner() { } } +@Composable +private fun SuccessorRoomBanner( + roomSuccessor: SuccessorRoom, + onRoomSuccessorClick: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + ComposerAlertMolecule( + avatar = null, + content = stringResource(R.string.screen_room_timeline_tombstoned_room_message).toAnnotatedString(), + onSubmitClick = { onRoomSuccessorClick(roomSuccessor.roomId) }, + modifier = modifier, + isCritical = false, + submitText = stringResource(R.string.screen_room_timeline_tombstoned_room_action) + ) +} + @PreviewsDayNight @Composable internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) = ElementPreview { @@ -535,9 +618,9 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) state = state, onBackClick = {}, onRoomDetailsClick = {}, - onEventContentClick = { false }, + onEventContentClick = { _, _ -> false }, onUserDataClick = {}, - onLinkClick = {}, + onLinkClick = { _, _ -> }, onSendLocationClick = {}, onCreatePollClick = {}, onJoinCallClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 5274dcbc22a..9c2760bf6c5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -43,7 +43,7 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -63,7 +63,7 @@ class DefaultActionListPresenter @AssistedInject constructor( private val postProcessor: TimelineItemActionPostProcessor, private val appPreferencesStore: AppPreferencesStore, private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled, - private val room: MatrixRoom, + private val room: BaseRoom, private val userSendFailureFactory: VerifiedUserSendFailureFactory, private val featureFlagService: FeatureFlagService, private val dateFormatter: DateFormatter, @@ -84,7 +84,9 @@ class DefaultActionListPresenter @AssistedInject constructor( mutableStateOf(ActionListState.Target.None) } - val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false) + val isDeveloperModeEnabled by remember { + appPreferencesStore.isDeveloperModeEnabledFlow() + }.collectAsState(initial = false) val isPinnedEventsEnabled = isPinnedMessagesFeatureEnabled() val pinnedEventIds by remember { room.roomInfoFlow.map { it.pinnedEventIds } @@ -167,7 +169,7 @@ class DefaultActionListPresenter @AssistedInject constructor( if (timelineItem.isRemote && timelineItem.content.canBeForwarded()) { add(TimelineItemAction.Forward) } - if (timelineItem.isEditable) { + if (timelineItem.isEditable && usersEventPermissions.canSendMessage) { if (timelineItem.content is TimelineItemEventContentWithAttachment) { // Caption if (timelineItem.content.caption == null) { 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 7a19468e939..78b4b43112b 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 @@ -27,10 +27,10 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -39,7 +39,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter @@ -51,6 +52,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.ChangedIdentity import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.None import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.UnsignedDevice +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction import io.element.android.features.messages.impl.timeline.components.MessageShieldView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent @@ -71,6 +73,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt 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 +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -189,6 +192,7 @@ private fun ActionListViewContent( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) + .clearAndSetSemantics {}, ) if (target.event.messageShield != null) { MessageShieldView( @@ -252,8 +256,13 @@ private fun MessageSummary( modifier: Modifier = Modifier, ) { val content: @Composable () -> Unit - val icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) } - val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = MaterialTheme.colorScheme.secondary) + val icon: @Composable () -> Unit = { + Avatar( + avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender), + avatarType = AvatarType.User, + ) + } + val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = ElementTheme.colors.textSecondary) @Composable fun ContentForBody(body: String) { @@ -316,7 +325,7 @@ private fun MessageSummary( Text( text = sentTimeFull, style = ElementTheme.typography.fontBodyXsRegular, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, textAlign = TextAlign.End, ) } @@ -348,17 +357,26 @@ private fun EmojiReactionsRow( ) for (emoji in defaultEmojis) { val isHighlighted = highlightedEmojis.contains(emoji) - EmojiButton(emoji, isHighlighted, onEmojiReactionClick) + EmojiButton( + modifier = Modifier + // Make it appear after the more useful actions for the accessibility service + .semantics { + traversalIndex = 1f + }, + emoji = emoji, + isHighlighted = isHighlighted, + onClick = onEmojiReactionClick + ) } Box( modifier = Modifier .size(48.dp), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { Icon( imageVector = CompoundIcons.ReactionAdd(), contentDescription = stringResource(id = CommonStrings.a11y_react_with_other_emojis), - tint = MaterialTheme.colorScheme.secondary, + tint = ElementTheme.colors.iconSecondary, modifier = Modifier .size(24.dp) .clickable( @@ -367,6 +385,10 @@ private fun EmojiReactionsRow( indication = ripple(bounded = false, radius = emojiRippleRadius), interactionSource = remember { MutableInteractionSource() } ) + // Make it appear after the more useful actions for the accessibility service + .semantics { + traversalIndex = 1f + } ) } } @@ -379,6 +401,7 @@ private fun VerifiedUserSendFailureView( modifier: Modifier = Modifier, ) { @Composable + @ReadOnlyComposable fun VerifiedUserSendFailure.headline(): String { return when (this) { is None -> "" @@ -392,7 +415,7 @@ private fun VerifiedUserSendFailureView( modifier = modifier .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 8.dp), - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Error())), + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ErrorSolid())), trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChevronRight())), headlineContent = { Text( @@ -414,36 +437,32 @@ private fun EmojiButton( emoji: String, isHighlighted: Boolean, onClick: (String) -> Unit, + modifier: Modifier = Modifier, ) { val backgroundColor = if (isHighlighted) { ElementTheme.colors.bgActionPrimaryRest } else { Color.Transparent } - val description = if (isHighlighted) { - stringResource(id = CommonStrings.a11y_remove_reaction_with, emoji) - } else { - stringResource(id = CommonStrings.a11y_react_with, emoji) - } + val a11yClickLabel = a11yReactionAction( + emoji = emoji, + userAlreadyReacted = isHighlighted, + ) Box( - modifier = Modifier + modifier = modifier .size(48.dp) .background(backgroundColor, CircleShape) - .clearAndSetSemantics { - contentDescription = description - }, + .clickable( + onClickLabel = a11yClickLabel, + onClick = { onClick(emoji) }, + indication = ripple(bounded = false, radius = emojiRippleRadius), + interactionSource = remember { MutableInteractionSource() } + ), contentAlignment = Alignment.Center ) { Text( emoji, style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = 24.dp.toSp(), color = Color.White), - modifier = Modifier - .clickable( - enabled = true, - onClick = { onClick(emoji) }, - indication = ripple(bounded = false, radius = emojiRippleRadius), - interactionSource = remember { MutableInteractionSource() } - ) ) } } 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 cac84ce9e2d..b48ff98073e 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 @@ -27,11 +27,13 @@ import io.element.android.libraries.androidutils.file.safeDelete 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.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.core.ProgressCallback import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.api.allFiles @@ -73,13 +75,17 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false) val textEditorState by rememberUpdatedState( - TextEditorState.Markdown(markdownTextEditorState) + TextEditorState.Markdown(markdownTextEditorState, isRoomEncrypted = null) ) val ongoingSendAttachmentJob = remember { mutableStateOf(null) } - val allowCaption by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionCreation).collectAsState(initial = false) - val showCaptionCompatibilityWarning by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionWarning).collectAsState(initial = false) + val allowCaption by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionCreation) + }.collectAsState(initial = false) + val showCaptionCompatibilityWarning by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionWarning) + }.collectAsState(initial = false) var useSendQueue by remember { mutableStateOf(false) } var preprocessMediaJob by remember { mutableStateOf(null) } @@ -123,6 +129,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( caption = caption, sendActionState = sendActionState, dismissAfterSend = !useSendQueue, + replyParameters = null, ) } } @@ -233,7 +240,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( caption: String?, sendActionState: MutableState, dismissAfterSend: Boolean, - ) = runCatching { + replyParameters: ReplyParameters?, + ) = runCatchingExceptions { val context = coroutineContext val progressCallback = object : ProgressCallback { override fun onProgress(current: Long, total: Long) { @@ -247,7 +255,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( mediaUploadInfo = mediaUploadInfo, caption = caption, formattedCaption = null, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ).getOrThrow() }.fold( onSuccess = { 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 5c54d3a68e8..e7dac45cd8a 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 @@ -24,7 +24,12 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider get() = sequenceOf( anAttachmentsPreviewState(), - anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing(displayProgress = false)), + anAttachmentsPreviewState( + sendActionState = SendActionState.Sending.Processing(displayProgress = false), + textEditorState = aTextEditorStateMarkdown( + initialText = "This is a caption!" + ) + ), anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing(displayProgress = true)), anAttachmentsPreviewState(sendActionState = SendActionState.Sending.ReadyToUpload(aMediaUploadInfo())), anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f, aMediaUploadInfo())), @@ -55,15 +60,15 @@ fun aMediaUploadInfo( filePath: String = "file://path", thumbnailFilePath: String? = null, ) = MediaUploadInfo.Image( - file = File(filePath), - imageInfo = ImageInfo( - height = 100, - width = 100, - mimetype = MimeTypes.Jpeg, - size = 1000, - thumbnailInfo = null, - thumbnailSource = null, - blurhash = null, - ), - thumbnailFile = thumbnailFilePath?.let { File(it) }, - ) + file = File(filePath), + imageInfo = ImageInfo( + height = 100, + width = 100, + mimetype = MimeTypes.Jpeg, + size = 1000, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ), + thumbnailFile = thumbnailFilePath?.let { File(it) }, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index ca9faddfee4..8b66fd9e625 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -196,6 +196,7 @@ private fun AttachmentsPreviewBottomActions( onDeleteVoiceMessage = {}, onReceiveSuggestion = {}, resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, + resolveAtRoomMentionDisplay = { TextDisplay.Plain }, onError = {}, onTyping = {}, onSelectRichContent = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt index e74004c1d02..149789d97d4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt @@ -10,5 +10,6 @@ package io.element.android.features.messages.impl.crypto.identity import io.element.android.libraries.matrix.api.core.UserId sealed interface IdentityChangeEvent { - data class Submit(val userId: UserId) : IdentityChangeEvent + data class PinIdentity(val userId: UserId) : IdentityChangeEvent + data class WithdrawVerification(val userId: UserId) : IdentityChangeEvent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt index 627c304bad5..0ff989fe706 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt @@ -7,15 +7,10 @@ package io.element.android.features.messages.impl.crypto.identity -import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange import kotlinx.collections.immutable.ImmutableList data class IdentityChangeState( val roomMemberIdentityStateChanges: ImmutableList, val eventSink: (IdentityChangeEvent) -> Unit, ) - -data class RoomMemberIdentityStateChange( - val identityRoomMember: IdentityRoomMember, - val identityState: IdentityState, -) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index adc39fbc746..1d3b9778c3e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -8,48 +8,39 @@ package io.element.android.features.messages.impl.crypto.identity import androidx.compose.runtime.Composable -import androidx.compose.runtime.ProduceStateScope import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberCoroutineScope import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.roomMembers -import io.element.android.libraries.matrix.ui.model.getAvatarData -import kotlinx.collections.immutable.PersistentList +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject class IdentityChangeStatePresenter @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val encryptionService: EncryptionService, ) : Presenter { @Composable override fun present(): IdentityChangeState { val coroutineScope = rememberCoroutineScope() val roomMemberIdentityStateChange by produceState(persistentListOf()) { - observeRoomMemberIdentityStateChange() + room.roomMemberIdentityStateChange(waitForEncryption = true).collect { value = it } } fun handleEvent(event: IdentityChangeEvent) { when (event) { - is IdentityChangeEvent.Submit -> coroutineScope.pinUserIdentity(event.userId) + is IdentityChangeEvent.WithdrawVerification -> { + coroutineScope.withdrawVerification(event.userId) + } + is IdentityChangeEvent.PinIdentity -> { + coroutineScope.pinUserIdentity(event.userId) + } } } @@ -59,56 +50,17 @@ class IdentityChangeStatePresenter @Inject constructor( ) } - @OptIn(ExperimentalCoroutinesApi::class) - private fun ProduceStateScope>.observeRoomMemberIdentityStateChange() { - room.syncUpdateFlow - .filter { - // Room cannot become unencrypted, so we can just apply a filter here. - room.isEncrypted - } - .distinctUntilChanged() - .flatMapLatest { - combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState -> - identityStateChanges.map { identityStateChange -> - val member = membersState.roomMembers() - ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId } - ?.toIdentityRoomMember() - ?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId) - RoomMemberIdentityStateChange( - identityRoomMember = member, - identityState = identityStateChange.identityState, - ) - } - } - .distinctUntilChanged() - .onEach { roomMemberIdentityStateChanges -> - value = roomMemberIdentityStateChanges.toPersistentList() - } - } - .launchIn(this) - } - private fun CoroutineScope.pinUserIdentity(userId: UserId) = launch { encryptionService.pinUserIdentity(userId) .onFailure { Timber.e(it, "Failed to pin identity for user $userId") } } -} - -private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember( - userId = userId, - displayNameOrDefault = displayNameOrDefault, - avatarData = getAvatarData(AvatarSize.ComposerAlert), -) -private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember( - userId = userId, - displayNameOrDefault = userId.extractedDisplayName, - avatarData = AvatarData( - id = userId.value, - name = null, - url = null, - size = AvatarSize.ComposerAlert, - ), -) + private fun CoroutineScope.withdrawVerification(userId: UserId) = launch { + encryptionService.withdrawVerification(userId) + .onFailure { + Timber.e(it, "Failed to withdraw verification for user $userId") + } + } +} 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 5ec38588091..858231ebd51 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 @@ -12,6 +12,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.ui.room.IdentityRoomMember +import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange import kotlinx.collections.immutable.toImmutableList class IdentityChangeStateProvider : PreviewParameterProvider { @@ -30,7 +32,7 @@ class IdentityChangeStateProvider : PreviewParameterProvider = emptyList(), + eventSink: (IdentityChangeEvent) -> Unit = {}, ) = IdentityChangeState( roomMemberIdentityStateChanges = roomMemberIdentityStateChanges.toImmutableList(), - eventSink = {}, + eventSink = eventSink, ) internal fun anIdentityRoomMember( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt index 5dbdeeb4052..0f55985740a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt @@ -7,6 +7,7 @@ package io.element.android.features.messages.impl.crypto.identity +import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -17,75 +18,105 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.appconfig.LearnMoreConfig +import io.element.android.compound.theme.ElementTheme 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.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.encryption.identity.isAViolation +import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange import io.element.android.libraries.ui.strings.CommonStrings @Composable fun IdentityChangeStateView( state: IdentityChangeState, - onLinkClick: (String) -> Unit, + onLinkClick: (String, Boolean) -> Unit, modifier: Modifier = Modifier, ) { - // Pick the first identity change to PinViolation - val pinViolationIdentityChange = state.roomMemberIdentityStateChanges.firstOrNull { - // For now only render PinViolation - it.identityState == IdentityState.PinViolation + // Pick the first identity change that is a violation + val identityChangeViolation = state.roomMemberIdentityStateChanges.firstOrNull { + it.identityState.isAViolation() } - if (pinViolationIdentityChange != null) { - ComposerAlertMolecule( + when (identityChangeViolation?.identityState) { + IdentityState.PinViolation -> ViolationAlert( + identityChangeViolation = identityChangeViolation, + onLinkClick = onLinkClick, + textId = CommonStrings.crypto_identity_change_pin_violation_new, + isCritical = false, + submitTextId = CommonStrings.action_dismiss, + onSubmitClick = { state.eventSink(IdentityChangeEvent.PinIdentity(identityChangeViolation.identityRoomMember.userId)) }, modifier = modifier, - avatar = pinViolationIdentityChange.identityRoomMember.avatarData, - content = buildAnnotatedString { - val learnMoreStr = stringResource(CommonStrings.action_learn_more) - val displayName = pinViolationIdentityChange.identityRoomMember.displayNameOrDefault - val userIdStr = stringResource( - CommonStrings.crypto_identity_change_pin_violation_new_user_id, - pinViolationIdentityChange.identityRoomMember.userId, - ) - val fullText = stringResource( - id = CommonStrings.crypto_identity_change_pin_violation_new, - displayName, - userIdStr, - learnMoreStr, - ) - append(fullText) - val userIdStartIndex = fullText.indexOf(userIdStr) - addStyle( - style = SpanStyle( - fontWeight = FontWeight.Bold, - ), - start = userIdStartIndex, - end = userIdStartIndex + userIdStr.length, - ) - val learnMoreStartIndex = fullText.lastIndexOf(learnMoreStr) - addStyle( - style = SpanStyle( - textDecoration = TextDecoration.Underline, - fontWeight = FontWeight.Bold, - ), - start = learnMoreStartIndex, - end = learnMoreStartIndex + learnMoreStr.length, - ) - addLink( - url = LinkAnnotation.Url( - url = LearnMoreConfig.IDENTITY_CHANGE_URL, - linkInteractionListener = { - onLinkClick(LearnMoreConfig.IDENTITY_CHANGE_URL) - } - ), - start = learnMoreStartIndex, - end = learnMoreStartIndex + learnMoreStr.length, - ) - }, - onSubmitClick = { state.eventSink(IdentityChangeEvent.Submit(pinViolationIdentityChange.identityRoomMember.userId)) }, - isCritical = pinViolationIdentityChange.identityState == IdentityState.VerificationViolation, ) + IdentityState.VerificationViolation -> ViolationAlert( + identityChangeViolation = identityChangeViolation, + onLinkClick = onLinkClick, + textId = CommonStrings.crypto_identity_change_verification_violation_new, + isCritical = true, + submitTextId = CommonStrings.crypto_identity_change_withdraw_verification_action, + onSubmitClick = { state.eventSink(IdentityChangeEvent.WithdrawVerification(identityChangeViolation.identityRoomMember.userId)) }, + modifier = modifier, + ) + else -> Unit } } +@Composable +private fun ViolationAlert( + identityChangeViolation: RoomMemberIdentityStateChange, + onLinkClick: (String, Boolean) -> Unit, + @StringRes textId: Int, + isCritical: Boolean, + @StringRes submitTextId: Int, + onSubmitClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ComposerAlertMolecule( + modifier = modifier, + avatar = identityChangeViolation.identityRoomMember.avatarData, + content = buildAnnotatedString { + val learnMoreStr = stringResource(CommonStrings.action_learn_more) + val displayName = identityChangeViolation.identityRoomMember.displayNameOrDefault + val userIdStr = stringResource( + CommonStrings.crypto_identity_change_pin_violation_new_user_id, + identityChangeViolation.identityRoomMember.userId, + ) + val fullText = stringResource(textId, displayName, userIdStr, learnMoreStr) + append(fullText) + val userIdStartIndex = fullText.indexOf(userIdStr) + addStyle( + style = SpanStyle( + fontWeight = FontWeight.Bold, + ), + start = userIdStartIndex, + end = userIdStartIndex + userIdStr.length, + ) + val learnMoreStartIndex = fullText.lastIndexOf(learnMoreStr) + addStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + fontWeight = FontWeight.Bold, + color = ElementTheme.colors.textPrimary + ), + start = learnMoreStartIndex, + end = learnMoreStartIndex + learnMoreStr.length, + ) + addLink( + url = LinkAnnotation.Url( + url = LearnMoreConfig.IDENTITY_CHANGE_URL, + linkInteractionListener = { + onLinkClick(LearnMoreConfig.IDENTITY_CHANGE_URL, true) + } + ), + start = learnMoreStartIndex, + end = learnMoreStartIndex + learnMoreStr.length, + ) + }, + submitText = stringResource(submitTextId), + onSubmitClick = onSubmitClick, + isCritical = isCritical, + ) +} + @PreviewsDayNight @Composable internal fun IdentityChangeStateViewPreview( @@ -93,6 +124,6 @@ internal fun IdentityChangeStateViewPreview( ) = ElementPreview { IdentityChangeStateView( state = state, - onLinkClick = {}, + onLinkClick = { _, _ -> }, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityRoomMember.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityRoomMember.kt deleted file mode 100644 index 536a1072a70..00000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityRoomMember.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.messages.impl.crypto.identity - -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.matrix.api.core.UserId - -data class IdentityRoomMember( - val userId: UserId, - val displayNameOrDefault: String, - val avatarData: AvatarData, -) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt index a72baa9e003..cdd91e86e42 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -33,9 +33,9 @@ internal fun MessagesViewWithIdentityChangePreview( ), onBackClick = {}, onRoomDetailsClick = {}, - onEventContentClick = { false }, + onEventContentClick = { _, _ -> false }, onUserDataClick = {}, - onLinkClick = {}, + onLinkClick = { _, _ -> }, onSendLocationClick = {}, onCreatePollClick = {}, onJoinCallClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailureFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailureFactory.kt index c1fe6fffb57..10523324038 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailureFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailureFactory.kt @@ -7,12 +7,12 @@ package io.element.android.features.messages.impl.crypto.sendfailure -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import javax.inject.Inject class VerifiedUserSendFailureFactory @Inject constructor( - private val room: MatrixRoom, + private val room: BaseRoom, ) { suspend fun create( sendState: LocalEventSendState?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenter.kt index e947af83464..0373002f7e9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenter.kt @@ -19,13 +19,13 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser 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.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import kotlinx.coroutines.launch import javax.inject.Inject class ResolveVerifiedUserSendFailurePresenter @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val verifiedUserSendFailureFactory: VerifiedUserSendFailureFactory, ) : Presenter { @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureResolver.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureResolver.kt index 29d6d7a186f..166ce212ddd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureResolver.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureResolver.kt @@ -10,7 +10,7 @@ package io.element.android.features.messages.impl.crypto.sendfailure.resolve import androidx.compose.runtime.mutableStateOf import io.element.android.libraries.matrix.api.core.SendHandle import io.element.android.libraries.matrix.api.core.TransactionId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import timber.log.Timber @@ -21,7 +21,7 @@ import timber.log.Timber * This way, the user can resolve and resend the message for each user concerned, one by one. */ class VerifiedUserSendFailureResolver( - private val room: MatrixRoom, + private val room: JoinedRoom, private val transactionId: TransactionId, private val sendHandle: SendHandle, private val iterator: VerifiedUserSendFailureIterator, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt index f06a743c6d4..ed84ef9df04 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt @@ -14,6 +14,8 @@ import io.element.android.features.messages.impl.crypto.identity.IdentityChangeS import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState +import io.element.android.features.messages.impl.link.LinkPresenter +import io.element.android.features.messages.impl.link.LinkState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter @@ -46,6 +48,9 @@ interface MessagesModule { @Binds fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter + @Binds + fun bindLinkPresenter(presenter: LinkPresenter): Presenter + @Binds fun bindVoiceMessageComposerPresenter(presenter: VoiceMessageComposerPresenter): Presenter diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt index d34651f7a41..489e5bd0462 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt @@ -8,9 +8,10 @@ package io.element.android.features.messages.impl.draft 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.room.draft.ComposerDraft interface ComposerDraftService { - suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? - suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) + suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?, isVolatile: Boolean): ComposerDraft? + suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?, isVolatile: Boolean) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftStore.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftStore.kt index d1297c10562..c460acb0654 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftStore.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftStore.kt @@ -8,9 +8,10 @@ package io.element.android.features.messages.impl.draft 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.room.draft.ComposerDraft interface ComposerDraftStore { - suspend fun loadDraft(roomId: RoomId): ComposerDraft? - suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) + suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?): ComposerDraft? + suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt index 1499aeac1d2..57909c5a8a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.draft import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.RoomScope 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.room.draft.ComposerDraft import javax.inject.Inject @@ -18,12 +19,12 @@ class DefaultComposerDraftService @Inject constructor( private val volatileComposerDraftStore: VolatileComposerDraftStore, private val matrixComposerDraftStore: MatrixComposerDraftStore, ) : ComposerDraftService { - override suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? { - return getStore(isVolatile).loadDraft(roomId) + override suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?, isVolatile: Boolean): ComposerDraft? { + return getStore(isVolatile).loadDraft(roomId, threadRoot) } - override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) { - getStore(isVolatile).updateDraft(roomId, draft) + override suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?, isVolatile: Boolean) { + getStore(isVolatile).updateDraft(roomId, threadRoot, draft) } private fun getStore(isVolatile: Boolean): ComposerDraftStore { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt index b850e7b5596..88000546dde 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.draft 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.ThreadId import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import timber.log.Timber import javax.inject.Inject @@ -20,26 +21,26 @@ import javax.inject.Inject class MatrixComposerDraftStore @Inject constructor( private val client: MatrixClient, ) : ComposerDraftStore { - override suspend fun loadDraft(roomId: RoomId): ComposerDraft? { + override suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?): ComposerDraft? { return client.getRoom(roomId)?.use { room -> - room.loadComposerDraft() + room.loadComposerDraft(threadRoot) .onFailure { Timber.e(it, "Failed to load composer draft for room $roomId") } .onSuccess { draft -> - room.clearComposerDraft() + room.clearComposerDraft(threadRoot) Timber.d("Loaded composer draft for room $roomId : $draft") } .getOrNull() } } - override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) { + override suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?) { client.getRoom(roomId)?.use { room -> val updateDraftResult = if (draft == null) { - room.clearComposerDraft() + room.clearComposerDraft(threadRoot) } else { - room.saveComposerDraft(draft) + room.saveComposerDraft(draft, threadRoot) } updateDraftResult .onFailure { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt index 568f68a2d64..b7b714f5c9c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.draft 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.room.draft.ComposerDraft import javax.inject.Inject @@ -17,18 +18,20 @@ import javax.inject.Inject * Currently it's used to store draft message when moving to edit mode. */ class VolatileComposerDraftStore @Inject constructor() : ComposerDraftStore { - private val drafts: MutableMap = mutableMapOf() + private val drafts: MutableMap = mutableMapOf() - override suspend fun loadDraft(roomId: RoomId): ComposerDraft? { + override suspend fun loadDraft(roomId: RoomId, threadRoot: ThreadId?): ComposerDraft? { + val key = threadRoot?.value ?: roomId.value // Remove the draft from the map when it is loaded - return drafts.remove(roomId) + return drafts.remove(key) } - override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) { + override suspend fun updateDraft(roomId: RoomId, threadRoot: ThreadId?, draft: ComposerDraft?) { + val key = threadRoot?.value ?: roomId.value if (draft == null) { - drafts.remove(roomId) + drafts.remove(key) } else { - drafts[roomId] = draft + drafts[key] = draft } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt index 960d54487af..013003e4b7e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt @@ -16,6 +16,7 @@ import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.timeline.TimelineProvider @@ -28,7 +29,8 @@ import kotlinx.coroutines.launch class ForwardMessagesPresenter @AssistedInject constructor( @Assisted eventId: String, @Assisted private val timelineProvider: TimelineProvider, - private val appCoroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, ) : Presenter { private val eventId: EventId = EventId(eventId) @@ -40,7 +42,7 @@ class ForwardMessagesPresenter @AssistedInject constructor( private val forwardingActionState: MutableState>> = mutableStateOf(AsyncAction.Uninitialized) fun onRoomSelected(roomIds: List) { - appCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState) + sessionCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState) } @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt index 9172a6c5aca..b1728f46572 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt @@ -24,7 +24,7 @@ open class ForwardMessagesStateProvider : PreviewParameterProvider { + @Composable + override fun present(): LinkState { + val linkClick: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + + fun handleEvents(linkEvents: LinkEvents) { + when (linkEvents) { + is LinkEvents.OnLinkClick -> { + linkClick.value = AsyncAction.Loading + val result = linkChecker.isSafe(linkEvents.link) + if (result) { + linkClick.value = AsyncAction.Success(linkEvents.link) + } else { + // Confirm first + linkClick.value = ConfirmingLinkClick(linkEvents.link) + } + } + LinkEvents.Confirm -> { + linkClick.value = (linkClick.value as? ConfirmingLinkClick) + ?.let { AsyncAction.Success(it.link) } + ?: AsyncAction.Uninitialized + } + LinkEvents.Cancel -> { + linkClick.value = AsyncAction.Uninitialized + } + } + } + return LinkState( + linkClick = linkClick.value, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkState.kt new file mode 100644 index 00000000000..09863780960 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkState.kt @@ -0,0 +1,16 @@ +/* + * 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.messages.impl.link + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.wysiwyg.link.Link + +data class LinkState( + val linkClick: AsyncAction, + val eventSink: (LinkEvents) -> Unit, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkStateProvider.kt new file mode 100644 index 00000000000..478f8bf322e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkStateProvider.kt @@ -0,0 +1,35 @@ +/* + * 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.messages.impl.link + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.wysiwyg.link.Link + +open class LinkStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLinkState(), + aLinkState( + linkClick = ConfirmingLinkClick( + Link( + url = "https://evil.io", + text = "https://element.io" + ), + ), + ), + ) +} + +fun aLinkState( + linkClick: AsyncAction = AsyncAction.Uninitialized, + eventSink: (LinkEvents) -> Unit = {}, +) = LinkState( + linkClick = linkClick, + eventSink = eventSink, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkView.kt new file mode 100644 index 00000000000..53306c8e997 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkView.kt @@ -0,0 +1,73 @@ +/* + * 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.messages.impl.link + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.core.extensions.ensureEndsLeftToRight +import io.element.android.libraries.core.extensions.filterDirectionOverrides +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.wysiwyg.link.Link + +@Composable +fun LinkView( + state: LinkState, + onLinkValid: (Link) -> Unit, + modifier: Modifier = Modifier, +) { + when (state.linkClick) { + AsyncAction.Uninitialized, + AsyncAction.Loading, + is AsyncAction.Failure -> Unit + is AsyncAction.Confirming -> { + if (state.linkClick is ConfirmingLinkClick) { + ConfirmationDialog( + modifier = modifier, + title = stringResource(CommonStrings.dialog_confirm_link_title), + content = stringResource( + CommonStrings.dialog_confirm_link_message, + state.linkClick.link.text.ensureEndsLeftToRight(), + state.linkClick.link.url.filterDirectionOverrides(), + ), + submitText = stringResource(CommonStrings.action_continue), + onSubmitClick = { + state.eventSink(LinkEvents.Confirm) + }, + onDismiss = { + state.eventSink(LinkEvents.Cancel) + }, + ) + } + } + is AsyncAction.Success -> { + val latestOnLinkValid by rememberUpdatedState(onLinkValid) + LaunchedEffect(state.linkClick.data) { + latestOnLinkValid(state.linkClick.data) + state.eventSink(LinkEvents.Cancel) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun LinkViewPreview(@PreviewParameter(LinkStateProvider::class) state: LinkState) = ElementPreview { + LinkView( + state = state, + onLinkValid = {}, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/DisabledComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/DisabledComposerView.kt new file mode 100644 index 00000000000..02d009d2cee --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/DisabledComposerView.kt @@ -0,0 +1,94 @@ +/* + * 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.messages.impl.messagecomposer + +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.IntrinsicSize +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.requiredHeightIn +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.Icon +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.IconColorButtonStyle + +@Composable +internal fun DisabledComposerView( + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .padding(3.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + IconColorButton( + onClick = {}, + imageVector = CompoundIcons.Plus(), + contentDescription = null, + iconColorButtonStyle = IconColorButtonStyle.Disabled, + ) + + val bgColor = ElementTheme.colors.bgCanvasDisabled + val borderColor = ElementTheme.colors.borderDisabled + + Box( + modifier = Modifier + .clip(RoundedCornerShape(21.dp)) + .border(0.5.dp, borderColor, RoundedCornerShape(21.dp)) + .background(color = bgColor) + .size(42.dp) + .requiredHeightIn(min = 42.dp) + .weight(1f), + ) + + Spacer(modifier = Modifier.width(8.dp)) + IconButton( + modifier = Modifier + .padding(start = 2.dp) + .size(48.dp), + enabled = false, + onClick = {}, + ) { + Icon( + modifier = Modifier.size(30.dp), + imageVector = CompoundIcons.SendSolid(), + contentDescription = "", + tint = ElementTheme.colors.iconQuaternary + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun DisabledComposerViewPreview() = ElementPreview { + Column { + DisabledComposerView( + modifier = Modifier.height(IntrinsicSize.Min), + ) + } +} 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 94aecec7781..f7187c50212 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 @@ -23,13 +23,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.media3.common.MimeTypes +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.media3.common.util.UnstableApi import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.Interaction +import io.element.android.features.location.api.LocationService import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError @@ -39,22 +40,24 @@ import io.element.android.features.messages.impl.messagecomposer.suggestions.Sug import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.utils.TextPillificationHelper import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +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.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder -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.room.IntentionalMention -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.isDm +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId -import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.mediapickers.api.PickerProvider @@ -63,7 +66,6 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.preferences.api.store.SessionPreferencesStore -import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState @@ -83,11 +85,13 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber import kotlin.time.Duration.Companion.seconds @@ -95,8 +99,9 @@ import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes class MessageComposerPresenter @AssistedInject constructor( @Assisted private val navigator: MessagesNavigator, - private val appCoroutineScope: CoroutineScope, - private val room: MatrixRoom, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, + private val room: JoinedRoom, private val mediaPickerProvider: PickerProvider, private val featureFlagService: FeatureFlagService, private val sessionPreferencesStore: SessionPreferencesStore, @@ -104,6 +109,7 @@ class MessageComposerPresenter @AssistedInject constructor( private val mediaSender: MediaSender, private val snackbarDispatcher: SnackbarDispatcher, private val analyticsService: AnalyticsService, + private val locationService: LocationService, private val messageComposerContext: DefaultMessageComposerContext, private val richTextEditorStateFactory: RichTextEditorStateFactory, private val roomAliasSuggestionsDataSource: RoomAliasSuggestionsDataSource, @@ -114,7 +120,6 @@ class MessageComposerPresenter @AssistedInject constructor( private val draftService: ComposerDraftService, private val mentionSpanProvider: MentionSpanProvider, private val pillificationHelper: TextPillificationHelper, - private val roomMemberProfilesCache: RoomMemberProfilesCache, private val suggestionsProcessor: SuggestionsProcessor, ) : Presenter { @AssistedFactory @@ -133,29 +138,25 @@ class MessageComposerPresenter @AssistedInject constructor( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var showTextFormatting: Boolean by mutableStateOf(false) - @OptIn(FlowPreview::class) @SuppressLint("UnsafeOptInUsageError") @Composable override fun present(): MessageComposerState { val localCoroutineScope = rememberCoroutineScope() + val roomInfo by room.roomInfoFlow.collectAsState() + val richTextEditorState = richTextEditorStateFactory.remember() if (isTesting) { richTextEditorState.isReadyToProcessActions = true } val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false) - var isMentionsEnabled by remember { mutableStateOf(false) } - var isRoomAliasSuggestionsEnabled by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - isMentionsEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.Mentions) - isRoomAliasSuggestionsEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.RoomAliasSuggestions) - } val cameraPermissionState = cameraPermissionPresenter.present() val canShareLocation = remember { mutableStateOf(false) } LaunchedEffect(Unit) { - canShareLocation.value = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing) + canShareLocation.value = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing) && + locationService.isServiceAvailable() } val canCreatePoll = remember { mutableStateOf(false) } @@ -167,22 +168,22 @@ class MessageComposerPresenter @AssistedInject constructor( handlePickedMedia(uri, mimeType) } val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri -> - handlePickedMedia(uri) + handlePickedMedia(uri, MimeTypes.OctetStream) } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri -> - handlePickedMedia(uri, MimeTypes.IMAGE_JPEG) + handlePickedMedia(uri, MimeTypes.Jpeg) } val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri -> - handlePickedMedia(uri, MimeTypes.VIDEO_MP4) + handlePickedMedia(uri, MimeTypes.Mp4) } val isFullScreen = rememberSaveable { mutableStateOf(false) } var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) } - val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true) - - val roomAliasSuggestions by roomAliasSuggestionsDataSource.getAllRoomAliasSuggestions().collectAsState(initial = emptyList()) + val sendTypingNotifications by remember { + sessionPreferencesStore.isSendTypingNotificationsEnabled() + }.collectAsState(initial = true) LaunchedEffect(cameraPermissionState.permissionGranted) { if (cameraPermissionState.permissionGranted) { @@ -196,40 +197,12 @@ class MessageComposerPresenter @AssistedInject constructor( } val suggestions = remember { mutableStateListOf() } - LaunchedEffect(isMentionsEnabled) { - if (!isMentionsEnabled) return@LaunchedEffect - val currentUserId = room.sessionId - - suspend fun canSendRoomMention(): Boolean { - val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false) - return !room.isDm && userCanSendAtRoom - } - - // This will trigger a search immediately when `@` is typed - val mentionStartTrigger = suggestionSearchTrigger.filter { it?.text.isNullOrEmpty() } - // This will start a search when the user changes the text after the `@` with a debounce to prevent too much wasted work - val mentionCompletionTrigger = suggestionSearchTrigger.debounce(0.3.seconds).filter { !it?.text.isNullOrEmpty() } - merge(mentionStartTrigger, mentionCompletionTrigger) - .combine(room.membersStateFlow) { suggestion, roomMembersState -> - suggestions.clear() - val result = suggestionsProcessor.process( - suggestion = suggestion, - roomMembersState = roomMembersState, - roomAliasSuggestions = if (isRoomAliasSuggestionsEnabled) roomAliasSuggestions else emptyList(), - currentUserId = currentUserId, - canSendRoomMention = ::canSendRoomMention, - ) - if (result.isNotEmpty()) { - suggestions.addAll(result) - } - } - .collect() - } + ResolveSuggestionsEffect(suggestions) DisposableEffect(Unit) { // Declare that the user is not typing anymore when the composer is disposed onDispose { - appCoroutineScope.launch { + sessionCoroutineScope.launch { if (sendTypingNotifications) { room.typingNotice(false) } @@ -239,14 +212,19 @@ class MessageComposerPresenter @AssistedInject constructor( val textEditorState by rememberUpdatedState( if (showTextFormatting) { - TextEditorState.Rich(richTextEditorState) + TextEditorState.Rich(richTextEditorState, roomInfo.isEncrypted == true) } else { - TextEditorState.Markdown(markdownTextEditorState) + TextEditorState.Markdown(markdownTextEditorState, roomInfo.isEncrypted == true) } ) LaunchedEffect(Unit) { - val draft = draftService.loadDraft(room.roomId, isVolatile = false) + val draft = draftService.loadDraft( + roomId = room.roomId, + // TODO support threads in composer + threadRoot = null, + isVolatile = false + ) if (draft != null) { applyDraft(draft, markdownTextEditorState, richTextEditorState) } @@ -265,12 +243,12 @@ class MessageComposerPresenter @AssistedInject constructor( } } is MessageComposerEvents.SendMessage -> { - appCoroutineScope.sendMessage( + sessionCoroutineScope.sendMessage( markdownTextEditorState = markdownTextEditorState, richTextEditorState = richTextEditorState, ) } - is MessageComposerEvents.SendUri -> appCoroutineScope.sendAttachment( + is MessageComposerEvents.SendUri -> sessionCoroutineScope.sendAttachment( attachment = Attachment.Media( localMedia = localMediaFactory.createFromUri( uri = event.uri, @@ -360,7 +338,6 @@ class MessageComposerPresenter @AssistedInject constructor( markdownTextEditorState.insertSuggestion( resolvedSuggestion = event.resolvedSuggestion, mentionSpanProvider = mentionSpanProvider, - permalinkBuilder = permalinkBuilder, ) suggestionSearchTrigger.value = null } @@ -368,27 +345,29 @@ class MessageComposerPresenter @AssistedInject constructor( } MessageComposerEvents.SaveDraft -> { val draft = createDraftFromState(markdownTextEditorState, richTextEditorState) - appCoroutineScope.updateDraft(draft, isVolatile = false) + sessionCoroutineScope.updateDraft(draft, isVolatile = false) } } } - val mentionSpanTheme = LocalMentionSpanTheme.current - val resolveMentionDisplay = remember(mentionSpanTheme) { + val resolveMentionDisplay = remember { { text: String, url: String -> - val permalinkData = permalinkParser.parse(url) - if (permalinkData is PermalinkData.UserLink) { - val displayNameOrId = roomMemberProfilesCache.getDisplayName(permalinkData.userId) ?: permalinkData.userId.value - val mentionSpan = mentionSpanProvider.getMentionSpanFor(displayNameOrId, url) - mentionSpan.update(mentionSpanTheme) + val mentionSpan = mentionSpanProvider.getMentionSpanFor(text, url) + if (mentionSpan != null) { TextDisplay.Custom(mentionSpan) } else { - val mentionSpan = mentionSpanProvider.getMentionSpanFor(text, url) - mentionSpan.update(mentionSpanTheme) - TextDisplay.Custom(mentionSpan) + TextDisplay.Plain } } } + + val resolveAtRoomMentionDisplay = remember { + { + val mentionSpan = mentionSpanProvider.createEveryoneMentionSpan() + TextDisplay.Custom(mentionSpan) + } + } + return MessageComposerState( textEditorState = textEditorState, isFullScreen = isFullScreen.value, @@ -399,10 +378,50 @@ class MessageComposerPresenter @AssistedInject constructor( canCreatePoll = canCreatePoll.value, suggestions = suggestions.toPersistentList(), resolveMentionDisplay = resolveMentionDisplay, + resolveAtRoomMentionDisplay = resolveAtRoomMentionDisplay, eventSink = { handleEvents(it) }, ) } + @OptIn(FlowPreview::class) + @Composable + private fun ResolveSuggestionsEffect( + suggestions: SnapshotStateList, + ) { + LaunchedEffect(Unit) { + val currentUserId = room.sessionId + + suspend fun canSendRoomMention(): Boolean { + val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false) + return !room.isDm() && userCanSendAtRoom + } + + // This will trigger a search immediately when `@` is typed + val mentionStartTrigger = suggestionSearchTrigger.filter { it?.text.isNullOrEmpty() } + // This will start a search when the user changes the text after the `@` with a debounce to prevent too much wasted work + val mentionCompletionTrigger = suggestionSearchTrigger.debounce(0.3.seconds).filter { !it?.text.isNullOrEmpty() } + + val mentionTriggerFlow = merge(mentionStartTrigger, mentionCompletionTrigger) + + val roomAliasSuggestionsFlow = roomAliasSuggestionsDataSource + .getAllRoomAliasSuggestions() + .stateIn(this, SharingStarted.Lazily, emptyList()) + + combine(mentionTriggerFlow, room.membersStateFlow, roomAliasSuggestionsFlow) { suggestion, roomMembersState, roomAliasSuggestions -> + val result = suggestionsProcessor.process( + suggestion = suggestion, + roomMembersState = roomMembersState, + roomAliasSuggestions = roomAliasSuggestions, + currentUserId = currentUserId, + canSendRoomMention = ::canSendRoomMention, + ) + suggestions.clear() + suggestions.addAll(result) + } + .collect() + } + } + private fun CoroutineScope.sendMessage( markdownTextEditorState: MarkdownTextEditorState, richTextEditorState: RichTextEditorState, @@ -413,7 +432,7 @@ class MessageComposerPresenter @AssistedInject constructor( resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit) when (capturedMode) { is MessageComposerMode.Attachment, - is MessageComposerMode.Normal -> room.sendMessage( + is MessageComposerMode.Normal -> room.liveTimeline.sendMessage( body = message.markdown, htmlBody = message.html, intentionalMentions = message.intentionalMentions @@ -442,7 +461,19 @@ class MessageComposerPresenter @AssistedInject constructor( } is MessageComposerMode.Reply -> { timelineController.invokeOnCurrentTimeline { - replyMessage(capturedMode.eventId, message.markdown, message.html, message.intentionalMentions) + with(capturedMode) { + replyMessage( + body = message.markdown, + htmlBody = message.html, + intentionalMentions = message.intentionalMentions, + replyParameters = ReplyParameters( + inReplyToEventId = eventId, + enforceThreadReply = inThread, + // This should be false until we add a way to make a reply in a thread an explicit reply to the provided eventId + replyWithinThread = false, + ), + ) + } } } } @@ -489,7 +520,7 @@ class MessageComposerPresenter @AssistedInject constructor( private suspend fun sendMedia( uri: Uri, mimeType: String, - ) = runCatching { + ) = runCatchingExceptions { mediaSender.sendMedia( uri = uri, mimeType = mimeType, @@ -513,7 +544,9 @@ class MessageComposerPresenter @AssistedInject constructor( draftService.updateDraft( roomId = room.roomId, draft = draft, - isVolatile = isVolatile + isVolatile = isVolatile, + // TODO support threads in composer + threadRoot = null, ) } @@ -629,8 +662,8 @@ class MessageComposerPresenter @AssistedInject constructor( analyticsService.captureInteraction(Interaction.Name.MobileRoomComposerFormattingEnabled) } else { val markdown = richTextEditorState.messageMarkdown - val pilliefiedMarkdown = pillificationHelper.pillify(markdown) - markdownTextEditorState.text.update(pilliefiedMarkdown, true) + val markdownWithMentions = pillificationHelper.pillify(markdown, false) + markdownTextEditorState.text.update(markdownWithMentions, true) // Give some time for the focus of the previous editor to be cleared delay(100) markdownTextEditorState.requestFocusAction() @@ -674,7 +707,12 @@ class MessageComposerPresenter @AssistedInject constructor( fromEdit: Boolean, ) { // Use the volatile draft only when coming from edit mode otherwise. - val draft = draftService.loadDraft(room.roomId, isVolatile = true).takeIf { fromEdit } + val draft = draftService.loadDraft( + roomId = room.roomId, + // TODO support threads in composer + threadRoot = null, + isVolatile = true + ).takeIf { fromEdit } if (draft != null) { applyDraft(draft, markdownTextEditorState, richTextEditorState) } else { @@ -698,7 +736,7 @@ class MessageComposerPresenter @AssistedInject constructor( if (content.isEmpty()) { markdownTextEditorState.selection = IntRange.EMPTY } - val pillifiedContent = pillificationHelper.pillify(content) + val pillifiedContent = pillificationHelper.pillify(content, false) markdownTextEditorState.text.update(pillifiedContent, true) if (requestFocus) { markdownTextEditorState.requestFocusAction() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index d86ba8d98c4..1f1f80b2032 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -25,5 +25,6 @@ data class MessageComposerState( val canCreatePoll: Boolean, val suggestions: ImmutableList, val resolveMentionDisplay: (String, String) -> TextDisplay, + val resolveAtRoomMentionDisplay: () -> TextDisplay, val eventSink: (MessageComposerEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index c0ea895618e..590cfd20cf3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -43,5 +43,6 @@ fun aMessageComposerState( canCreatePoll = canCreatePoll, suggestions = suggestions, resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, + resolveAtRoomMentionDisplay = { TextDisplay.Plain }, eventSink = eventSink, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 86e30a22c04..b3d8e5bf2dc 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 @@ -33,7 +33,6 @@ import kotlinx.coroutines.launch internal fun MessageComposerView( state: MessageComposerState, voiceMessageState: VoiceMessageComposerState, - subcomposing: Boolean, enableVoiceMessages: Boolean, modifier: Modifier = Modifier, ) { @@ -98,7 +97,6 @@ internal fun MessageComposerView( modifier = modifier, state = state.textEditorState, voiceMessageState = voiceMessageState.voiceMessageState, - subcomposing = subcomposing, onRequestFocus = ::onRequestFocus, onSendMessage = ::sendMessage, composerMode = state.mode, @@ -113,6 +111,7 @@ internal fun MessageComposerView( onDeleteVoiceMessage = onDeleteVoiceMessage, onReceiveSuggestion = ::onSuggestionReceived, resolveMentionDisplay = state.resolveMentionDisplay, + resolveAtRoomMentionDisplay = state.resolveAtRoomMentionDisplay, onError = ::onError, onTyping = ::onTyping, onSelectRichContent = ::sendUri, @@ -130,15 +129,14 @@ internal fun MessageComposerViewPreview( state = state, voiceMessageState = aVoiceMessageComposerState(), enableVoiceMessages = true, - subcomposing = false, ) MessageComposerView( modifier = Modifier.height(200.dp), state = state, voiceMessageState = aVoiceMessageComposerState(), enableVoiceMessages = true, - subcomposing = false, ) + DisabledComposerView() } } @@ -153,7 +151,6 @@ internal fun MessageComposerViewVoicePreview( state = aMessageComposerState(), voiceMessageState = state, enableVoiceMessages = true, - subcomposing = false, ) } } 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 12379163a63..93703377eb1 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 @@ -27,6 +27,8 @@ import io.element.android.features.messages.impl.R 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.anAvatarData import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider @@ -45,7 +47,7 @@ import kotlinx.collections.immutable.persistentListOf fun SuggestionsPickerView( roomId: RoomId, roomName: String?, - roomAvatarData: AvatarData?, + roomAvatarData: AvatarData, suggestions: ImmutableList, onSelectSuggestion: (ResolvedSuggestion) -> Unit, modifier: Modifier = Modifier, @@ -97,6 +99,11 @@ private fun SuggestionItemView( is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize) is ResolvedSuggestion.Alias -> suggestion.getAvatarData(avatarSize) } + val avatarType = when (suggestion) { + is ResolvedSuggestion.Alias -> AvatarType.Room() + ResolvedSuggestion.AtRoom, + is ResolvedSuggestion.Member -> AvatarType.User + } val title = when (suggestion) { is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title) is ResolvedSuggestion.Member -> suggestion.roomMember.displayName @@ -107,9 +114,11 @@ private fun SuggestionItemView( is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value is ResolvedSuggestion.Alias -> suggestion.roomAlias.value } - - Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)) - + Avatar( + avatarData = avatarData, + avatarType = avatarType, + modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp), + ) Column( modifier = Modifier .fillMaxWidth() @@ -149,12 +158,13 @@ internal fun SuggestionsPickerViewPreview() { normalizedPowerLevel = 0L, isIgnored = false, role = RoomMember.Role.USER, + membershipChangeReason = null, ) val anAlias = remember { RoomAlias("#room:domain.org") } SuggestionsPickerView( roomId = RoomId("!room:matrix.org"), roomName = "Room", - roomAvatarData = null, + roomAvatarData = anAvatarData(), suggestions = persistentListOf( ResolvedSuggestion.AtRoom, ResolvedSuggestion.Member(roomMember), 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 2729f5e7765..5055d09a3b9 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 @@ -9,8 +9,8 @@ package io.element.android.features.messages.impl.messagecomposer.suggestions import io.element.android.libraries.core.data.filterUpTo import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion @@ -33,7 +33,7 @@ class SuggestionsProcessor @Inject constructor() { */ suspend fun process( suggestion: Suggestion?, - roomMembersState: MatrixRoomMembersState, + roomMembersState: RoomMembersState, roomAliasSuggestions: List, currentUserId: UserId, canSendRoomMention: suspend () -> Boolean, @@ -53,7 +53,11 @@ class SuggestionsProcessor @Inject constructor() { } SuggestionType.Room -> { roomAliasSuggestions - .filter { it.roomAlias.value.contains(suggestion.text, ignoreCase = true) } + .filter { roomAliasSuggestion -> + // Filter by either room alias or room name (if available) + roomAliasSuggestion.roomAlias.value.contains(suggestion.text, ignoreCase = true) || + roomAliasSuggestion.roomName?.contains(suggestion.text, ignoreCase = true) == true + } .map { ResolvedSuggestion.Alias( roomAlias = it.roomAlias, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt index 8840411a24a..5210631409c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt @@ -8,12 +8,14 @@ package io.element.android.features.messages.impl.pinned import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.sync.SyncService import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineProvider @@ -26,13 +28,15 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext import javax.inject.Inject @SingleIn(RoomScope::class) class PinnedEventsTimelineProvider @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val syncService: SyncService, private val featureFlagService: FeatureFlagService, + private val dispatchers: CoroutineDispatchers, ) : TimelineProvider { private val _timelineStateFlow: MutableStateFlow> = MutableStateFlow(AsyncData.Uninitialized) @@ -58,6 +62,7 @@ class PinnedEventsTimelineProvider @Inject constructor( } } .launchIn(scope) + .invokeOnCompletion { timelineStateFlow.value.dataOrNull()?.close() } } private suspend fun onActive() = coroutineScope { @@ -100,7 +105,9 @@ class PinnedEventsTimelineProvider @Inject constructor( when (timelineStateFlow.value) { is AsyncData.Uninitialized, is AsyncData.Failure -> { timelineStateFlow.emit(AsyncData.Loading()) - room.pinnedEventsTimeline() + withContext(dispatchers.io) { + room.createTimeline(CreateTimelineParams.PinnedOnly) + } .fold( { timelineStateFlow.emit(AsyncData.Success(it)) }, { timelineStateFlow.emit(AsyncData.Failure(it)) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt index 2a446df0e84..21a137b363b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt @@ -20,7 +20,7 @@ import androidx.compose.runtime.setValue import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.onEach import javax.inject.Inject class PinnedMessagesBannerPresenter @Inject constructor( - private val room: MatrixRoom, + private val room: BaseRoom, private val itemFactory: PinnedMessagesBannerItemFactory, private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider, ) : Presenter { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt index ff36ba8999b..785de159544 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt @@ -10,14 +10,31 @@ package io.element.android.features.messages.impl.pinned.banner import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import io.element.android.libraries.designsystem.text.toAnnotatedString import io.element.android.libraries.ui.strings.CommonStrings @Immutable sealed interface PinnedMessagesBannerState { data object Hidden : PinnedMessagesBannerState - sealed interface Visible : PinnedMessagesBannerState + @Immutable + sealed interface Visible : PinnedMessagesBannerState { + fun pinnedMessagesCount() = when (this) { + is Loading -> expectedPinnedMessagesCount + is Loaded -> loadedPinnedMessagesCount + } + + fun currentPinnedMessageIndex() = when (this) { + is Loading -> expectedPinnedMessagesCount - 1 + is Loaded -> currentPinnedMessageIndex + } + + @Composable + fun formattedMessage() = when (this) { + is Loading -> stringResource(id = CommonStrings.screen_room_pinned_banner_loading_description).toAnnotatedString() + is Loaded -> currentPinnedMessage.formatted + } + } + data class Loading(val expectedPinnedMessagesCount: Int) : Visible data class Loaded( val currentPinnedMessage: PinnedMessagesBannerItem, @@ -25,23 +42,4 @@ sealed interface PinnedMessagesBannerState { val loadedPinnedMessagesCount: Int, val eventSink: (PinnedMessagesBannerEvents) -> Unit ) : Visible - - fun pinnedMessagesCount() = when (this) { - is Hidden -> 0 - is Loading -> expectedPinnedMessagesCount - is Loaded -> loadedPinnedMessagesCount - } - - fun currentPinnedMessageIndex() = when (this) { - is Hidden -> 0 - is Loading -> expectedPinnedMessagesCount - 1 - is Loaded -> currentPinnedMessageIndex - } - - @Composable - fun formattedMessage() = when (this) { - is Hidden -> AnnotatedString("") - is Loading -> stringResource(id = CommonStrings.screen_room_pinned_banner_loading_description).toAnnotatedString() - is Loaded -> currentPinnedMessage.formatted - } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt index 2e4bd922899..194cc433f4d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt @@ -78,7 +78,7 @@ fun PinnedMessagesBannerView( @Composable private fun PinnedMessagesBannerRow( - state: PinnedMessagesBannerState, + state: PinnedMessagesBannerState.Visible, onClick: (EventId) -> Unit, onViewAllClick: () -> Unit, modifier: Modifier = Modifier, @@ -108,7 +108,7 @@ private fun PinnedMessagesBannerRow( Icon( imageVector = CompoundIcons.PinSolid(), contentDescription = null, - tint = ElementTheme.materialColors.secondary, + tint = ElementTheme.colors.iconSecondary, modifier = Modifier .padding(horizontal = 10.dp) .size(20.dp) @@ -265,7 +265,7 @@ internal interface PinnedMessagesBannerViewScrollBehavior { internal object PinnedMessagesBannerViewDefaults { @Composable - fun rememberExitOnScrollBehavior(): PinnedMessagesBannerViewScrollBehavior = remember { + fun rememberScrollBehavior(pinnedMessagesCount: Int): PinnedMessagesBannerViewScrollBehavior = remember(pinnedMessagesCount) { ExitOnScrollBehavior() } } 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 201ec019540..2dbf76bc8d5 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 @@ -8,10 +8,12 @@ package io.element.android.features.messages.impl.pinned.list import android.content.Context +import android.view.HapticFeedbackConstants import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -23,6 +25,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId @@ -30,6 +33,8 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.ui.strings.CommonStrings @ContributesNode(RoomScope::class) class PinnedMessagesListNode @AssistedInject constructor( @@ -59,8 +64,8 @@ class PinnedMessagesListNode @AssistedInject constructor( return callbacks.forEach { it.onEventClick(event) } } - private fun onUserDataClick(userId: UserId) { - callbacks.forEach { it.onUserDataClick(userId) } + private fun onUserDataClick(user: MatrixUser) { + callbacks.forEach { it.onUserDataClick(user.userId) } } private fun onLinkClick(context: Context, url: String) { @@ -98,13 +103,23 @@ class PinnedMessagesListNode @AssistedInject constructor( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { val context = LocalContext.current + val view = LocalView.current val state = presenter.present() PinnedMessagesListView( state = state, onBackClick = ::navigateUp, onEventClick = ::onEventClick, onUserDataClick = ::onUserDataClick, - onLinkClick = { url -> onLinkClick(context, url) }, + onLinkClick = { link -> onLinkClick(context, link.url) }, + onLinkLongClick = { + view.performHapticFeedback( + HapticFeedbackConstants.LONG_PRESS + ) + context.copyToClipboard( + it.url, + context.getString(CommonStrings.common_copied_to_clipboard) + ) + }, modifier = modifier ) } 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 d00e9e7d1af..1c9e18c9e6e 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 @@ -25,6 +25,7 @@ import im.vector.app.features.analytics.plan.PinUnpinAction import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.link.LinkState import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory @@ -37,12 +38,13 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.ui.room.isDmAsState 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 @@ -59,13 +61,15 @@ import timber.log.Timber class PinnedMessagesListPresenter @AssistedInject constructor( @Assisted private val navigator: PinnedMessagesListNavigator, - private val room: MatrixRoom, + private val room: JoinedRoom, timelineItemsFactoryCreator: TimelineItemsFactory.Creator, private val timelineProvider: PinnedEventsTimelineProvider, private val timelineProtectionPresenter: Presenter, + private val linkPresenter: Presenter, private val snackbarDispatcher: SnackbarDispatcher, @Assisted private val actionListPresenter: Presenter, - private val appCoroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val analyticsService: AnalyticsService, ) : Presenter { @AssistedFactory @@ -85,10 +89,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor( @Composable override fun present(): PinnedMessagesListState { - val timelineRoomInfo = remember { + val isDm by room.isDmAsState() + + val timelineRoomInfo = remember(isDm) { TimelineRoomInfo( - isDm = room.isDm, - name = room.displayName, + isDm = isDm, + name = room.info().name, // We don't need to compute those values userHasPermissionToSendMessage = false, userHasPermissionToSendReaction = false, @@ -100,10 +106,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor( renderTypingNotifications = false, typingMembers = persistentListOf(), reserveSpace = false, - ) + ), + predecessorRoom = room.predecessorRoom(), ) } val timelineProtectionState = timelineProtectionPresenter.present() + val linkState = linkPresenter.present() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userEventPermissions by userEventPermissions(syncUpdateFlow.value) @@ -118,13 +126,14 @@ class PinnedMessagesListPresenter @AssistedInject constructor( fun handleEvents(event: PinnedMessagesListEvents) { when (event) { - is PinnedMessagesListEvents.HandleAction -> appCoroutineScope.handleTimelineAction(event.action, event.event) + is PinnedMessagesListEvents.HandleAction -> sessionCoroutineScope.handleTimelineAction(event.action, event.event) } } return pinnedMessagesListState( timelineRoomInfo = timelineRoomInfo, timelineProtectionState = timelineProtectionState, + linkState = linkState, userEventPermissions = userEventPermissions, timelineItems = pinnedMessageItems, eventSink = ::handleEvents @@ -221,6 +230,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( private fun pinnedMessagesListState( timelineRoomInfo: TimelineRoomInfo, timelineProtectionState: TimelineProtectionState, + linkState: LinkState, userEventPermissions: UserEventPermissions, timelineItems: AsyncData>, eventSink: (PinnedMessagesListEvents) -> Unit @@ -236,6 +246,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( PinnedMessagesListState.Filled( timelineRoomInfo = timelineRoomInfo, timelineProtectionState = timelineProtectionState, + linkState = linkState, userEventPermissions = userEventPermissions, timelineItems = timelineItems.data, actionListState = actionListState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt index feaac1daa4e..d702e2d40f4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.ActionListState +import io.element.android.features.messages.impl.link.LinkState import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState @@ -31,6 +32,7 @@ sealed interface PinnedMessagesListState { val userEventPermissions: UserEventPermissions, val timelineItems: ImmutableList, val actionListState: ActionListState, + val linkState: LinkState, val eventSink: (PinnedMessagesListEvents) -> Unit, ) : PinnedMessagesListState { val loadedPinnedMessagesCount = timelineItems.count { timelineItem -> timelineItem is TimelineItem.Event } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt index e2600e5034f..2a9b7a085cd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState +import io.element.android.features.messages.impl.link.LinkState +import io.element.android.features.messages.impl.link.aLinkState import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineItemDaySeparator import io.element.android.features.messages.impl.timeline.aTimelineItemEvent @@ -86,6 +88,7 @@ fun anEmptyPinnedMessagesListState() = PinnedMessagesListState.Empty fun aLoadedPinnedMessagesListState( timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), + linkState: LinkState = aLinkState(), timelineItems: List = emptyList(), actionListState: ActionListState = anActionListState(), aUserEventPermissions: UserEventPermissions = UserEventPermissions.DEFAULT, @@ -93,6 +96,7 @@ fun aLoadedPinnedMessagesListState( ) = PinnedMessagesListState.Filled( timelineRoomInfo = timelineRoomInfo, timelineProtectionState = timelineProtectionState, + linkState = linkState, timelineItems = timelineItems.toImmutableList(), actionListState = actionListState, userEventPermissions = aUserEventPermissions, 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 a1fd6602516..86cd8c740ff 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 @@ -23,11 +23,12 @@ 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.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.link.LinkEvents +import io.element.android.features.messages.impl.link.LinkView import io.element.android.features.messages.impl.timeline.components.TimelineItemRow import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData @@ -44,20 +45,21 @@ 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.Scaffold -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser 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 io.element.android.wysiwyg.link.Link @Composable fun PinnedMessagesListView( state: PinnedMessagesListState, onBackClick: () -> Unit, onEventClick: (event: TimelineItem.Event) -> Unit, - onUserDataClick: (UserId) -> Unit, - onLinkClick: (String) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -78,6 +80,7 @@ fun PinnedMessagesListView( onEventClick = onEventClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, onErrorDismiss = onBackClick, modifier = Modifier .padding(padding) @@ -95,12 +98,7 @@ private fun PinnedMessagesListTopBar( modifier: Modifier = Modifier, ) { TopAppBar( - title = { - Text( - text = state.title(), - style = ElementTheme.typography.fontBodyLgMedium - ) - }, + titleStr = state.title(), navigationIcon = { BackButton(onClick = onBackClick) }, modifier = modifier, ) @@ -110,8 +108,9 @@ private fun PinnedMessagesListTopBar( private fun PinnedMessagesListContent( state: PinnedMessagesListState, onEventClick: (event: TimelineItem.Event) -> Unit, - onUserDataClick: (UserId) -> Unit, - onLinkClick: (String) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, onErrorDismiss: () -> Unit, modifier: Modifier = Modifier, ) { @@ -130,6 +129,7 @@ private fun PinnedMessagesListContent( onEventClick = onEventClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, ) PinnedMessagesListState.Loading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -164,8 +164,9 @@ private fun PinnedMessagesListEmpty( private fun PinnedMessagesListLoaded( state: PinnedMessagesListState.Filled, onEventClick: (event: TimelineItem.Event) -> Unit, - onUserDataClick: (UserId) -> Unit, - onLinkClick: (String) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, modifier: Modifier = Modifier, ) { fun onActionSelected(timelineItemAction: TimelineItemAction, event: TimelineItem.Event) { @@ -215,7 +216,10 @@ private fun PinnedMessagesListLoaded( isLastOutgoingMessage = false, focusedEventId = null, onUserDataClick = onUserDataClick, - onLinkClick = onLinkClick, + onLinkClick = { link -> + state.linkState.eventSink(LinkEvents.OnLinkClick(link)) + }, + onLinkLongClick = onLinkLongClick, onContentClick = onEventClick, onLongClick = ::onMessageLongClick, inReplyToClick = {}, @@ -232,7 +236,10 @@ private fun PinnedMessagesListLoaded( timelineProtectionState = state.timelineProtectionState, onContentClick = { onEventClick(event) }, onLongClick = { onMessageLongClick(event) }, - onLinkClick = onLinkClick, + onLinkClick = { link -> + state.linkState.eventSink(LinkEvents.OnLinkClick(link)) + }, + onLinkLongClick = onLinkLongClick, modifier = contentModifier, onContentLayoutChange = onContentLayoutChange ) @@ -240,6 +247,10 @@ private fun PinnedMessagesListLoaded( ) } } + LinkView( + state.linkState, + onLinkValid = onLinkClick, + ) } @Composable @@ -247,7 +258,8 @@ private fun TimelineItemEventContentViewWrapper( event: TimelineItem.Event, timelineProtectionState: TimelineProtectionState, onContentClick: () -> Unit, - onLinkClick: (String) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, onLongClick: (() -> Unit)?, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, @@ -264,6 +276,7 @@ private fun TimelineItemEventContentViewWrapper( hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, eventSink = { }, modifier = modifier, onContentClick = onContentClick, @@ -283,5 +296,6 @@ internal fun PinnedMessagesListViewPreview(@PreviewParameter(PinnedMessagesListS onEventClick = { }, onUserDataClick = {}, onLinkClick = {}, + onLinkLongClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt index 471641e6a5e..538d9ce25a8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt @@ -25,13 +25,13 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage 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.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class ReportMessagePresenter @AssistedInject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, @Assisted private val inputs: Inputs, private val snackbarDispatcher: SnackbarDispatcher, ) : Presenter { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt index e1127d26441..cb3b5534973 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt @@ -17,7 +17,7 @@ open class ReportMessageStateProvider : PreviewParameterProvider = mutableStateOf(null) @Composable - override fun Update(currentUserId: UserId) { + override fun Update() { val isInEditMode = LocalInspectionMode.current val mentionDetector = remember(isInEditMode) { if (isInEditMode) null else newMentionDetector() } val editorStyle = ElementRichTextEditorStyle.textStyle() - val mentionSpanTheme = LocalMentionSpanTheme.current val context = LocalContext.current - htmlConverter.value = remember(editorStyle, mentionSpanTheme) { + htmlConverter.value = remember(editorStyle) { StyledHtmlConverter( context = context, mentionDisplayHandler = object : MentionDisplayHandler { override fun resolveAtRoomMentionDisplay(): TextDisplay { - val mentionSpan = mentionSpanProvider.getMentionSpanFor(text = "@room", url = "#") - mentionSpan.update(mentionSpanTheme) + val mentionSpan = mentionSpanProvider.createEveryoneMentionSpan() return TextDisplay.Custom(mentionSpan) } override fun resolveMentionDisplay(text: String, url: String): TextDisplay { val mentionSpan = mentionSpanProvider.getMentionSpanFor(text, url) - mentionSpan.update(mentionSpanTheme) - return TextDisplay.Custom(mentionSpan) + return if (mentionSpan != null) { + TextDisplay.Custom(mentionSpan) + } else { + TextDisplay.Plain + } } }, isEditor = false, - isMention = { _, url -> mentionDetector?.isMention(url).orFalse() } + isMention = { _, url -> + mentionDetector?.isMention(url).orFalse() + } ).apply { configureWith(editorStyle) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt new file mode 100644 index 00000000000..b2962e2f5ad --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt @@ -0,0 +1,37 @@ +/* + * 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.messages.impl.timeline + +import com.squareup.anvil.annotations.ContributesBinding +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.timeline.ReceiptType +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +interface MarkAsFullyRead { + operator fun invoke(roomId: RoomId) +} + +@ContributesBinding(SessionScope::class) +class DefaultMarkAsFullyRead @Inject constructor( + private val matrixClient: MatrixClient, +) : MarkAsFullyRead { + override fun invoke(roomId: RoomId) { + matrixClient.sessionCoroutineScope.launch { + matrixClient.getRoom(roomId)?.use { room -> + room.markAsRead(receiptType = ReceiptType.FULLY_READ) + .onFailure { + Timber.e("Failed to mark room $roomId as fully read", it) + } + } + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt index bfac2914328..4800d64c9bf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt @@ -11,7 +11,8 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineProvider @@ -41,7 +42,7 @@ import javax.inject.Inject @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class, boundType = TimelineProvider::class) class TimelineController @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, ) : Closeable, TimelineProvider { private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -64,7 +65,7 @@ class TimelineController @Inject constructor( } suspend fun focusOnEvent(eventId: EventId): Result { - return room.timelineFocusedOnEvent(eventId) + return room.createTimeline(CreateTimelineParams.Focused(eventId)) .onFailure { if (it is CancellationException) { throw it diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index 464a5caa226..da6837df21e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.timeline import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlin.time.Duration @@ -30,6 +31,7 @@ sealed interface TimelineEvents { data class ComputeVerifiedUserSendFailure(val event: TimelineItem.Event) : EventFromTimelineItem data class ShowShieldDialog(val messageShield: MessageShield) : EventFromTimelineItem data class LoadMore(val direction: Timeline.PaginationDirection) : EventFromTimelineItem + data class NavigateToRoom(val roomId: RoomId) : EventFromTimelineItem /** * Events coming from a poll item. 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 8a496859d0f..d8c43e7de75 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 @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState @@ -28,16 +29,19 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.roomMembers @@ -61,9 +65,10 @@ const val FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS = 200L class TimelinePresenter @AssistedInject constructor( timelineItemsFactoryCreator: TimelineItemsFactory.Creator, - private val room: MatrixRoom, + private val room: JoinedRoom, private val dispatchers: CoroutineDispatchers, - private val appScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, @Assisted private val navigator: MessagesNavigator, private val redactedVoiceMessageManager: RedactedVoiceMessageManager, private val sendPollResponseAction: SendPollResponseAction, @@ -74,6 +79,7 @@ class TimelinePresenter @AssistedInject constructor( private val resolveVerifiedUserSendFailurePresenter: Presenter, private val typingNotificationPresenter: Presenter, private val roomCallStatePresenter: Presenter, + private val markAsFullyRead: MarkAsFullyRead, ) : Presenter { @AssistedFactory interface Factory { @@ -95,7 +101,7 @@ class TimelinePresenter @AssistedInject constructor( val lastReadReceiptId = rememberSaveable { mutableStateOf(null) } - val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val roomInfo by room.roomInfoFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() @@ -108,9 +114,15 @@ class TimelinePresenter @AssistedInject constructor( val messageShield: MutableState = remember { mutableStateOf(null) } val resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailurePresenter.present() - val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true) - val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true) - val isLive by timelineController.isLive().collectAsState(initial = true) + val isSendPublicReadReceiptsEnabled by remember { + sessionPreferencesStore.isSendPublicReadReceiptsEnabled() + }.collectAsState(initial = true) + val renderReadReceipts by remember { + sessionPreferencesStore.isRenderReadReceiptsEnabled() + }.collectAsState(initial = true) + val isLive by remember { + timelineController.isLive() + }.collectAsState(initial = true) fun handleEvents(event: TimelineEvents) { when (event) { @@ -124,8 +136,8 @@ class TimelinePresenter @AssistedInject constructor( if (event.firstIndex == 0) { newEventState.value = NewEventState.None } - println("## sendReadReceiptIfNeeded firstVisibleIndex: ${event.firstIndex}") - appScope.sendReadReceiptIfNeeded( + Timber.d("## sendReadReceiptIfNeeded firstVisibleIndex: ${event.firstIndex}") + sessionCoroutineScope.sendReadReceiptIfNeeded( firstVisibleIndex = event.firstIndex, timelineItems = timelineItems, lastReadReceiptId = lastReadReceiptId, @@ -135,13 +147,13 @@ class TimelinePresenter @AssistedInject constructor( newEventState.value = NewEventState.None } } - is TimelineEvents.SelectPollAnswer -> appScope.launch { + is TimelineEvents.SelectPollAnswer -> sessionCoroutineScope.launch { sendPollResponseAction.execute( pollStartId = event.pollStartId, answerId = event.answerId ) } - is TimelineEvents.EndPoll -> appScope.launch { + is TimelineEvents.EndPoll -> sessionCoroutineScope.launch { endPollAction.execute( pollStartId = event.pollStartId, ) @@ -166,6 +178,15 @@ class TimelinePresenter @AssistedInject constructor( is TimelineEvents.ComputeVerifiedUserSendFailure -> { resolveVerifiedUserSendFailureState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(event.event)) } + is TimelineEvents.NavigateToRoom -> { + navigator.onNavigateToRoom(event.roomId) + } + } + } + + DisposableEffect(Unit) { + onDispose { + markAsFullyRead(room.roomId) } } @@ -231,16 +252,17 @@ class TimelinePresenter @AssistedInject constructor( val typingNotificationState = typingNotificationPresenter.present() val roomCallState = roomCallStatePresenter.present() - val timelineRoomInfo by remember(typingNotificationState, roomCallState) { + val timelineRoomInfo by remember(typingNotificationState, roomCallState, roomInfo) { derivedStateOf { TimelineRoomInfo( - name = room.displayName, - isDm = room.isDm, + name = roomInfo.name, + isDm = roomInfo.isDm.orFalse(), userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = userHasPermissionToSendReaction, roomCallState = roomCallState, - pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(), + pinnedEventIds = roomInfo.pinnedEventIds, typingNotificationState = typingNotificationState, + predecessorRoom = room.predecessorRoom(), ) } } @@ -271,7 +293,10 @@ class TimelinePresenter @AssistedInject constructor( if (newEventState.value == NewEventState.FromMe) { return@withContext } - val newMostRecentItem = timelineItems.firstOrNull() + val newMostRecentItem = timelineItems.firstOrNull { + // Ignore typing item + (it as? TimelineItem.Virtual)?.model !is TimelineItemTypingNotificationModel + } val prevMostRecentItemIdValue = prevMostRecentItemId.value val newMostRecentItemId = newMostRecentItem?.identifier() val hasNewEvent = prevMostRecentItemIdValue != null && diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index af4b7622fe0..a3cdb34c945 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlinx.collections.immutable.ImmutableList import kotlin.time.Duration @@ -77,4 +78,5 @@ data class TimelineRoomInfo( val roomCallState: RoomCallState, val pinnedEventIds: List, val typingNotificationState: TypingNotificationState, + val predecessorRoom: PredecessorRoom?, ) 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 d47379da3d8..8bb66b4f602 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 @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield @@ -246,6 +247,7 @@ internal fun aTimelineRoomInfo( userHasPermissionToSendMessage: Boolean = true, pinnedEventIds: List = emptyList(), typingNotificationState: TypingNotificationState = aTypingNotificationState(), + predecessorRoom: PredecessorRoom? = null, ) = TimelineRoomInfo( isDm = isDm, name = name, @@ -254,4 +256,5 @@ internal fun aTimelineRoomInfo( roomCallState = aStandByCallState(), pinnedEventIds = pinnedEventIds, typingNotificationState = typingNotificationState, + predecessorRoom = predecessorRoom, ) 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 75f59946c2a..cc9bf3cf84d 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 @@ -7,7 +7,7 @@ package io.element.android.features.messages.impl.timeline -import android.view.accessibility.AccessibilityManager +import android.view.HapticFeedbackConstants import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -35,12 +35,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.rotate import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter @@ -59,6 +61,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState +import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.designsystem.components.dialogs.AlertDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -66,16 +69,29 @@ import io.element.android.libraries.designsystem.theme.components.FloatingAction import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.utils.animateScrollToItemCenter 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.Timeline +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.wysiwyg.link.Link +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch +import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds @Composable fun TimelineView( state: TimelineState, timelineProtectionState: TimelineProtectionState, - onUserDataClick: (UserId) -> Unit, - onLinkClick: (String) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, + onLinkClick: (Link) -> Unit, onContentClick: (TimelineItem.Event) -> Unit, onMessageLongClick: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit, @@ -106,26 +122,39 @@ fun TimelineView( } val context = LocalContext.current + val view = LocalView.current // Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version - val useReverseLayout = remember { - val accessibilityManager = context.getSystemService(AccessibilityManager::class.java) - accessibilityManager.isTouchExplorationEnabled.not() - } + val useReverseLayout = !isTalkbackActive() fun inReplyToClick(eventId: EventId) { state.eventSink(TimelineEvents.FocusOnEvent(eventId)) } + fun onLinkLongClick(link: Link) { + view.performHapticFeedback( + HapticFeedbackConstants.LONG_PRESS + ) + context.copyToClipboard( + link.url, + context.getString(CommonStrings.common_copied_to_clipboard) + ) + } + + fun prefetchMoreItems() { + state.eventSink(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)) + } + // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms AnimatedVisibility(visible = true, enter = fadeIn()) { Box(modifier) { LazyColumn( modifier = Modifier .fillMaxSize() - .nestedScroll(nestedScrollConnection), + .nestedScroll(nestedScrollConnection) + .testTag(TestTags.timeline), state = lazyListState, reverseLayout = useReverseLayout, - contentPadding = PaddingValues(vertical = 8.dp), + contentPadding = PaddingValues(top = 64.dp, bottom = 8.dp), ) { items( items = state.timelineItems, @@ -141,6 +170,7 @@ fun TimelineView( focusedEventId = state.focusedEventId, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onLinkLongClick = ::onLinkLongClick, onContentClick = onContentClick, onLongClick = onMessageLongClick, inReplyToClick = ::inReplyToClick, @@ -160,6 +190,11 @@ fun TimelineView( onClearFocusRequestState = ::clearFocusRequestState ) + TimelinePrefetchingHelper( + lazyListState = lazyListState, + prefetch = ::prefetchMoreItems + ) + TimelineScrollHelper( hasAnyEvent = state.hasAnyEvent, lazyListState = lazyListState, @@ -188,6 +223,45 @@ private fun MessageShieldDialog(state: TimelineState) { ) } +@Composable +private fun TimelinePrefetchingHelper( + lazyListState: LazyListState, + prefetch: () -> Unit, +) { + val latestPrefetch by rememberUpdatedState(prefetch) + + LaunchedEffect(Unit) { + // We're using snapshot flows for these because using `LaunchedEffect` with `derivedState` doesn't seem to be responsive enough + val firstVisibleItemIndexFlow = snapshotFlow { lazyListState.firstVisibleItemIndex } + val layoutInfoFlow = snapshotFlow { lazyListState.layoutInfo } + val isScrollingFlow = snapshotFlow { lazyListState.isScrollInProgress } + // This value changes too frequently, so we debounce it to avoid unnecessary prefetching. It's the equivalent of a conditional 'throttleLatest' + .conflate() + .transform { isScrolling -> + emit(isScrolling) + if (isScrolling) delay(100.milliseconds) + } + + val isCloseToStartOfLoadedTimelineFlow = combine(layoutInfoFlow, firstVisibleItemIndexFlow) { layoutInfo, firstVisibleItemIndex -> + firstVisibleItemIndex + layoutInfo.visibleItemsInfo.size >= layoutInfo.totalItemsCount - 40 + } + + combine( + isCloseToStartOfLoadedTimelineFlow.distinctUntilChanged(), + isScrollingFlow.distinctUntilChanged(), + ) { needsPrefetch, isScrolling -> + needsPrefetch && isScrolling + } + .distinctUntilChanged() + .collectLatest { needsPrefetch -> + if (needsPrefetch) { + Timber.d("Prefetching pagination with ${lazyListState.layoutInfo.totalItemsCount} items") + latestPrefetch() + } + } + } +} + @Composable private fun BoxScope.TimelineScrollHelper( hasAnyEvent: Boolean, @@ -209,11 +283,16 @@ private fun BoxScope.TimelineScrollHelper( } var jumpToLiveHandled by remember { mutableStateOf(true) } - fun scrollToBottom() { + /** + * @param force If true, scroll to the bottom even if the user is already seeing the most recent item. + * This fixes the issue where the user is seeing typing notification and so the read receipt is not sent + * when a new message comes in. + */ + fun scrollToBottom(force: Boolean) { coroutineScope.launch { if (lazyListState.firstVisibleItemIndex > 10) { lazyListState.scrollToItem(0) - } else { + } else if (force || lazyListState.firstVisibleItemIndex != 0) { lazyListState.animateScrollToItem(0) } } @@ -221,7 +300,7 @@ private fun BoxScope.TimelineScrollHelper( fun jumpToBottom() { if (isLive) { - scrollToBottom() + scrollToBottom(force = false) } else { jumpToLiveHandled = false onJumpToLive() @@ -244,9 +323,10 @@ private fun BoxScope.TimelineScrollHelper( } LaunchedEffect(canAutoScroll, newEventState) { - val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe) + val shouldScrollToBottom = isScrollFinished && + (canAutoScroll && newEventState == NewEventState.FromOther || newEventState == NewEventState.FromMe) if (shouldScrollToBottom) { - scrollToBottom() + scrollToBottom(force = true) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt new file mode 100644 index 00000000000..9da0168b388 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt @@ -0,0 +1,61 @@ +/* + * 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.messages.impl.timeline.a11y + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import io.element.android.features.messages.impl.R +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +@ReadOnlyComposable +fun a11yReactionAction( + emoji: String, + userAlreadyReacted: Boolean, +): String { + return if (userAlreadyReacted) { + stringResource(id = CommonStrings.a11y_remove_reaction_with, emoji) + } else { + stringResource(id = CommonStrings.a11y_react_with, emoji) + } +} + +@Composable +@ReadOnlyComposable +fun a11yReactionDetails( + emoji: String, + userAlreadyReacted: Boolean, + reactionCount: Int, +): String { + val reaction = if (emoji.startsWith("mxc://")) { + stringResource(CommonStrings.common_an_image) + } else { + emoji + } + return if (userAlreadyReacted) { + if (reactionCount == 1) { + stringResource(R.string.screen_room_timeline_reaction_you_a11y, reaction) + } else { + pluralStringResource( + R.plurals.screen_room_timeline_reaction_including_you_a11y, + reactionCount - 1, + reactionCount - 1, + reaction, + ) + } + } else { + pluralStringResource( + R.plurals.screen_room_timeline_reaction_a11y, + reactionCount, + reactionCount, + reaction, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index 685914e4e84..8434242abd7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -21,7 +21,6 @@ internal fun ATimelineItemEventRow( timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), renderReadReceipts: Boolean = false, isLastOutgoingMessage: Boolean = false, - isHighlighted: Boolean = false, timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), ) = TimelineItemEventRow( event = event, @@ -29,10 +28,10 @@ internal fun ATimelineItemEventRow( renderReadReceipts = renderReadReceipts, timelineProtectionState = timelineProtectionState, isLastOutgoingMessage = isLastOutgoingMessage, - isHighlighted = isHighlighted, onEventClick = {}, onLongClick = {}, onLinkClick = {}, + onLinkLongClick = {}, onUserDataClick = {}, inReplyToClick = {}, onReactionClick = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt index 098421f177c..4bbccabc535 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt @@ -38,6 +38,9 @@ internal fun CallMenuItem( modifier: Modifier = Modifier, ) { when (roomCallState) { + RoomCallState.Unavailable -> { + Box(modifier) + } is RoomCallState.StandBy -> { StandByCallMenuItem( roomCallState = roomCallState, 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 c5f243a18a6..22853d41511 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 @@ -7,7 +7,6 @@ package io.element.android.features.messages.impl.timeline.components -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -49,6 +48,7 @@ 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.time.isTalkbackActive private val BUBBLE_RADIUS = 12.dp private val avatarRadius = AvatarSize.TimelineSender.dp / 2 @@ -57,7 +57,6 @@ private val avatarRadius = AvatarSize.TimelineSender.dp / 2 private const val BUBBLE_WIDTH_RATIO = 0.78f private val MIN_BUBBLE_WIDTH = 80.dp -@OptIn(ExperimentalFoundationApi::class) @Composable fun MessageEventBubble( state: BubbleState, @@ -95,6 +94,17 @@ fun MessageEventBubble( } } + val clickableModifier = if (isTalkbackActive()) { + Modifier + } else { + Modifier.combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + indication = ripple(), + interactionSource = interactionSource + ) + } + // Ignore state.isHighlighted for now, we need a design decision on it. val backgroundBubbleColor = when { state.isMine -> ElementTheme.colors.messageFromMeBackground @@ -137,12 +147,7 @@ fun MessageEventBubble( .toDp() ) .clip(bubbleShape) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - indication = ripple(), - interactionSource = interactionSource - ), + .then(clickableModifier), color = backgroundBubbleColor, shape = bubbleShape, content = content @@ -174,7 +179,7 @@ internal fun MessageEventBubblePreview(@PreviewParameter(BubbleStateProvider::cl contentAlignment = Alignment.Center, ) { Text( - text = "${state.groupPosition.javaClass.simpleName} m:${state.isMine.to01()} h:${state.isHighlighted.to01()}", + text = "${state.groupPosition.javaClass.simpleName} isMine:${state.isMine.to01()}", style = ElementTheme.typography.fontBodyXsRegular, ) } 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 c2d2c3a3de3..9d7d4caac52 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 @@ -80,6 +80,7 @@ internal fun MessageShield.toText(): String { is MessageShield.UnverifiedIdentity -> CommonStrings.event_shield_reason_unverified_identity is MessageShield.SentInClear -> CommonStrings.event_shield_reason_sent_in_clear is MessageShield.VerificationViolation -> CommonStrings.event_shield_reason_previously_verified + is MessageShield.MismatchedSender -> CommonStrings.event_shield_mismatched_sender } ) } @@ -91,7 +92,8 @@ internal fun MessageShield.toIcon(): ImageVector { is MessageShield.UnknownDevice, is MessageShield.UnsignedDevice, is MessageShield.UnverifiedIdentity, - is MessageShield.VerificationViolation -> CompoundIcons.HelpSolid() + is MessageShield.VerificationViolation, + is MessageShield.MismatchedSender -> CompoundIcons.HelpSolid() is MessageShield.SentInClear -> CompoundIcons.LockOff() } } @@ -122,6 +124,9 @@ internal fun MessageShieldViewPreview() { MessageShieldView( shield = MessageShield.VerificationViolation(false) ) + MessageShieldView( + shield = MessageShield.MismatchedSender(false) + ) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt index 010132b47ab..c9886796263 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt @@ -7,12 +7,8 @@ package io.element.android.features.messages.impl.timeline.components -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ripple @@ -21,24 +17,23 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource 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.Surface +import io.element.android.libraries.ui.strings.CommonStrings private val CORNER_RADIUS = 8.dp -@OptIn(ExperimentalFoundationApi::class) @Composable fun MessageStateEventContainer( - @Suppress("UNUSED_PARAMETER") isHighlighted: Boolean, interactionSource: MutableInteractionSource, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit = {}, ) { - // Ignore isHighlighted for now, we need a design decision on it. val backgroundColor = Color.Transparent val shape = RoundedCornerShape(CORNER_RADIUS) Surface( @@ -48,6 +43,7 @@ fun MessageStateEventContainer( .combinedClickable( onClick = onClick, onLongClick = onLongClick, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), indication = ripple(), interactionSource = interactionSource ), @@ -60,22 +56,9 @@ fun MessageStateEventContainer( @PreviewsDayNight @Composable internal fun MessageStateEventContainerPreview() = ElementPreview { - Column { - MessageStateEventContainer( - isHighlighted = false, - interactionSource = remember { MutableInteractionSource() }, - onClick = {}, - onLongClick = {}, - ) { - Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp)) - } - MessageStateEventContainer( - isHighlighted = true, - interactionSource = remember { MutableInteractionSource() }, - onClick = {}, - onLongClick = {}, - ) { - Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp)) - } - } + MessageStateEventContainer( + interactionSource = remember { MutableInteractionSource() }, + onClick = {}, + onLongClick = {}, + ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt index f2db4e6efbf..8c2a1d9ff62 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt @@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.components import androidx.annotation.DrawableRes import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable @@ -23,7 +22,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment @@ -31,12 +29,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.R +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionDetails import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.features.messages.impl.timeline.model.AggregatedReactionProvider import io.element.android.features.messages.impl.timeline.model.aTimelineItemReactions @@ -49,9 +51,9 @@ import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.ui.strings.CommonStrings @Composable -@OptIn(ExperimentalFoundationApi::class) @Suppress("ModifierClickableOrder") // This is needed to display the right ripple shape fun MessagesReactionButton( onClick: () -> Unit, @@ -71,12 +73,24 @@ fun MessagesReactionButton( buttonColor } + val a11yText = when (content) { + is MessagesReactionsButtonContent.Icon -> stringResource(id = R.string.screen_room_timeline_add_reaction) + is MessagesReactionsButtonContent.Text -> content.text + is MessagesReactionsButtonContent.Reaction -> { + a11yReactionDetails( + emoji = content.reaction.key, + userAlreadyReacted = content.isHighlighted, + reactionCount = content.reaction.count, + ) + } + } + Surface( modifier = modifier .background(Color.Transparent) // Outer border, same colour as background .border( - BorderStroke(2.dp, MaterialTheme.colorScheme.background), + BorderStroke(2.dp, ElementTheme.colors.bgCanvasDefault), shape = RoundedCornerShape(corner = CornerSize(14.dp)) ) .padding(vertical = 2.dp, horizontal = 2.dp) @@ -84,12 +98,22 @@ fun MessagesReactionButton( .clip(RoundedCornerShape(corner = CornerSize(12.dp))) .combinedClickable( onClick = onClick, + onClickLabel = (content as? MessagesReactionsButtonContent.Reaction)?.let { + a11yReactionAction( + emoji = content.reaction.key, + userAlreadyReacted = content.isHighlighted + ) + }, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), onLongClick = onLongClick ) // Inner border, to highlight when selected .border(BorderStroke(1.dp, borderColor), RoundedCornerShape(corner = CornerSize(12.dp))) .background(buttonColor, RoundedCornerShape(corner = CornerSize(12.dp))) - .padding(vertical = 4.dp, horizontal = 10.dp), + .padding(vertical = 4.dp, horizontal = 10.dp) + .clearAndSetSemantics { + contentDescription = a11yText + }, color = buttonColor ) { when (content) { @@ -122,7 +146,7 @@ private fun TextContent( .height(REACTION_EMOJI_LINE_HEIGHT.toDp()), text = text, style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.materialColors.primary + color = ElementTheme.colors.textPrimary ) @Composable @@ -131,7 +155,7 @@ private fun IconContent( ) = Icon( resourceId = resourceId, contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction), - tint = ElementTheme.materialColors.secondary, + tint = ElementTheme.colors.iconSecondary, modifier = Modifier .size(ADD_EMOJI_SIZE) ) @@ -164,7 +188,7 @@ private fun ReactionContent( Spacer(modifier = Modifier.width(4.dp)) Text( text = reaction.count.toString(), - color = if (reaction.isHighlighted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary, + color = if (reaction.isHighlighted) ElementTheme.colors.textPrimary else ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodyMdRegular, ) } 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 85b61810502..644cbf23108 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 @@ -14,11 +14,12 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme 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.hideFromAccessibility +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 @@ -26,6 +27,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.isEdited +import io.element.android.features.messages.impl.timeline.model.event.isRedacted import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -45,7 +47,8 @@ fun TimelineEventTimestampView( val hasError = event.localSendState is LocalEventSendState.Failed val hasEncryptionCritical = event.messageShield?.isCritical.orFalse() val isMessageEdited = event.content.isEdited() - val tint = if (hasError || hasEncryptionCritical) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary + val isMessageRedacted = event.content.isRedacted() + val tint = if (hasError || hasEncryptionCritical && !isMessageRedacted) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary Row( modifier = Modifier .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)) @@ -69,29 +72,34 @@ fun TimelineEventTimestampView( val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser Spacer(modifier = Modifier.width(2.dp)) Icon( - imageVector = CompoundIcons.Error(), + imageVector = CompoundIcons.ErrorSolid(), contentDescription = stringResource(id = CommonStrings.common_sending_failed), tint = tint, modifier = Modifier .size(15.dp, 18.dp) .clickable(isVerifiedUserSendFailure) { eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event)) - }, + } + .semantics { hideFromAccessibility() } ) } - event.messageShield?.let { shield -> - Spacer(modifier = Modifier.width(2.dp)) - Icon( - imageVector = shield.toIcon(), - contentDescription = shield.toText(), - modifier = Modifier + + if (!isMessageRedacted) { + event.messageShield?.let { shield -> + Spacer(modifier = Modifier.width(2.dp)) + Icon( + imageVector = shield.toIcon(), + contentDescription = shield.toText(), + modifier = Modifier .size(15.dp) .clickable { eventSink(TimelineEvents.ShowShieldDialog(shield)) - }, - tint = shield.toIconColor(), - ) - Spacer(modifier = Modifier.width(4.dp)) + } + .semantics { hideFromAccessibility() }, + 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 41d9fa111b2..ca34184491a 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 @@ -7,7 +7,6 @@ package io.element.android.features.messages.impl.timeline.components -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -34,12 +33,12 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt 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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.ui.strings.CommonStrings -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun TimelineItemCallNotifyView( event: TimelineItem.Event, @@ -52,12 +51,20 @@ internal fun TimelineItemCallNotifyView( modifier = modifier .fillMaxWidth() .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(8.dp)) - .combinedClickable(enabled = true, onClick = {}, onLongClick = { onLongClick(event) }) + .combinedClickable( + enabled = true, + onClick = {}, + onLongClick = { onLongClick(event) }, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) .padding(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, ) { - Avatar(avatarData = event.senderAvatar) + Avatar( + avatarData = event.senderAvatar, + avatarType = AvatarType.User, + ) Column(modifier = Modifier.weight(1f)) { Text( text = event.safeSenderName, @@ -103,10 +110,12 @@ internal fun TimelineItemCallNotifyView( @PreviewsDayNight @Composable -internal fun TimelineItemCallNotifyViewPreview() { - ElementPreview { - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - RoomCallStateProvider().values.forEach { roomCallState -> +internal fun TimelineItemCallNotifyViewPreview() = ElementPreview { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + RoomCallStateProvider() + .values + .filter { it !is RoomCallState.Unavailable } + .forEach { roomCallState -> TimelineItemCallNotifyView( event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()), roomCallState = roomCallState, @@ -114,6 +123,5 @@ internal fun TimelineItemCallNotifyViewPreview() { onJoinCallClick = {}, ) } - } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt index b3cb657d668..568df862957 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield @@ -33,5 +34,9 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider Unit, onLongClick: () -> Unit, - onLinkClick: (String) -> Unit, - onUserDataClick: (UserId) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, inReplyToClick: (EventId) -> Unit, onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, @@ -138,6 +144,7 @@ fun TimelineItemEventRow( onLongClick = onLongClick, onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, eventSink = eventSink, modifier = contentModifier, onContentLayoutChange = onContentLayoutChange @@ -155,7 +162,12 @@ fun TimelineItemEventRow( } fun onUserDataClick() { - onUserDataClick(event.senderId) + val sender = MatrixUser( + userId = event.senderId, + displayName = event.senderProfile.getDisplayName(), + avatarUrl = event.senderProfile.getAvatarUrl(), + ) + onUserDataClick(sender) } fun inReplyToClick() { @@ -183,7 +195,6 @@ fun TimelineItemEventRow( TimelineItemEventRowContent( event = event, timelineProtectionState = timelineProtectionState, - isHighlighted = isHighlighted, timelineRoomInfo = timelineRoomInfo, interactionSource = interactionSource, onContentClick = onContentClick, @@ -217,7 +228,6 @@ fun TimelineItemEventRow( TimelineItemEventRowContent( event = event, timelineProtectionState = timelineProtectionState, - isHighlighted = isHighlighted, timelineRoomInfo = timelineRoomInfo, interactionSource = interactionSource, onContentClick = onContentClick, @@ -240,7 +250,7 @@ fun TimelineItemEventRow( ), renderReadReceipts = renderReadReceipts, onReadReceiptsClick = { onReadReceiptClick(event) }, - modifier = Modifier.padding(top = 4.dp), + modifier = Modifier.padding(top = 4.dp) ) } } @@ -268,12 +278,10 @@ private fun SwipeSensitivity( } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun TimelineItemEventRowContent( event: TimelineItem.Event, timelineProtectionState: TimelineProtectionState, - isHighlighted: Boolean, timelineRoomInfo: TimelineRoomInfo, interactionSource: MutableInteractionSource, onContentClick: () -> Unit, @@ -311,6 +319,7 @@ private fun TimelineItemEventRowContent( event.senderId, event.senderProfile, event.senderAvatar, + onUserDataClick, Modifier .constrainAs(sender) { top.linkTo(parent.top) @@ -318,13 +327,7 @@ private fun TimelineItemEventRowContent( start.linkTo(parent.start) } .padding(horizontal = 16.dp) - .zIndex(1f) - .clickable(onClick = onUserDataClick) - // This is redundant when using talkback - .clearAndSetSemantics { - invisibleToUser() - testTag = TestTags.timelineItemSenderInfo.value - } + .zIndex(1f), ) } @@ -332,7 +335,6 @@ private fun TimelineItemEventRowContent( val bubbleState = BubbleState( groupPosition = event.groupPosition, isMine = event.isMine, - isHighlighted = isHighlighted, timelineRoomInfo = timelineRoomInfo, ) MessageEventBubble( @@ -422,13 +424,32 @@ private fun MessageSenderInformation( senderId: UserId, senderProfile: ProfileTimelineDetails, senderAvatar: AvatarData, + onClick: () -> Unit, modifier: Modifier = Modifier ) { val avatarColors = AvatarColorsProvider.provide(senderAvatar.id) - Row(modifier = modifier) { - Avatar(senderAvatar) - Spacer(modifier = Modifier.width(4.dp)) + Row( + modifier = modifier + // Add external clickable modifier with no indicator so the touch target is larger than just the display name + .clickable(onClick = onClick, enabled = true, interactionSource = remember { MutableInteractionSource() }, indication = null) + .clearAndSetSemantics { + hideFromAccessibility() + } + ) { + Avatar( + modifier = Modifier + .testTag(TestTags.timelineItemSenderAvatar) + .clip(CircleShape) + .clickable(onClick = onClick), + avatarData = senderAvatar, + avatarType = AvatarType.User, + ) SenderName( + modifier = Modifier + .testTag(TestTags.timelineItemSenderName) + .clip(RoundedCornerShape(6.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 4.dp), senderId = senderId, senderProfile = senderProfile, senderNameMode = SenderNameMode.Timeline(avatarColors.foreground), @@ -577,7 +598,10 @@ private fun MessageEventBubbleContent( timestampPosition = timestampPosition, eventSink = eventSink, canShrinkContent = canShrinkContent, - modifier = timestampLayoutModifier, + modifier = timestampLayoutModifier.semantics(mergeDescendants = false) { + isTraversalGroup = true + traversalIndex = -1f + }, content = { onContentLayoutChange -> eventContentView(contentModifier, onContentLayoutChange) } @@ -589,17 +613,23 @@ private fun MessageEventBubbleContent( val inReplyToModifier = Modifier .padding(top = topPadding, start = 8.dp, end = 8.dp) .clip(RoundedCornerShape(6.dp)) - // FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent - .clickable(onClick = inReplyToClick) + + val talkbackCompatModifier = if (isTalkbackActive()) { + // Use z-index to make the replied to text being read after the message + // Usually, you'd use traversalIndex for that, but it's not working for some reason + inReplyToModifier.zIndex(1f) + } else { + inReplyToModifier.clickable(onClick = inReplyToClick) + } InReplyToView( inReplyTo = inReplyTo, hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()), - modifier = inReplyToModifier, + modifier = talkbackCompatModifier, ) } if (inReplyToDetails != null) { // Use SubComposeLayout only if necessary as it can have consequences on the performance. - EqualWidthColumn(modifier = modifier, spacing = 8.dp) { + EqualWidthColumn(spacing = 8.dp) { threadDecoration() inReplyTo(inReplyToDetails) contentWithTimestamp() @@ -634,9 +664,7 @@ private fun MessageEventBubbleContent( paddingBehaviour = paddingBehaviour, inReplyToDetails = event.inReplyTo, canShrinkContent = event.content is TimelineItemVoiceContent, - modifier = bubbleModifier.semantics(mergeDescendants = true) { - contentDescription = event.safeSenderName - } + modifier = bubbleModifier, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowTimestampPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowTimestampPreview.kt index 12a61254049..0555670e46e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowTimestampPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowTimestampPreview.kt @@ -22,18 +22,25 @@ internal fun TimelineItemEventRowTimestampPreview( @PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event ) = ElementPreview { Column { - val oldContent = event.content as TimelineItemTextContent - listOf( - "Text", - "Text longer, displayed on 1 line", - "Text which should be rendered on several lines", - ).forEach { str -> - ATimelineItemEventRow( - event = event.copy( - content = oldContent.copy( - body = str, - pillifiedBody = str, + when (event.content) { + is TimelineItemTextContent -> listOf( + "Text", + "Text longer, displayed on 1 line", + "Text which should be rendered on several lines", + ).forEach { str -> + ATimelineItemEventRow( + event = event.copy( + content = event.content.copy( + body = str, + formattedBody = str, + ), + reactionsState = aTimelineItemReactions(count = 0), ), + ) + } + else -> ATimelineItemEventRow( + event = event.copy( + content = event.content, reactionsState = aTimelineItemReactions(count = 0), ), ) 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 5e538fb483f..a32f36ced6b 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 @@ -31,7 +31,9 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.user.MatrixUser +import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.wysiwyg.link.Link @Composable fun TimelineItemGroupedEventsRow( @@ -44,8 +46,9 @@ fun TimelineItemGroupedEventsRow( onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, - onUserDataClick: (UserId) -> Unit, - onLinkClick: (String) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit, @@ -59,6 +62,7 @@ fun TimelineItemGroupedEventsRow( hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, eventSink = eventSink, modifier = contentModifier, onContentClick = null, @@ -87,6 +91,7 @@ fun TimelineItemGroupedEventsRow( inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, @@ -110,8 +115,9 @@ private fun TimelineItemGroupedEventsRowContent( onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, - onUserDataClick: (UserId) -> Unit, - onLinkClick: (String) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit, @@ -125,6 +131,7 @@ private fun TimelineItemGroupedEventsRowContent( hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, eventSink = eventSink, modifier = contentModifier, onContentClick = null, @@ -146,7 +153,13 @@ private fun TimelineItemGroupedEventsRowContent( ) if (isExpanded) { Column { - timelineItem.events.forEach { subGroupEvent -> + timelineItem.events.let { + if (isTalkbackActive()) { + it.reversed() + } else { + it + } + }.forEach { subGroupEvent -> TimelineItemRow( timelineItem = subGroupEvent, timelineRoomInfo = timelineRoomInfo, @@ -156,6 +169,7 @@ private fun TimelineItemGroupedEventsRowContent( focusedEventId = focusedEventId, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, onContentClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, @@ -199,6 +213,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi isLastOutgoingMessage = false, onClick = {}, onLongClick = {}, + onLinkLongClick = {}, inReplyToClick = {}, onUserDataClick = {}, onLinkClick = {}, @@ -224,6 +239,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi isLastOutgoingMessage = false, onClick = {}, onLongClick = {}, + onLinkLongClick = {}, inReplyToClick = {}, onUserDataClick = {}, onLinkClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt index 8c72ae7710c..07811b532d3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt @@ -16,6 +16,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.R @@ -40,7 +42,9 @@ fun TimelineItemReactionsView( ) { var expanded: Boolean by rememberSaveable { mutableStateOf(false) } TimelineItemReactionsView( - modifier = modifier, + modifier = modifier.semantics { + hideFromAccessibility() + }, reactions = reactionsState.reactions, userCanSendReaction = userCanSendReaction, expanded = expanded, 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 106d4eb532d..ffbcfee305a 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 @@ -7,7 +7,10 @@ package io.element.android.features.messages.impl.timeline.components +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -15,6 +18,10 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -25,13 +32,22 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toPx +import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor 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.user.MatrixUser +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.wysiwyg.link.Link +import kotlin.time.DurationUnit @Composable internal fun TimelineItemRow( @@ -41,8 +57,9 @@ internal fun TimelineItemRow( isLastOutgoingMessage: Boolean, timelineProtectionState: TimelineProtectionState, focusedEventId: EventId?, - onUserDataClick: (UserId) -> Unit, - onLinkClick: (String) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, onContentClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, @@ -63,6 +80,7 @@ internal fun TimelineItemRow( onContentClick = { onContentClick(event) }, onLongClick = { onLongClick(event) }, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, eventSink = eventSink, modifier = contentModifier, onContentLayoutChange = onContentLayoutChange @@ -95,7 +113,6 @@ internal fun TimelineItemRow( event = timelineItem, renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, - isHighlighted = timelineItem.isEvent(focusedEventId), onClick = { onContentClick(timelineItem) }, onReadReceiptsClick = onReadReceiptClick, onLongClick = { onLongClick(timelineItem) }, @@ -112,16 +129,40 @@ internal fun TimelineItemRow( ) } else -> { + val a11yVoiceMessage = stringResource(CommonStrings.a11y_voice_message) TimelineItemEventRow( + modifier = Modifier + .semantics(mergeDescendants = true) { + contentDescription = if (timelineItem.content is TimelineItemVoiceContent) { + val voiceMessageText = String.format(a11yVoiceMessage, timelineItem.content.duration.toString(DurationUnit.MINUTES)) + "${timelineItem.safeSenderName}, $voiceMessageText" + } else { + timelineItem.safeSenderName + } + // For Polls, allow the answers to be traversed by Talkback + isTraversalGroup = timelineItem.content is TimelineItemPollContent + } + // Custom clickable that applies over the whole item for accessibility + .then( + if (isTalkbackActive()) { + Modifier.combinedClickable( + onClick = { onContentClick(timelineItem) }, + onLongClick = { onLongClick(timelineItem) }, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) + } else { + Modifier + } + ), event = timelineItem, timelineRoomInfo = timelineRoomInfo, renderReadReceipts = renderReadReceipts, timelineProtectionState = timelineProtectionState, isLastOutgoingMessage = isLastOutgoingMessage, - isHighlighted = timelineItem.isEvent(focusedEventId), onEventClick = { onContentClick(timelineItem) }, onLongClick = { onLongClick(timelineItem) }, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, onUserDataClick = onUserDataClick, inReplyToClick = inReplyToClick, onReactionClick = onReactionClick, @@ -150,6 +191,7 @@ internal fun TimelineItemRow( inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, @@ -167,9 +209,14 @@ private fun Modifier.focusedEvent( focusedEventOffset: Dp ): Modifier { val highlightedLineColor = ElementTheme.colors.textActionAccent + val gradientFirstColor = if (LocalBuildMeta.current.isEnterpriseBuild) { + ElementTheme.colors.textActionAccent.copy(alpha = 0.125f) + } else { + ElementTheme.colors.highlightedMessageBackgroundColor + } val gradientColors = listOf( - ElementTheme.colors.highlightedMessageBackgroundColor, - ElementTheme.materialColors.background + gradientFirstColor, + ElementTheme.colors.bgCanvasDefault, ) val verticalOffset = focusedEventOffset.toPx() val verticalRatio = 0.7f @@ -192,3 +239,15 @@ private fun Modifier.focusedEvent( } }.padding(top = 4.dp) } + +@PreviewsDayNight +@Composable +internal fun FocusedEventPreview() = ElementPreview { + Box( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .height(160.dp) + .focusedEvent(0.dp), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index f6b13803ae3..2f66d586a9b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -40,7 +40,6 @@ fun TimelineItemStateEventRow( event: TimelineItem.Event, renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, - isHighlighted: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, onReadReceiptsClick: (event: TimelineItem.Event) -> Unit, @@ -60,7 +59,6 @@ fun TimelineItemStateEventRow( contentAlignment = Alignment.Center ) { MessageStateEventContainer( - isHighlighted = isHighlighted, interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, @@ -71,6 +69,7 @@ fun TimelineItemStateEventRow( TimelineItemEventContentView( content = event.content, onLinkClick = {}, + onLinkLongClick = {}, hideMediaContent = false, onShowContentClick = {}, eventSink = eventSink, @@ -106,7 +105,6 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview { ), renderReadReceipts = true, isLastOutgoingMessage = false, - isHighlighted = false, onClick = {}, onLongClick = {}, onReadReceiptsClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt index aa4a0062494..45b167b68f0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt @@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel import io.element.android.features.messages.impl.typing.TypingNotificationView +import timber.log.Timber @Composable fun TimelineItemVirtualRow( @@ -40,11 +41,21 @@ fun TimelineItemVirtualRow( when (virtual.model) { is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model) TimelineItemReadMarkerModel -> TimelineItemReadMarkerView() - TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name) + TimelineItemRoomBeginningModel -> { + TimelineItemRoomBeginningView( + predecessorRoom = timelineRoomInfo.predecessorRoom, + roomName = timelineRoomInfo.name, + isDm = timelineRoomInfo.isDm, + onPredecessorRoomClick = { roomId -> + eventSink(TimelineEvents.NavigateToRoom(roomId)) + }, + ) + } is TimelineItemLoadingIndicatorModel -> { TimelineLoadingMoreIndicator(virtual.model.direction) val latestEventSink by rememberUpdatedState(eventSink) LaunchedEffect(virtual.model.timestamp) { + Timber.d("Pagination triggered by load more indicator") latestEventSink(TimelineEvents.LoadMore(virtual.model.direction)) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt index a27de9c3240..b6ad695aa80 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.remember 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.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.unit.TextUnit @@ -30,11 +29,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.compound.theme.ElementTheme import io.element.android.emojibasebindings.Emoji +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction 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.designsystem.theme.components.Text -import io.element.android.libraries.ui.strings.CommonStrings @Composable fun EmojiItem( @@ -49,11 +48,10 @@ fun EmojiItem( } else { Color.Transparent } - val description = if (isSelected) { - stringResource(id = CommonStrings.a11y_remove_reaction_with, item.unicode) - } else { - stringResource(id = CommonStrings.a11y_react_with, item.unicode) - } + val description = a11yReactionAction( + emoji = item.unicode, + userAlreadyReacted = isSelected, + ) Box( modifier = modifier .sizeIn(minWidth = 40.dp, minHeight = 40.dp) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt index b8256ba4021..fcaac65e820 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt @@ -7,7 +7,6 @@ package io.element.android.features.messages.impl.timeline.components.customreaction -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -41,7 +40,7 @@ import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.launch -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun EmojiPicker( onSelectEmoji: (Emoji) -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt index a2b55f00e5f..dc40fd77fac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt @@ -77,7 +77,7 @@ private fun TimelineItemAttachmentHeaderView( modifier = Modifier .size(iconSize) .clip(CircleShape) - .background(ElementTheme.materialColors.background), + .background(ElementTheme.colors.bgCanvasDefault), contentAlignment = Alignment.Center, ) { icon() @@ -86,14 +86,14 @@ private fun TimelineItemAttachmentHeaderView( Column { Text( text = filename, - color = ElementTheme.materialColors.primary, + color = ElementTheme.colors.textPrimary, maxLines = 2, style = ElementTheme.typography.fontBodyLgRegular, overflow = TextOverflow.Ellipsis ) Text( text = fileExtensionAndSize, - color = ElementTheme.materialColors.secondary, + color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodySmRegular, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -119,7 +119,7 @@ private fun TimelineItemAttachmentCaptionView( Text( modifier = modifier, text = caption, - color = ElementTheme.materialColors.primary, + color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontBodyLgRegular, onTextLayout = ContentAvoidingLayout.measureLastTextLine( onContentLayoutChange = onContentLayoutChange, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt index 6b28d5922b5..d25d6aa4570 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt @@ -8,13 +8,12 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.GraphicEq import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentProvider @@ -36,9 +35,9 @@ fun TimelineItemAudioView( modifier = modifier, icon = { Icon( - imageVector = Icons.Outlined.GraphicEq, + imageVector = CompoundIcons.Audio(), contentDescription = null, - tint = ElementTheme.materialColors.primary, + tint = ElementTheme.colors.iconPrimary, modifier = Modifier .size(16.dp), ) 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 31f3b8fd542..332f58777cf 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 @@ -31,6 +31,7 @@ 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.libraries.architecture.Presenter import io.element.android.libraries.voiceplayer.api.VoiceMessageState +import io.element.android.wysiwyg.link.Link @Composable fun TimelineItemEventContentView( @@ -39,7 +40,8 @@ fun TimelineItemEventContentView( onContentClick: (() -> Unit)?, onLongClick: (() -> Unit)?, onShowContentClick: () -> Unit, - onLinkClick: (url: String) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit = {}, @@ -60,6 +62,7 @@ fun TimelineItemEventContentView( content = content, modifier = modifier, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, onContentLayoutChange = onContentLayoutChange ) is TimelineItemUnknownContent -> TimelineItemUnknownView( @@ -78,6 +81,7 @@ fun TimelineItemEventContentView( onLongClick = onLongClick, onShowContentClick = onShowContentClick, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, onContentLayoutChange = onContentLayoutChange, modifier = modifier, ) @@ -96,6 +100,7 @@ fun TimelineItemEventContentView( onLongClick = onLongClick, onShowContentClick = onShowContentClick, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, onContentLayoutChange = onContentLayoutChange, modifier = modifier ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt index 91260ec8aaa..86218b8e255 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +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 @@ -21,6 +22,7 @@ import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun TimelineItemFileView( @@ -37,8 +39,8 @@ fun TimelineItemFileView( icon = { Icon( resourceId = CompoundDrawables.ic_compound_attachment, - contentDescription = null, - tint = ElementTheme.materialColors.primary, + contentDescription = stringResource(CommonStrings.common_file), + tint = ElementTheme.colors.iconPrimary, modifier = Modifier .size(16.dp) .rotate(-45f), 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 17d7cc58a9a..41d3873a65c 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 @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl.timeline.components.event import android.text.SpannedString -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column @@ -33,12 +32,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter +import coil3.compose.AsyncImage +import coil3.compose.AsyncImagePainter import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow @@ -55,24 +52,25 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.wysiwyg.compose.EditorStyledText +import io.element.android.wysiwyg.link.Link -@OptIn(ExperimentalFoundationApi::class) @Composable fun TimelineItemImageView( content: TimelineItemImageContent, hideMediaContent: Boolean, onContentClick: (() -> Unit)?, onLongClick: (() -> Unit)?, - onLinkClick: (String) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, onShowContentClick: () -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, ) { - val description = stringResource(CommonStrings.common_image) - Column( - modifier = modifier.semantics { contentDescription = description }, - ) { + val a11yLabel = stringResource(CommonStrings.common_image) + val description = content.caption?.let { "$a11yLabel: $it" } ?: a11yLabel + Column(modifier = modifier) { val containerModifier = if (content.showCaption) { Modifier.clip(RoundedCornerShape(10.dp)) } else { @@ -91,9 +89,18 @@ fun TimelineItemImageView( modifier = Modifier .fillMaxWidth() .then(if (isLoaded) Modifier.background(Color.White) else Modifier) - .then(if (onContentClick != null) Modifier.combinedClickable(onClick = onContentClick, onLongClick = onLongClick) else Modifier), + .then( + if (!isTalkbackActive() && onContentClick != null) { + Modifier.combinedClickable( + onClick = onContentClick, + onLongClick = onLongClick + ) + } else { + Modifier + } + ), model = content.thumbnailMediaRequestData, - contentScale = ContentScale.Fit, + contentScale = ContentScale.Crop, alignment = Alignment.Center, contentDescription = description, onState = { isLoaded = it is AsyncImagePainter.State.Success }, @@ -120,6 +127,7 @@ fun TimelineItemImageView( text = caption, style = ElementRichTextEditorStyle.textStyle(), onLinkClickedListener = onLinkClick, + onLinkLongClickedListener = onLinkLongClick, releaseOnDetach = false, onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange), ) @@ -138,6 +146,7 @@ internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageCon onContentClick = {}, onLongClick = {}, onLinkClick = {}, + onLinkLongClick = {}, onContentLayoutChange = {}, ) } @@ -152,6 +161,7 @@ internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview { onContentClick = {}, onLongClick = {}, onLinkClick = {}, + onLinkLongClick = {}, onContentLayoutChange = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt index 7343846dfe7..74594210b0f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -53,7 +52,7 @@ fun TimelineItemInformativeView( ) { Icon( resourceId = iconResourceId, - tint = MaterialTheme.colorScheme.secondary, + tint = ElementTheme.colors.iconSecondary, contentDescription = iconDescription, modifier = Modifier.size(16.dp) ) @@ -61,7 +60,7 @@ fun TimelineItemInformativeView( Spacer(modifier = Modifier.width(4.dp)) Text( fontStyle = FontStyle.Italic, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodyMdRegular, text = text ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLegacyCallInviteView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLegacyCallInviteView.kt index 09d28b49fd1..c2fa1edfa0e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLegacyCallInviteView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLegacyCallInviteView.kt @@ -31,7 +31,7 @@ fun TimelineItemLegacyCallInviteView( modifier = modifier, ) { Icon( - imageVector = CompoundIcons.VoiceCall(), + imageVector = CompoundIcons.VoiceCallSolid(), contentDescription = null, tint = ElementTheme.colors.iconSecondary, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt index 536c5374b59..334a7463f7e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt @@ -7,7 +7,6 @@ package io.element.android.features.messages.impl.timeline.components.event -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign @@ -25,7 +24,7 @@ fun TimelineItemStateView( ) { Text( modifier = modifier, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodyMdRegular, text = content.body, textAlign = TextAlign.Center, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt index 47402f4f75f..dc7cfb2d521 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt @@ -7,7 +7,6 @@ package io.element.android.features.messages.impl.timeline.components.event -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column @@ -25,8 +24,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter -import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter +import coil3.compose.AsyncImage +import coil3.compose.AsyncImagePainter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider import io.element.android.features.messages.impl.timeline.protection.ProtectedView @@ -39,7 +38,6 @@ import io.element.android.libraries.ui.strings.CommonStrings private const val STICKER_SIZE_IN_DP = 128 -@OptIn(ExperimentalFoundationApi::class) @Composable fun TimelineItemStickerView( content: TimelineItemStickerContent, @@ -68,7 +66,18 @@ fun TimelineItemStickerView( modifier = Modifier .fillMaxSize() .then(if (isLoaded) Modifier.background(Color.White) else Modifier) - .then(if (onContentClick != null) Modifier.combinedClickable(onClick = onContentClick, onLongClick = onLongClick) else Modifier), + .then( + if (onContentClick != null) { + Modifier + .combinedClickable( + onClick = onContentClick, + onLongClick = onLongClick, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) + } else { + Modifier + } + ), model = MediaRequestData( source = content.preferredMediaSource, kind = MediaRequestData.Kind.File( @@ -76,7 +85,7 @@ fun TimelineItemStickerView( mimeType = content.mimeType, ), ), - contentScale = ContentScale.Fit, + contentScale = ContentScale.Crop, alignment = Alignment.Center, contentDescription = description, onState = { isLoaded = it is AsyncImagePainter.State.Success }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt index debc03e0ebf..d496b94f3b4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt @@ -7,46 +7,42 @@ package io.element.android.features.messages.impl.timeline.components.event -import android.text.SpannableString +import android.text.SpannedString import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.Box import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentProvider +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.utils.containsOnlyEmojis +import io.element.android.libraries.androidutils.text.LinkifyHelper import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache -import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle -import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme -import io.element.android.libraries.textcomposer.mentions.MentionSpan -import io.element.android.libraries.textcomposer.mentions.getMentionSpans -import io.element.android.libraries.textcomposer.mentions.updateMentionStyles +import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanUpdater import io.element.android.wysiwyg.compose.EditorStyledText +import io.element.android.wysiwyg.link.Link @Composable fun TimelineItemTextView( content: TimelineItemTextBasedContent, - onLinkClick: (String) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, modifier: Modifier = Modifier, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit = {}, ) { - val emojiOnly = (content.formattedBody == null || content.formattedBody.toString() == content.body) && + val emojiOnly = content.formattedBody.toString() == content.body && content.body.replace(" ", "").containsOnlyEmojis() val textStyle = when { emojiOnly -> ElementTheme.typography.fontHeadingXlRegular @@ -56,11 +52,12 @@ fun TimelineItemTextView( LocalContentColor provides ElementTheme.colors.textPrimary, LocalTextStyle provides textStyle ) { - val body = getTextWithResolvedMentions(content) + val text = getTextWithResolvedMentions(content) Box(modifier.semantics { contentDescription = content.plainText }) { EditorStyledText( - text = body, + text = text, onLinkClickedListener = onLinkClick, + onLinkLongClickedListener = onLinkLongClick, style = ElementRichTextEditorStyle.textStyle(), onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange), releaseOnDetach = false, @@ -72,36 +69,9 @@ fun TimelineItemTextView( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @Composable internal fun getTextWithResolvedMentions(content: TimelineItemTextBasedContent): CharSequence { - val userProfileCache = LocalRoomMemberProfilesCache.current - val lastCacheUpdate by userProfileCache.lastCacheUpdate.collectAsState() - val mentionSpanTheme = LocalMentionSpanTheme.current - val formattedBody = content.formattedBody ?: content.pillifiedBody - val textWithMentions = remember(formattedBody, mentionSpanTheme, lastCacheUpdate) { - updateMentionSpans(formattedBody, userProfileCache) - mentionSpanTheme.updateMentionStyles(formattedBody) - formattedBody - } - return SpannableString(textWithMentions) -} - -private fun updateMentionSpans(text: CharSequence, cache: RoomMemberProfilesCache): Boolean { - var changedContents = false - for (mentionSpan in text.getMentionSpans()) { - when (mentionSpan.type) { - MentionSpan.Type.USER -> { - val displayName = cache.getDisplayName(UserId(mentionSpan.rawValue)) ?: mentionSpan.rawValue - if (mentionSpan.text != displayName) { - changedContents = true - mentionSpan.text = displayName - } - } - // There's no need to do anything for `@room` pills - MentionSpan.Type.EVERYONE -> Unit - // Nothing yet for room mentions - MentionSpan.Type.ROOM -> Unit - } - } - return changedContents + val mentionSpanUpdater = LocalMentionSpanUpdater.current + val bodyWithResolvedMentions = mentionSpanUpdater.rememberMentionSpans(content.formattedBody) + return SpannedString.valueOf(bodyWithResolvedMentions) } @PreviewsDayNight @@ -112,5 +82,32 @@ internal fun TimelineItemTextViewPreview( TimelineItemTextView( content = content, onLinkClick = {}, + onLinkLongClick = {}, + ) +} + +@Preview +@Composable +internal fun TimelineItemTextViewWithLinkifiedUrlPreview() = ElementPreview { + val content = aTimelineItemTextContent( + formattedBody = LinkifyHelper.linkify("The link should end after the first '?' (url: github.com/element-hq/element-x-android/README?)?.") + ) + TimelineItemTextView( + content = content, + onLinkClick = {}, + onLinkLongClick = {}, + ) +} + +@Preview +@Composable +internal fun TimelineItemTextViewWithLinkifiedUrlAndNestedParenthesisPreview() = ElementPreview { + val content = aTimelineItemTextContent( + formattedBody = LinkifyHelper.linkify("The link should end after the '(ME)' ((url: github.com/element-hq/element-x-android/READ(ME)))!") + ) + TimelineItemTextView( + content = content, + onLinkClick = {}, + onLinkLongClick = {}, ) } 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 d917079ef35..18a59325d59 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 @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl.timeline.components.event import android.text.SpannedString -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -20,8 +19,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable @@ -38,13 +35,14 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter +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.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout @@ -64,9 +62,10 @@ import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.wysiwyg.compose.EditorStyledText +import io.element.android.wysiwyg.link.Link -@OptIn(ExperimentalFoundationApi::class) @Composable fun TimelineItemVideoView( content: TimelineItemVideoContent, @@ -74,14 +73,15 @@ fun TimelineItemVideoView( onContentClick: (() -> Unit)?, onLongClick: (() -> Unit)?, onShowContentClick: () -> Unit, - onLinkClick: (String) -> Unit, + onLinkClick: (Link) -> Unit, + onLinkLongClick: (Link) -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, modifier: Modifier = Modifier, ) { - val description = stringResource(CommonStrings.common_video) - Column( - modifier = modifier.semantics { contentDescription = description } - ) { + val isTalkbackActive = isTalkbackActive() + val a11yLabel = stringResource(CommonStrings.common_video) + val description = content.caption?.let { "$a11yLabel: $it" } ?: a11yLabel + Column(modifier = modifier) { val containerModifier = if (content.showCaption) { Modifier .padding(top = 6.dp) @@ -103,7 +103,16 @@ fun TimelineItemVideoView( modifier = Modifier .fillMaxWidth() .then(if (isLoaded) Modifier.background(Color.White) else Modifier) - .then(if (onContentClick != null) Modifier.combinedClickable(onClick = onContentClick, onLongClick = onLongClick) else Modifier), + .then( + if (!isTalkbackActive && onContentClick != null) { + Modifier.combinedClickable( + onClick = onContentClick, + onLongClick = onLongClick + ) + } else { + Modifier + } + ), model = MediaRequestData( source = content.thumbnailSource, kind = MediaRequestData.Kind.Thumbnail( @@ -111,7 +120,7 @@ fun TimelineItemVideoView( height = content.thumbnailHeight?.toLong() ?: MAX_THUMBNAIL_HEIGHT, ) ), - contentScale = ContentScale.Fit, + contentScale = ContentScale.Crop, alignment = Alignment.Center, contentDescription = description, onState = { isLoaded = it is AsyncImagePainter.State.Success }, @@ -122,9 +131,10 @@ fun TimelineItemVideoView( contentAlignment = Alignment.Center, ) { Image( - Icons.Default.PlayArrow, + imageVector = CompoundIcons.PlaySolid(), contentDescription = stringResource(id = CommonStrings.a11y_play), colorFilter = ColorFilter.tint(Color.White), + modifier = Modifier.semantics { hideFromAccessibility() } ) } } @@ -148,6 +158,7 @@ fun TimelineItemVideoView( .widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio), text = caption, onLinkClickedListener = onLinkClick, + onLinkLongClickedListener = onLinkLongClick, style = ElementRichTextEditorStyle.textStyle(), releaseOnDetach = false, onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange), @@ -167,6 +178,7 @@ internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoCon onContentClick = {}, onLongClick = {}, onLinkClick = {}, + onLinkLongClick = {}, onContentLayoutChange = {}, ) } @@ -181,6 +193,7 @@ internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview { onContentClick = {}, onLongClick = {}, onLinkClick = {}, + onLinkLongClick = {}, onContentLayoutChange = {}, ) } 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 46ae92b22d6..6ff38d1970c 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 @@ -28,10 +28,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.onClick import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -41,7 +42,6 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider -import io.element.android.libraries.androidutils.accessibility.isScreenReaderEnabled import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -50,6 +50,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents import io.element.android.libraries.voiceplayer.api.VoiceMessageState import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider @@ -77,10 +78,27 @@ fun TimelineItemVoiceView( } val a11y = stringResource(CommonStrings.common_voice_message) + val a11yActionLabel = stringResource( + when (state.button) { + VoiceMessageState.Button.Play -> CommonStrings.a11y_play + VoiceMessageState.Button.Pause -> CommonStrings.a11y_pause + VoiceMessageState.Button.Downloading -> CommonStrings.common_downloading + VoiceMessageState.Button.Retry -> CommonStrings.action_retry + VoiceMessageState.Button.Disabled -> CommonStrings.error_unknown + } + ) Row( modifier = modifier - .semantics { + .clearAndSetSemantics { contentDescription = a11y + if (state.button == VoiceMessageState.Button.Disabled) { + disabled() + } else if (state.button in listOf(VoiceMessageState.Button.Play, VoiceMessageState.Button.Pause)) { + onClick(label = a11yActionLabel) { + playPause() + true + } + } } .onSizeChanged { onContentLayoutChange( @@ -92,29 +110,32 @@ fun TimelineItemVoiceView( }, verticalAlignment = Alignment.CenterVertically, ) { - when (state.button) { - VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause) - VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause) - VoiceMessageState.Button.Downloading -> ProgressButton() - VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause) - VoiceMessageState.Button.Disabled -> PlayButton(onClick = {}, enabled = false) + if (!isTalkbackActive()) { + when (state.button) { + VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause) + VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause) + VoiceMessageState.Button.Downloading -> ProgressButton() + VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause) + VoiceMessageState.Button.Disabled -> PlayButton(onClick = {}, enabled = false) + } } Spacer(Modifier.width(8.dp)) Text( text = state.time, - color = ElementTheme.materialColors.secondary, + color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodySmMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, ) Spacer(Modifier.width(8.dp)) - val context = LocalContext.current WaveformPlaybackView( showCursor = state.showCursor, playbackProgress = state.progress, waveform = content.waveform, - modifier = Modifier.height(34.dp).weight(1f), - seekEnabled = !context.isScreenReaderEnabled(), + modifier = Modifier + .weight(1f) + .height(34.dp), + seekEnabled = !isTalkbackActive(), onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) }, ) TextButton(onClick = ::changeSpeed) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt index a11b36f58fc..22825b532cd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt @@ -16,8 +16,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -25,6 +25,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -50,7 +53,15 @@ fun GroupHeaderView( Box( modifier = modifier - .fillMaxWidth(), + .fillMaxWidth() + .toggleable( + value = isExpanded, + onValueChange = { onClick() }, + role = Role.DropdownList, + ) + .clearAndSetSemantics { + contentDescription = text + }, contentAlignment = Alignment.Center ) { Surface( @@ -67,7 +78,7 @@ fun GroupHeaderView( ) { Text( text = text, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodyMdRegular, ) val rotation: Float by animateFloatAsState( @@ -82,7 +93,7 @@ fun GroupHeaderView( modifier = Modifier.rotate(rotation), imageVector = CompoundIcons.ChevronRight(), contentDescription = null, - tint = MaterialTheme.colorScheme.secondary + tint = ElementTheme.colors.iconSecondary ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt index 648f3254e55..a5c7eb89bed 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt @@ -15,7 +15,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.roomMembers import io.element.android.libraries.matrix.api.user.MatrixUser @@ -24,7 +24,7 @@ import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject class ReactionSummaryPresenter @Inject constructor( - private val room: MatrixRoom, + private val room: BaseRoom, ) : Presenter { @Composable override fun present(): ReactionSummaryState { 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 b4b02ca45d6..081d93465f9 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 @@ -7,7 +7,6 @@ package io.element.android.features.messages.impl.timeline.components.reactionsummary -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -28,6 +27,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api @@ -46,17 +46,23 @@ 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.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import io.element.android.compound.theme.ElementTheme +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionDetails import io.element.android.features.messages.impl.timeline.components.REACTION_IMAGE_ASPECT_RATIO import io.element.android.features.messages.impl.timeline.model.AggregatedReaction 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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp @@ -94,7 +100,6 @@ fun ReactionSummaryView( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun ReactionSummaryViewContent( summary: ReactionSummaryState.Summary, @@ -142,9 +147,7 @@ private fun ReactionSummaryViewContent( HorizontalPager(state = pagerState) { page -> LazyColumn(modifier = Modifier.fillMaxHeight()) { items(summary.reactions[page].senders) { sender -> - val user = sender.user ?: MatrixUser(userId = sender.senderId) - SenderRow( avatarData = user.getAvatarData(AvatarSize.UserListItem), name = user.displayName ?: user.userId.value, @@ -168,21 +171,32 @@ private fun AggregatedReactionButton( } else { Color.Transparent } - val textColor = if (isHighlighted) { MaterialTheme.colorScheme.inversePrimary } else { - MaterialTheme.colorScheme.primary + ElementTheme.colors.textPrimary } - val roundedCornerShape = RoundedCornerShape(corner = CornerSize(percent = 50)) + val a11yText = a11yReactionDetails( + emoji = reaction.key, + userAlreadyReacted = reaction.isHighlighted, + reactionCount = reaction.count, + ) Surface( modifier = Modifier .background(buttonColor, roundedCornerShape) .clip(roundedCornerShape) .clickable(onClick = onClick) - .padding(vertical = 8.dp, horizontal = 12.dp), - color = buttonColor + .padding(vertical = 8.dp, horizontal = 12.dp) + .selectable( + selected = isHighlighted, + role = Role.Tab, + onClick = onClick, + ) + .clearAndSetSemantics { + contentDescription = a11yText + }, + color = buttonColor, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -232,10 +246,14 @@ private fun SenderRow( modifier = Modifier .fillMaxWidth() .heightIn(min = 56.dp) - .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp), + .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp) + .semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically ) { - Avatar(avatarData) + Avatar( + avatarData = avatarData, + avatarType = AvatarType.User, + ) Column( modifier = Modifier.padding(start = 12.dp), ) { @@ -250,12 +268,12 @@ private fun SenderRow( text = name, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontBodyMdRegular, ) Text( text = sentTime, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodySmRegular, @@ -263,7 +281,7 @@ private fun SenderRow( } Text( text = userId, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodySmRegular, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt index 3e11e1e0aee..fd6e5cea532 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -27,6 +26,8 @@ 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.hideFromAccessibility +import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex @@ -36,6 +37,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.avatar.getBestName import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -43,7 +45,6 @@ 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.timeline.item.event.LocalEventSendState 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 kotlinx.collections.immutable.ImmutableList @@ -57,16 +58,19 @@ fun TimelineItemReadReceiptView( ) { if (state.receipts.isNotEmpty()) { if (renderReadReceipts) { - ReadReceiptsRow(modifier = modifier) { + ReadReceiptsRow( + modifier = modifier.clearAndSetSemantics { + hideFromAccessibility() + } + ) { ReadReceiptsAvatars( receipts = state.receipts, modifier = Modifier - .testTag(TestTags.messageReadReceipts) - .clip(RoundedCornerShape(4.dp)) - .clickable { - onReadReceiptsClick() - } - .padding(2.dp) + .clip(RoundedCornerShape(4.dp)) + .clickable { + onReadReceiptsClick() + } + .padding(2.dp) ) } } @@ -109,9 +113,9 @@ private fun ReadReceiptsRow( ) { Row( modifier = modifier - .fillMaxWidth() - .height(AvatarSize.TimelineReadReceipt.dp + 8.dp) - .padding(horizontal = 18.dp), + .fillMaxWidth() + .height(AvatarSize.TimelineReadReceipt.dp + 8.dp) + .padding(horizontal = 18.dp), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { @@ -131,11 +135,12 @@ private fun ReadReceiptsAvatars( ) { val avatarSize = AvatarSize.TimelineReadReceipt.dp val avatarStrokeSize = 1.dp - val avatarStrokeColor = MaterialTheme.colorScheme.background + val avatarStrokeColor = ElementTheme.colors.bgCanvasDefault val receiptDescription = computeReceiptDescription(receipts) Row( modifier = modifier .clearAndSetSemantics { + testTag = TestTags.messageReadReceipts.value contentDescription = receiptDescription }, horizontalArrangement = Arrangement.spacedBy(4.dp - avatarStrokeSize), @@ -150,15 +155,16 @@ private fun ReadReceiptsAvatars( .forEachIndexed { index, readReceiptData -> Box( modifier = Modifier - .padding(end = (12.dp + avatarStrokeSize * 2) * index) - .size(size = avatarSize + avatarStrokeSize * 2) - .clip(CircleShape) - .background(avatarStrokeColor) - .zIndex(index.toFloat()), + .padding(end = (12.dp + avatarStrokeSize * 2) * index) + .size(size = avatarSize + avatarStrokeSize * 2) + .clip(CircleShape) + .background(avatarStrokeColor) + .zIndex(index.toFloat()), contentAlignment = Alignment.Center, ) { Avatar( avatarData = readReceiptData.avatarData, + avatarType = AvatarType.User, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemDaySeparatorView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemDaySeparatorView.kt index 86dce5349fc..08fe34b724e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemDaySeparatorView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemDaySeparatorView.kt @@ -11,10 +11,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.heading +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 @@ -37,9 +38,13 @@ internal fun TimelineItemDaySeparatorView( contentAlignment = Alignment.Center, ) { Text( + modifier = Modifier + .semantics { + heading() + }, text = model.formattedDate, style = ElementTheme.typography.fontBodyMdMedium, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemReadMarkerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemReadMarkerView.kt index 039a60d6709..b666f6ad569 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemReadMarkerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemReadMarkerView.kt @@ -37,13 +37,13 @@ internal fun TimelineItemReadMarkerView( Text( text = stringResource(id = R.string.screen_room_timeline_read_marker_title).uppercase(), style = ElementTheme.typography.fontBodySmMedium, - color = ElementTheme.colors.textSecondary, + color = ElementTheme.colors.textActionAccent, ) HorizontalDivider( modifier = Modifier .fillMaxWidth() .padding(horizontal = 2.dp), - color = ElementTheme.colors.borderInteractivePrimary, + color = ElementTheme.colors.textActionAccent, ) } } 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 f6a8c3821e0..72035cdc3df 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 @@ -7,11 +7,11 @@ package io.element.android.features.messages.impl.timeline.components.virtual +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -20,44 +20,83 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme 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.text.toAnnotatedString import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.allBooleans +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.tombstone.PredecessorRoom @Composable fun TimelineItemRoomBeginningView( roomName: String?, + predecessorRoom: PredecessorRoom?, + isDm: Boolean, + onPredecessorRoomClick: (RoomId) -> Unit, modifier: Modifier = Modifier ) { - Box( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - contentAlignment = Alignment.Center, + Column( + modifier = modifier.fillMaxWidth() ) { - val text = if (roomName == null) { - stringResource(id = R.string.screen_room_timeline_beginning_of_room_no_name) - } else { - stringResource(id = R.string.screen_room_timeline_beginning_of_room, roomName) + if (predecessorRoom != null) { + ComposerAlertMolecule( + avatar = null, + content = stringResource(R.string.screen_room_timeline_upgraded_room_message).toAnnotatedString(), + onSubmitClick = { onPredecessorRoomClick(predecessorRoom.roomId) }, + isCritical = false, + submitText = stringResource(R.string.screen_room_timeline_upgraded_room_action) + ) + } + // Only display for non-DM room + if (!isDm) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + contentAlignment = Alignment.Center, + ) { + val text = if (roomName == null) { + stringResource(id = R.string.screen_room_timeline_beginning_of_room_no_name) + } else { + stringResource(id = R.string.screen_room_timeline_beginning_of_room, roomName) + } + Text( + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodyMdRegular, + text = text, + textAlign = TextAlign.Center, + ) + } } - Text( - color = MaterialTheme.colorScheme.secondary, - style = ElementTheme.typography.fontBodyMdRegular, - text = text, - textAlign = TextAlign.Center, - ) } } @PreviewsDayNight @Composable internal fun TimelineItemRoomBeginningViewPreview() = ElementPreview { - Column { - TimelineItemRoomBeginningView( - roomName = null, - ) - TimelineItemRoomBeginningView( - roomName = "Room Name", - ) + Column(verticalArrangement = spacedBy(16.dp)) { + allBooleans.forEach { isDm -> + TimelineItemRoomBeginningView( + predecessorRoom = null, + roomName = null, + isDm = isDm, + onPredecessorRoomClick = {}, + ) + TimelineItemRoomBeginningView( + predecessorRoom = null, + roomName = "Room Name", + isDm = isDm, + onPredecessorRoomClick = {}, + ) + TimelineItemRoomBeginningView( + predecessorRoom = PredecessorRoom(RoomId("!roomId:matrix.org"), EventId("\$eventId:matrix.org")), + roomName = "Room Name", + isDm = isDm, + onPredecessorRoomClick = {}, + ) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt index a9467a9a8f7..cd22006804a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt @@ -44,7 +44,6 @@ import io.element.android.compound.tokens.generated.CompoundIcons 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.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text @@ -72,12 +71,7 @@ fun EventDebugInfoView( Scaffold( topBar = { TopAppBar( - title = { - Text( - text = "Debug event info", - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = "Debug event info", navigationIcon = { BackButton(onClick = onBackClick) } ) }, 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 8ec1dcdb8a1..f3b54dae534 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 @@ -7,13 +7,10 @@ package io.element.android.features.messages.impl.timeline.factories.event -import android.text.Spannable import android.text.style.URLSpan -import android.text.util.Linkify import androidx.core.text.buildSpannedString import androidx.core.text.getSpans import androidx.core.text.toSpannable -import androidx.core.text.util.LinkifyCompat import io.element.android.features.location.api.Location import io.element.android.features.messages.api.timeline.HtmlConverterProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent @@ -29,6 +26,7 @@ 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.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 import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags @@ -71,13 +69,16 @@ class TimelineItemContentMessageFactory @Inject constructor( return when (val messageType = content.type) { is EmoteMessageType -> { val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}" + val formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisambiguatedDisplayName") ?: textPillificationHelper.pillify( + emoteBody + ).safeLinkify() TimelineItemEmoteContent( body = emoteBody, htmlDocument = messageType.formatted?.toHtmlDocument( permalinkParser = permalinkParser, prefix = "* $senderDisambiguatedDisplayName", ), - formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisambiguatedDisplayName") ?: emoteBody.withLinks(), + formattedBody = formattedBody, isEdited = content.isEdited, ) } @@ -125,10 +126,8 @@ class TimelineItemContentMessageFactory @Inject constructor( val body = messageType.body.trimEnd() TimelineItemTextContent( body = body, - pillifiedBody = textPillificationHelper.pillify(body), htmlDocument = null, - plainText = body, - formattedBody = null, + formattedBody = body, isEdited = content.isEdited, ) } else { @@ -221,20 +220,26 @@ class TimelineItemContentMessageFactory @Inject constructor( } is NoticeMessageType -> { val body = messageType.body.trimEnd() + val formattedBody = parseHtml(messageType.formatted) ?: textPillificationHelper.pillify( + body + ).safeLinkify() + val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser) TimelineItemNoticeContent( body = body, - htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser), - formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(), + htmlDocument = htmlDocument, + formattedBody = formattedBody, isEdited = content.isEdited, ) } is TextMessageType -> { val body = messageType.body.trimEnd() + val formattedBody = parseHtml(messageType.formatted) ?: textPillificationHelper.pillify( + body + ).safeLinkify() TimelineItemTextContent( body = body, - pillifiedBody = textPillificationHelper.pillify(body), htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser), - formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(), + formattedBody = formattedBody, isEdited = content.isEdited, ) } @@ -242,9 +247,8 @@ class TimelineItemContentMessageFactory @Inject constructor( val body = messageType.body.trimEnd() TimelineItemTextContent( body = body, - pillifiedBody = textPillificationHelper.pillify(body), htmlDocument = null, - formattedBody = body.withLinks(), + formattedBody = textPillificationHelper.pillify(body).safeLinkify(), isEdited = content.isEdited, ) } @@ -265,7 +269,8 @@ class TimelineItemContentMessageFactory @Inject constructor( if (formattedBody == null || formattedBody.format != MessageFormat.HTML) return null val result = htmlConverterProvider.provide() .fromHtmlToSpans(formattedBody.body.trimEnd()) - .withFixedURLSpans() + .let { textPillificationHelper.pillify(it) } + .safeLinkify() return if (prefix != null) { buildSpannedString { append(prefix) @@ -276,36 +281,11 @@ class TimelineItemContentMessageFactory @Inject constructor( result } } - - private fun CharSequence.withFixedURLSpans(): CharSequence { - val spannable = this.toSpannable() - // Get all URL spans, as they will be removed by LinkifyCompat.addLinks - val oldURLSpans = spannable.getSpans(0, length).associateWith { - val start = spannable.getSpanStart(it) - val end = spannable.getSpanEnd(it) - Pair(start, end) - } - // Find and set as URLSpans any links present in the text - LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES) - // Restore old spans, remove new ones if there is a conflict - for ((urlSpan, location) in oldURLSpans) { - val (start, end) = location - val addedSpans = spannable.getSpans(start, end).orEmpty() - if (addedSpans.isNotEmpty()) { - for (span in addedSpans) { - spannable.removeSpan(span) - } - } - spannable.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } - return spannable - } } @Suppress("USELESS_ELVIS") private fun String.withLinks(): CharSequence? { // Note: toSpannable() can return null when running unit tests - val spannable = toSpannable() ?: return null - val addedLinks = LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES) - return spannable.takeIf { addedLinks } + val spannable = safeLinkify().toSpannable() ?: return null + return spannable.takeIf { spannable.getSpans(0, length).isNotEmpty() } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleState.kt index 07219f42811..81aa4594138 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleState.kt @@ -13,7 +13,6 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou data class BubbleState( val groupPosition: TimelineItemGroupPosition, val isMine: Boolean, - val isHighlighted: Boolean, val timelineRoomInfo: TimelineRoomInfo, ) { /** True to cut out the top start corner of the bubble, to give margin for the sender avatar. */ diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt index 97b69b1f643..02a6e19504e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt @@ -21,15 +21,11 @@ open class BubbleStateProvider : PreviewParameterProvider { TimelineItemGroupPosition.None, ).map { groupPosition -> sequenceOf(false, true).map { isMine -> - sequenceOf(false, true).map { isHighlighted -> - aBubbleState( - groupPosition = groupPosition, - isMine = isMine, - isHighlighted = isHighlighted, - ) - } + aBubbleState( + groupPosition = groupPosition, + isMine = isMine, + ) } - .flatten() } .flatten() } @@ -37,11 +33,9 @@ open class BubbleStateProvider : PreviewParameterProvider { internal fun aBubbleState( groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First, isMine: Boolean = false, - isHighlighted: Boolean = false, timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), ) = BubbleState( groupPosition = groupPosition, isMine = isMine, - isHighlighted = isHighlighted, timelineRoomInfo = timelineRoomInfo, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt index 82cd5a3f398..dc04e5b2695 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt @@ -12,11 +12,10 @@ import org.jsoup.nodes.Document data class TimelineItemEmoteContent( override val body: String, - override val pillifiedBody: CharSequence = body, override val htmlDocument: Document?, - override val plainText: String = htmlDocument?.toPlainText() ?: body, - override val formattedBody: CharSequence?, + override val formattedBody: CharSequence, override val isEdited: Boolean, ) : TimelineItemTextBasedContent { override val type: String = "TimelineItemEmoteContent" + override val plainText: String = htmlDocument?.toPlainText() ?: body } 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 9eda2e72536..6c20d7326f9 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 @@ -92,6 +92,11 @@ fun TimelineItemEventContent.isEdited(): Boolean = when (this) { else -> false } +/** + * Whether the event content has been redacted. + */ +fun TimelineItemEventContent.isRedacted(): Boolean = this is TimelineItemRedactedContent + fun TimelineItemEventContentWithAttachment.duration(): Duration? { return when (this) { is TimelineItemAudioContent -> duration 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 09484ac7e3b..36c32222bec 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 @@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent +import org.jsoup.nodes.Document class TimelineItemEventContentProvider : PreviewParameterProvider { override val values = sequenceOf( @@ -58,35 +59,46 @@ class TimelineItemTextBasedContentProvider : PreviewParameterProvider { + private val allowedEvents = mutableStateOf>(setOf()) + @Composable override fun present(): TimelineProtectionState { - val hideMediaContent by appPreferencesStore.doesHideImagesAndVideosFlow().collectAsState(initial = false) - var allowedEvents by remember { mutableStateOf>(setOf()) } - val protectionState by remember(hideMediaContent) { + val mediaPreviewValue = remember { + mediaPreviewService.mediaPreviewConfigFlow.mapState { config -> config.mediaPreviewValue } + }.collectAsState() + val roomInfo = room.roomInfoFlow.collectAsState() + val protectionState by remember { derivedStateOf { - if (hideMediaContent) { - ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet()) - } else { + val isPreviewEnabled = mediaPreviewValue.value.isPreviewEnabled(roomInfo.value.joinRule) + if (isPreviewEnabled) { ProtectionState.RenderAll + } else { + ProtectionState.RenderOnly(eventIds = allowedEvents.value.toImmutableSet()) } } } @@ -40,7 +48,7 @@ class TimelineProtectionPresenter @Inject constructor( fun handleEvent(event: TimelineProtectionEvent) { when (event) { is TimelineProtectionEvent.ShowContent -> { - allowedEvents = allowedEvents + setOfNotNull(event.eventId) + allowedEvents.value = allowedEvents.value + setOfNotNull(event.eventId) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt index 35759ade742..7d293055a29 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt @@ -18,7 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.roomMembers import io.element.android.libraries.preferences.api.store.SessionPreferencesStore @@ -32,12 +32,14 @@ import kotlinx.coroutines.flow.onEach import javax.inject.Inject class TypingNotificationPresenter @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val sessionPreferencesStore: SessionPreferencesStore, ) : Presenter { @Composable override fun present(): TypingNotificationState { - val renderTypingNotifications by sessionPreferencesStore.isRenderTypingNotificationsEnabled().collectAsState(initial = true) + val renderTypingNotifications by remember { + sessionPreferencesStore.isRenderTypingNotificationsEnabled() + }.collectAsState(initial = true) val typingMembersState by produceState(initialValue = persistentListOf(), key1 = renderTypingNotifications) { if (renderTypingNotifications) { observeRoomTypingMembers() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/TextPillificationHelper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/TextPillificationHelper.kt index c6dc27870b9..458c27061c6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/TextPillificationHelper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/TextPillificationHelper.kt @@ -9,6 +9,9 @@ package io.element.android.features.messages.impl.utils import android.text.Spannable import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.URLSpan +import android.util.Patterns import androidx.core.text.getSpans import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.RoomScope @@ -16,63 +19,90 @@ import io.element.android.libraries.matrix.api.core.MatrixPatternType import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.permalink.PermalinkParser -import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache -import io.element.android.libraries.textcomposer.mentions.MentionSpan import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.mentions.getMentionSpans +import io.element.android.wysiwyg.view.spans.CodeBlockSpan +import io.element.android.wysiwyg.view.spans.InlineCodeSpan import javax.inject.Inject interface TextPillificationHelper { - fun pillify(text: CharSequence): CharSequence + fun pillify(text: CharSequence, pillifyPermalinks: Boolean = true): CharSequence } @ContributesBinding(RoomScope::class) class DefaultTextPillificationHelper @Inject constructor( private val mentionSpanProvider: MentionSpanProvider, - private val permalinkBuilder: PermalinkBuilder, private val permalinkParser: PermalinkParser, - private val roomMemberProfilesCache: RoomMemberProfilesCache, + private val permalinkBuilder: PermalinkBuilder, ) : TextPillificationHelper { @Suppress("LoopWithTooManyJumpStatements") - override fun pillify(text: CharSequence): CharSequence { - val matches = MatrixPatterns.findPatterns(text, permalinkParser).sortedByDescending { it.end } - if (matches.isEmpty()) return text + override fun pillify(text: CharSequence, pillifyPermalinks: Boolean): CharSequence { + return SpannableStringBuilder(text).apply { + pillifyMatrixPatterns(this) + if (pillifyPermalinks) { + pillifyPermalinks(this) + } + } + } - val spannable = SpannableStringBuilder(text) + private fun pillifyMatrixPatterns(text: SpannableStringBuilder) { + val matches = MatrixPatterns.findPatterns(text, permalinkParser).sortedByDescending { it.end } + if (matches.isEmpty()) return for (match in matches) { + if (!text.canPillify(match.start, match.end)) continue when (match.type) { MatrixPatternType.USER_ID -> { - val mentionSpanExists = spannable.getSpans(match.start, match.end).isNotEmpty() - if (!mentionSpanExists) { - val userId = UserId(match.value) - val permalink = permalinkBuilder.permalinkForUser(userId).getOrNull() ?: continue - val mentionSpan = mentionSpanProvider.getMentionSpanFor(match.value, permalink) - roomMemberProfilesCache.getDisplayName(userId)?.let { mentionSpan.text = it } - spannable.replace(match.start, match.end, "@ ") - spannable.setSpan(mentionSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val userId = UserId(match.value) + val mentionSpan = mentionSpanProvider.createUserMentionSpan(userId) + text.replace(match.start, match.end, "@ ") + text.setSpan(mentionSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + permalinkBuilder.permalinkForUser(userId).getOrNull()?.also { permalink -> + // Also add a URLSpan in case of raw user id so it can be clicked + val urlSpan = URLSpan(permalink) + text.setSpan(urlSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } MatrixPatternType.ROOM_ALIAS -> { - val mentionSpanExists = spannable.getSpans(match.start, match.end).isNotEmpty() - if (!mentionSpanExists) { - val permalink = permalinkBuilder.permalinkForRoomAlias(RoomAlias(match.value)).getOrNull() ?: continue - val mentionSpan = mentionSpanProvider.getMentionSpanFor(match.value, permalink) - spannable.replace(match.start, match.end, "@ ") - spannable.setSpan(mentionSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val roomAlias = RoomAlias(match.value) + val mentionSpan = mentionSpanProvider.createRoomMentionSpan(roomAlias.toRoomIdOrAlias()) + text.replace(match.start, match.end, "@ ") + text.setSpan(mentionSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + permalinkBuilder.permalinkForRoomAlias(roomAlias).getOrNull()?.also { permalink -> + // Also add a URLSpan in case of raw room alias so it can be clicked + val urlSpan = URLSpan(permalink) + text.setSpan(urlSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } MatrixPatternType.AT_ROOM -> { - val mentionSpanExists = spannable.getSpans(match.start, match.end).isNotEmpty() - if (!mentionSpanExists) { - val mentionSpan = mentionSpanProvider.getMentionSpanFor("@room", "") - spannable.replace(match.start, match.end, "@ ") - spannable.setSpan(mentionSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } + val mentionSpan = mentionSpanProvider.createEveryoneMentionSpan() + text.replace(match.start, match.end, "@ ") + text.setSpan(mentionSpan, match.start, match.start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } else -> Unit } } - return spannable + } + + private fun pillifyPermalinks(text: SpannableStringBuilder) { + for (match in Patterns.WEB_URL.toRegex().findAll(text)) { + val start = match.range.first + val end = match.range.last + 1 + if (!text.canPillify(start, end)) continue + val url = text.substring(match.range) + val mentionSpan = mentionSpanProvider.getMentionSpanFor(match.value, url) + if (mentionSpan != null) { + text.setSpan(mentionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + } + + private fun Spanned.canPillify(start: Int, end: Int): Boolean { + if (getMentionSpans(start, end).isNotEmpty()) return false + if (getSpans(start, end).isNotEmpty()) return false + if (getSpans(start, end).isNotEmpty()) return false + return true } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt index 1f44d3dd1f6..8a057709428 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/DefaultMessageSummaryFormatter.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.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.libraries.core.extensions.toSafeLength import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.RoomScope import io.element.android.libraries.ui.strings.CommonStrings @@ -35,11 +36,6 @@ import javax.inject.Inject class DefaultMessageSummaryFormatter @Inject constructor( @ApplicationContext private val context: Context, ) : MessageSummaryFormatter { - companion object { - // Max characters to display in the summary message. This works around https://github.com/element-hq/element-x-android/issues/2105 - private const val MAX_SAFE_LENGTH = 500 - } - override fun format(event: TimelineItem.Event): String { return when (event.content) { is TimelineItemTextBasedContent -> event.content.plainText @@ -58,6 +54,8 @@ class DefaultMessageSummaryFormatter @Inject constructor( is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio) is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call) is TimelineItemCallNotifyContent -> context.getString(CommonStrings.common_call_started) - }.take(MAX_SAFE_LENGTH) + } + // 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/VoiceMessageComposerPlayer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt index 6f0fa9735ad..57b18817f5b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.voicemessages.composer import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.mediaplayer.api.MediaPlayer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -26,11 +27,12 @@ import javax.inject.Inject * A media player for the voice message composer. * * @param mediaPlayer The [MediaPlayer] to use. - * @param coroutineScope + * @param sessionCoroutineScope */ class VoiceMessageComposerPlayer @Inject constructor( private val mediaPlayer: MediaPlayer, - private val coroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, ) { companion object { const val MIME_TYPE = MimeTypes.Ogg @@ -116,7 +118,7 @@ class VoiceMessageComposerPlayer @Inject constructor( seekJob?.cancelAndJoin() seekingTo.value = position - seekJob = coroutineScope.launch { + seekJob = sessionCoroutineScope.launch { val mediaState = mediaPlayer.ensureMediaReady(mediaPath) val duration = mediaState.duration ?: return@launch val positionMs = (duration * position).toLong() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index 032e00cd368..9b723ff72a7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.Lifecycle import im.vector.app.features.analytics.plan.Composer import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter @@ -44,7 +45,8 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds class VoiceMessageComposerPresenter @Inject constructor( - private val appCoroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val voiceRecorder: VoiceRecorder, private val analyticsService: AnalyticsService, private val mediaSender: MediaSender, @@ -74,11 +76,11 @@ class VoiceMessageComposerPresenter @Inject constructor( val onLifecycleEvent = { event: Lifecycle.Event -> when (event) { Lifecycle.Event.ON_PAUSE -> { - appCoroutineScope.finishRecording() + sessionCoroutineScope.finishRecording() player.pause() } Lifecycle.Event.ON_DESTROY -> { - appCoroutineScope.cancelRecording() + sessionCoroutineScope.cancelRecording() } else -> {} } @@ -145,7 +147,7 @@ class VoiceMessageComposerPresenter @Inject constructor( isSending = true player.pause() analyticsService.captureComposerEvent() - appCoroutineScope.launch { + sessionCoroutineScope.launch { val result = sendMessage( file = finishedState.file, mimeType = finishedState.mimeType, diff --git a/features/messages/impl/src/main/res/values-bg/translations.xml b/features/messages/impl/src/main/res/values-bg/translations.xml index 284177448c4..bd910f2e582 100644 --- a/features/messages/impl/src/main/res/values-bg/translations.xml +++ b/features/messages/impl/src/main/res/values-bg/translations.xml @@ -18,6 +18,7 @@ "Анкета" "Форматиране на текст" "Хронологията на съобщенията не е налична в момента." + "Всеки" "Добавяне на емоджи" "Това е началото на %1$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 af33e7376d1..3f5f94e595b 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -35,14 +35,30 @@ "Zobrazit méně" "Zpráva zkopírována" "Nemáte oprávnění zveřejňovat příspěvky v této místnosti" + + "%1$d člen reagoval s %2$s" + "%1$d členové reagovali s %2$s" + "%1$d členů reagovalo s %2$s" + + + "Vy a %1$d člen jste reagovali s %2$s" + "Vy a %1$d členové jste reagovali s %2$s" + "Vy a %1$d členů reagovalo s %2$s" + + "Reagovali jste s %1$s" "Zobrazit méně" "Zobrazit více" + "Zobrazit souhrn reakcí" "Nové" "%1$d změna místnosti" "%1$d změny místnosti" "%1$d změn místnosti" + "Přejít do nové místnosti" + "Tato místnost byla nahrazena a již není aktivní" + "Zobrazit staré zprávy" + "Tato místnost je pokračováním jiné místnosti" "%1$s, %2$s a %3$d další" "%1$s, %2$s a %3$d další" diff --git a/features/messages/impl/src/main/res/values-cy/translations.xml b/features/messages/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..eb277d2f759 --- /dev/null +++ b/features/messages/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,72 @@ + + + "Gweithgareddau" + "Baneri" + "Bwyd a Diod" + "Anifeiliaid a Natur" + "Gwrthrychau" + "Wynebau Hapus a Phobl" + "Teithio a Llefydd" + "Symbolau" + "Rhwystro defnyddiwr" + "Gwiriwch a ydych am guddio\'r holl negeseuon presennol ac yn y dyfodol gan y defnyddiwr hwn" + "Bydd y neges hon yn cael ei hadrodd i weinyddwr eich gweinyddwr cartref. Fyddan nhw ddim yn gallu darllen unrhyw negeseuon wedi\'u hamgryptio." + "Rheswm dros adrodd am y cynnwys hwn" + "Camera" + "Cymryd llun" + "Recordio fideo" + "Atodiad" + "Llyfrgell Ffotograffau a Fideo" + "Lleoliad" + "Pôl" + "Fformatio Testun" + "Nid yw hanes negeseuon ar gael ar hyn o bryd." + "Nid yw hanes negeseuon ar gael yn yr ystafell hon. Dilyswch y ddyfais hon i weld hanes eich neges." + "Hoffech chi eu gwahodd yn ôl?" + "Rydych chi ar eich pen eich hun yn y sgwrs hon" + "Rhowch wybod i\'r ystafell gyfan" + "Pawb" + "Anfon eto" + "Methodd eich neges ag anfon" + "Ychwanegu emoji" + "Dyma ddechrau %1$s." + "Dyma ddechrau\'r sgwrs hon." + "Galwad heb ei chefnogi. Gofynnwch a all y galwr ddefnyddio\'r ap Element X newydd." + "Dangos llai" + "Neges wedi\'i chopïo" + "Does gennych chi ddim caniatâd i bostio i\'r ystafell hon" + "Rydych chi wedi ymateb gyda %1$s" + "Dangos llai" + "Dangos rhagor" + "Dangos crynodeb o ymatebion" + "Newydd" + + "%1$d newid ystafelloedd" + "%1$d newid ystafell" + "%1$d newid ystafell" + "%1$d newid ystafell" + "%1$d newid ystafell" + "%1$d newid ystafell" + + "Neidio i ystafell newydd" + "Mae\'r ystafell hon wedi\'i disodli ac nid yw\'n weithredol mwyach" + "Gweld hen negeseuon" + "Mae\'r ystafell hon yn barhad o ystafell arall" + + "%1$s, %2$s a %3$d arall" + "%1$s, %2$s a %3$d arall" + "%1$s, %2$s a %3$d arall" + "%1$s, %2$s a %3$d arall" + "%1$s, %2$s a %3$d arall" + "%1$s, %2$s a %3$d arall" + + + "Mae %1$s yn teipio" + "Mae %1$s yn teipio" + "Mae %1$s yn teipio" + "Mae %1$s yn teipio" + "Mae %1$s yn teipio" + "Mae %1$s yn teipio" + + "%1$s a %2$s" + diff --git a/features/messages/impl/src/main/res/values-da/translations.xml b/features/messages/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..a249db99f3a --- /dev/null +++ b/features/messages/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,68 @@ + + + "Aktiviteter" + "Flag" + "Mad og drikke" + "Dyr og natur" + "Objekter" + "Smileys og mennesker" + "Rejser og steder" + "Symboler" + "Bloker bruger" + "Marker, hvis du vil skjule alle nuværende og fremtidige beskeder fra denne bruger" + "Denne meddelelse vil blive indberettet til administratoren af din hjemmeserver. De vil ikke være i stand til at læse nogen krypterede meddelelser." + "Årsag til indberetning af dette indhold" + "Kamera" + "Tag billede" + "Optag video" + "Vedhæftning" + "Foto- og videobibliotek" + "Lokation" + "Afstemning" + "Tekstformatering" + "Beskedhistorikken er i øjeblikket ikke tilgængelig." + "Beskedhistorikken er ikke tilgængelig i dette rum. Bekræft denne enhed for at se din beskedhistorik." + "Vil du invitere dem igen?" + "Du er alene i denne samtale" + "Giv hele rummet besked" + "Alle" + "Send igen" + "Din besked kunne ikke sendes" + "Tilføj emoji" + "Dette er begyndelsen på %1$s." + "Dette er begyndelsen på denne samtale." + "Ikke-understøttet opkald. Spørg, om den, der ringer op, kan bruge den nye Element X-app." + "Vis mindre" + "Beskeden er kopieret" + "Du har ikke tilladelse til at slå noget op i dette rum" + + "%1$d medlem reagerede med %2$s" + "%1$d medlemmer reagerede med %2$s" + + + "Du og %1$d medlem reagerede med %2$s" + "Du og %1$d medlemmer reagerede med %2$s" + + "Du reagerede med %1$s" + "Vis mindre" + "Vis mere" + "Vis oversigt over reaktioner" + "Ny" + + "%1$d rumændring" + "%1$d rumændringer" + + "Gå til nyt rum" + "Dette rum er blevet erstattet og er ikke længere aktivt" + "Se ældre beskeder" + "Dette rum er en fortsættelse af et andet rum" + + "%1$s, %2$s og %3$d anden" + "%1$s, %2$s og %3$d andre" + + + "%1$s skriver" + "%1$s skriver" + + "%1$s og %2$s" + 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 e235088a604..4b2d3cff0c7 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -8,9 +8,9 @@ "Smileys & Menschen" "Reisen & Orte" "Symbole" - "Benutzer blockieren" - "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest" - "Diese Meldung wird an den Administrator deines Homeservers weitergeleitet. Dieser kann keine verschlüsselten Nachrichten lesen." + "Nutzer blockieren" + "Prüfen Sie, ob Sie alle aktuellen und zukünftigen Nachrichten dieses Nutzers ausblenden wollen" + "Diese Nachricht wird dem Administrator ihres Homeservers gemeldet. Dieser kann allerdings keine verschlüsselten Nachrichten lesen." "Grund für die Meldung dieses Inhalts" "Kamera" "Foto aufnehmen" @@ -22,8 +22,8 @@ "Textformatierung" "Der Nachrichtenverlauf ist derzeit nicht verfügbar" "Der Nachrichtenverlauf ist nicht verfügbar. Verifiziere dieses Gerät, um deinen Nachrichtenverlauf zu sehen." - "Möchtest du sie wieder einladen?" - "Du bist allein in diesem Chat" + "Möchten Sie sie erneut einladen?" + "Sie sind in diesem Chat allein" "Alle Mitglieder benachrichtigen" "Alle" "Erneut senden" @@ -35,13 +35,27 @@ "Weniger anzeigen" "Nachricht wurde kopiert" "Du bist nicht berechtigt, in diesem Raum zu schreiben" + + "%1$d Mitglied reagierte mit %2$s" + "%1$d Mitglieder reagierten mit %2$s" + + + "Du und %1$d Mitglied reagierten mit %2$s" + "Du und %1$d Mitglieder reagierten mit %2$s" + + "Du hast reagiert mit %1$s" "Weniger anzeigen" "Mehr anzeigen" + "Zusammenfassung der Reaktionen anzeigen" "Neu" "%1$d Raumänderung" "%1$d Raumänderungen" + "Zum neuen Raum springen" + "Dieser Raum wurde ersetzt und ist nicht mehr aktiv" + "Alte Nachrichten ansehen" + "Dieser Raum ist eine Fortsetzung eines anderen Raums" "%1$s, %2$s und %3$d weitere Person" "%1$s, %2$s und %3$d weitere Person" diff --git a/features/messages/impl/src/main/res/values-el/translations.xml b/features/messages/impl/src/main/res/values-el/translations.xml index 954ab07a649..2b64d3ce98a 100644 --- a/features/messages/impl/src/main/res/values-el/translations.xml +++ b/features/messages/impl/src/main/res/values-el/translations.xml @@ -21,10 +21,10 @@ "Δημοσκόπηση" "Μορφοποίηση Κειμένου" "Το ιστορικό μηνυμάτων δεν είναι διαθέσιμο προς το παρόν." - "Το ιστορικό μηνυμάτων δεν είναι διαθέσιμο σε αυτό το δωμάτιο. Επαλήθευσε αυτήν τη συσκευή για να δεις το ιστορικό μηνυμάτων σου." + "Το ιστορικό μηνυμάτων δεν είναι διαθέσιμο σε αυτή την αίθουσα. Επαληθεύστε αυτή τη συσκευή για να δείτε το ιστορικό των μηνυμάτων σας." "Θα ήθελες να τους προσκαλέσεις και συ;" "Είσαι μόνος σε αυτή τη συνομιλία" - "Ειδοποίησε όλο το δωμάτιο" + "Ειδοποιήστε όλη την αίθουσα" "Όλοι" "Αποστολή ξανά" "Το μήνυμά σου απέτυχε να σταλεί" @@ -34,14 +34,28 @@ "Μη υποστηριζόμενη κλήση. Ρώτα εάν ο καλών μπορεί να χρησιμοποιήσει τη νέα εφαρμογή Element X." "Εμφάνιση λιγότερων" "Το μήνυμα αντιγράφηκε" - "Δεν έχεις άδεια να δημοσιεύσεις σε αυτό το δωμάτιο" + "Δεν έχετε δικαιώματα για να δημοσιεύσετε σε αυτήν την αίθουσα" + + "%1$d μέλος αντέδρασε με %2$s" + "%1$d μέλη αντέδρασαν με %2$s" + + + "Εσύ και %1$d μέλος αντιδράσατε με %2$s" + "Εσύ και τα %1$d μέλη αντιδράσατε με %2$s" + + "Αντέδρασες με %1$s" "Εμφάνιση λιγότερων" "Εμφάνιση περισσότερων" + "Εμφάνιση περίληψης αντιδράσεων" "Νέο" - "%1$d αλλαγή δωματίου" - "%1$d αλλαγές δωματίου" + "%1$d αλλαγή αίθουσας" + "%1$d αλλαγές αίθουσας" + "Μετάβαση σε νέα αίθουσα" + "Αυτή η αίθουσα έχει αντικατασταθεί και δεν είναι πλέον ενεργή" + "Δες παλιά μηνύματα" + "Αυτή η αίθουσα αποτελεί συνέχεια μιας άλλης αίθουσας" "%1$s, %2$s και %3$d ακόμη" "%1$s, %2$s και %3$d ακόμη" diff --git a/features/messages/impl/src/main/res/values-es/translations.xml b/features/messages/impl/src/main/res/values-es/translations.xml index 8404cc6ec6b..54b15f66a5f 100644 --- a/features/messages/impl/src/main/res/values-es/translations.xml +++ b/features/messages/impl/src/main/res/values-es/translations.xml @@ -9,8 +9,8 @@ "Viajes y lugares" "Símbolos" "Bloquear usuario" - "Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario" - "Este mensaje se notificará al administrador de su homeserver. No podrán leer ningún mensaje cifrado." + "Marca esta casilla si quieres ocultar todos los mensajes actuales y futuros de este usuario" + "Se denunciará este mensaje al administrador de tu servidor base. No será capaz de leer ningún mensaje cifrado." "Motivo para denunciar este contenido" "Cámara" "Hacer foto" @@ -19,7 +19,7 @@ "Biblioteca de fotos y vídeos" "Ubicación" "Encuesta" - "Formato de Texto" + "Formato de texto" "El historial de mensajes no está disponible en este momento." "El historial de mensajes no está disponible en esta sala. Verifica este dispositivo para ver tu historial de mensajes." "¿Quieres volver a invitarlos?" @@ -31,6 +31,7 @@ "Añadir emoji" "Este es el principio de %1$s." "Este es el principio de esta conversación." + "Llamada no compatible. Pregunta a la persona que llama si puede utilizar la nueva aplicación Element X." "Mostrar menos" "Mensaje copiado" "No tienes permiso para publicar en esta sala" 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 13a187cedce..6d369a083a6 100644 --- a/features/messages/impl/src/main/res/values-et/translations.xml +++ b/features/messages/impl/src/main/res/values-et/translations.xml @@ -35,13 +35,27 @@ "Näita vähem" "Sõnum on kopeeritud" "Sul pole õigusi siia jututuppa kirjutada" + + "%1$d liige reageeris: %2$s" + "%1$d liige reageerisid: %2$s" + + + "Sina ja veel %1$d liige reageeris: %2$s" + "Sina ja veel %1$d liiget reageerisid: %2$s" + + "Sina reageerisid: %1$s" "Näita vähem" "Näita rohkem" + "Näita reageerimiste kokkuvõtet" "Uus" "%1$d jututoa muudatus" "%1$d jututoa muudatust" + "Hüppa uude jututuppa" + "See jututuba on asendatud uuega ning pole enam aktiivne" + "Vaata vanu sõnumeid" + "See jututuba on varasema jututoa jätk" "%1$s, %2$s ja veel %3$d osaleja" "%1$s, %2$s ja veel %3$d osalejat" diff --git a/features/messages/impl/src/main/res/values-eu/translations.xml b/features/messages/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..34d0a114ee4 --- /dev/null +++ b/features/messages/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,52 @@ + + + "Jarduerak" + "Banderak" + "Jana eta edana" + "Animaliak eta natura" + "Objektuak" + "Irribartxoak eta jendea" + "Bidaiak eta tokiak" + "Ikurrak" + "Blokeatu erabiltzailea" + "Mezua zure zerbitzariko administratzaileari jakinaraziko zaio. Ezingo dute zifratutako mezurik irakurri." + "Edukia salatzeko arrazoia" + "Kamera" + "Egin argazkia" + "Grabatu bideoa" + "Eranskina" + "Argazki- eta bideo-liburutegia" + "Kokapena" + "Inkesta" + "Testuaren formatua" + "Mezuen historia ez dago erabilgarri gaur-gaurkoz." + "Berriro gonbidatu nahi al dituzu?" + "Bakarrik zaude txat honetan" + "Jakinarazi gela osoari" + "Guztiak" + "Bidali berriro" + "Huts egin du mezuaren bidalketak" + "Gehitu emojia" + "Hauxe da %1$s(r)en hasiera" + "Hau da elkarrizketaren hasiera." + "Deia ez da bateragarria. Galdetu deika ari denari Element X aplikazio berria erabil dezakeen." + "Erakutsi gutxiago" + "Mezua kopiatu da" + "Ez duzu gela honetara mezuak bidaltzeko baimenik" + "Erakutsi gutxiago" + "Erakutsi gehiago" + "Berria" + + "Gela aldaketa %1$d" + "%1$d gela aldaketa" + + + "%1$s, %2$s, eta beste %3$d" + "%1$s, %2$s, eta beste %3$d" + + + "%1$s idazten ari da" + "%1$s idazten ari dira" + + "%1$s eta %2$s" + 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 b539950608d..58201d9537e 100644 --- a/features/messages/impl/src/main/res/values-fa/translations.xml +++ b/features/messages/impl/src/main/res/values-fa/translations.xml @@ -9,6 +9,8 @@ "سفر و مکان‌ها" "نمادها" "انسداد کاربر" + "اگر می‌خواهید همه پیام‌های فعلی و آینده را از این کاربر را پنهان کنید، علامت بزنید" + "این پیام به مدیر سرور خانگی شما گزارش خواهد شد. آنها قادر به خواندن پیام های رمزگذاری شده نخواهند بود." "دلیل گزارش این محتوا" "دوربین" "عکس گرفتن" @@ -18,6 +20,7 @@ "مکان" "نظرسنجی" "قالب‌بندی متن" + "تاریخچه پیام درحال حاضر دردسترس نیست." "می‌خواهید دوباره دعوتش کنید؟" "در این گپ تنهایید" "آگاهی به تمام اتاق" @@ -33,5 +36,9 @@ "نمایش کم‌تر" "نمایش بیش‌تر" "جدید" + + "%1$dتغییر اتاق" + "%1$dتغییر اتاق" + "%1$s و %2$s" diff --git a/features/messages/impl/src/main/res/values-fi/translations.xml b/features/messages/impl/src/main/res/values-fi/translations.xml index e689f9e4061..912e610b603 100644 --- a/features/messages/impl/src/main/res/values-fi/translations.xml +++ b/features/messages/impl/src/main/res/values-fi/translations.xml @@ -42,6 +42,10 @@ "%1$d muutos huoneeseen" "%1$d muutosta huoneeseen" + "Siirry uuteen huoneeseen" + "Tämä huone on korvattu, eikä se ole enää aktiivinen" + "Katso vanhoja viestejä" + "Tämä huone on jatkoa toiselle huoneelle" "%1$s, %2$s ja %3$d muu" "%1$s, %2$s ja %3$d muuta" diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index 132d175eb8e..87465ddf365 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -35,13 +35,27 @@ "Afficher moins" "Message copié" "Vous n’êtes pas autorisé à publier dans ce salon" + + "%1$d membre a réagi avec %2$s" + "%1$d membres ont réagi avec %2$s" + + + "Vous et %1$d membre avez réagi avec %2$s" + "Vous et %1$d membres avez réagi avec %2$s" + + "Vous avez réagi avec %1$s" "Afficher moins" "Afficher plus" + "Afficher le résumé des réactions" "Nouveau" "%1$d changement dans le salon" "%1$d changements dans le salon" + "Aller dans le nouveau salon" + "Ce salon a été remplacé et n’est plus actif" + "Voir les anciens messages" + "Ce salon est la continuation du salon précédent" "%1$s, %2$s et %3$d autre" "%1$s, %2$s et %3$d autres" diff --git a/features/messages/impl/src/main/res/values-hu/translations.xml b/features/messages/impl/src/main/res/values-hu/translations.xml index 6fce42474cd..9e7de8616d8 100644 --- a/features/messages/impl/src/main/res/values-hu/translations.xml +++ b/features/messages/impl/src/main/res/values-hu/translations.xml @@ -35,13 +35,27 @@ "Kevesebb megjelenítése" "Üzenet másolva" "Nincs jogosultsága arra, hogy bejegyzést tegyen közzé ebben a szobában" + + "%1$d tag reagált így: %2$s" + "%1$d tag reagált így: %2$s" + + + "Ön és %1$d tag reagáltak így: %2$s" + "Ön és %1$d tag reagáltak így: %2$s" + + "Ezzel reagált: %1$s" "Kevesebb megjelenítése" "Több megjelenítése" + "Reakció-összefoglaló megjelenítése" "Új" "%1$d szobaváltozás" "%1$d szobaváltozás" + "Ugrás az új szobába" + "Ezt a szobát lecserélték, és már nem aktív." + "Régi üzenetek megtekintése" + "Ez a szoba egy másik szoba folytatása" "%1$s, %2$s és %3$d további felhasználó" "%1$s, %2$s és %3$d további felhasználó" 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 c1c526b298b..253e521c883 100644 --- a/features/messages/impl/src/main/res/values-in/translations.xml +++ b/features/messages/impl/src/main/res/values-in/translations.xml @@ -35,12 +35,24 @@ "Tampilkan lebih sedikit" "Pesan disalin" "Anda tidak memiliki izin untuk mengirim di ruangan ini" + + "%1$d anggota bereaksi dengan %2$s" + + + "Anda dan %1$d anggota bereaksi dengan %2$s" + + "Anda bereaksi dengan %1$s" "Tampilkan lebih sedikit" "Tampilkan lebih banyak" + "Tampilkan ringkasan reaksi" "Baru" "%1$d perubahan ruangan" + "Lompat ke ruangan baru" + "Ruangan ini telah diganti dan tidak lagi aktif" + "Lihat pesan lama" + "Ruangan ini adalah lanjutan dari ruangan lain" "%1$s, %2$s, dan %3$d lainnya" diff --git a/features/messages/impl/src/main/res/values-it/translations.xml b/features/messages/impl/src/main/res/values-it/translations.xml index 229a02b714a..69c6c996a9f 100644 --- a/features/messages/impl/src/main/res/values-it/translations.xml +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -35,13 +35,27 @@ "Mostra meno" "Messaggio copiato" "Non sei autorizzato a postare in questa stanza" + + "un membro ha reagito con %2$s" + "%1$d membri hanno reagito con %2$s" + + + "Tu e un altro membro avete reagito con %2$s" + "Tu e %1$d altri membri avete reagito con %2$s" + + "Hai reagito con %1$s" "Mostra meno" "Mostra di più" + "Mostra il riepilogo delle reazioni" "Nuovo" "%1$d modifica alla stanza" "%1$d modifiche alla stanza" + "Vai alla nuova stanza" + "Questa stanza è stata sostituita e non è più attiva" + "Visualizza i vecchi messaggi" + "Questa stanza è la continuazione di un\'altra stanza" "%1$s, %2$s e %3$d altro" "%1$s, %2$s e altri %3$d" diff --git a/features/messages/impl/src/main/res/values-ka/translations.xml b/features/messages/impl/src/main/res/values-ka/translations.xml index 47261affcce..1c5ad9e8eda 100644 --- a/features/messages/impl/src/main/res/values-ka/translations.xml +++ b/features/messages/impl/src/main/res/values-ka/translations.xml @@ -41,4 +41,13 @@ "%1$dოთახის ცვლილება" "%1$dოთახის ცვლილებები" + + "%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-lt/translations.xml b/features/messages/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..0b5c33adeb7 --- /dev/null +++ b/features/messages/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,28 @@ + + + "Veikla" + "Vėliavos" + "Maistas ir Gėrimai" + "Gyvūnai ir Gamta" + "Objektai" + "Šypsenėlės ir Žmonės" + "Kelionės ir Vietovės" + "Simboliai" + "Blokuoti vartotoją" + "Pažymėkite, jei norite paslėpti visas esamas ir būsimas šio vartotojo žinutes" + "Apie šią žinutę bus pranešta Jūsų serverio administracijai. Jie negalės perskaityti jokių užšifruotų žinučių." + "Skundo dėl šio turinio priežastis" + "Kamera" + "Fotografuoti" + "Įrašyti vaizdo įrašą" + "Priedas" + "Nuotraukų ir vaizdo įrašų biblioteka" + "Tai yra %1$s pradžia." + "Tai yra šio pokalbio pradžia." + "Naujų" + + "%1$d kambario pakeitimas" + "%1$d kambario pakeitimai" + "%1$d kambario pakeitimų" + + diff --git a/features/messages/impl/src/main/res/values-nb/translations.xml b/features/messages/impl/src/main/res/values-nb/translations.xml index b4a2914dda8..acb6762a1be 100644 --- a/features/messages/impl/src/main/res/values-nb/translations.xml +++ b/features/messages/impl/src/main/res/values-nb/translations.xml @@ -12,18 +12,57 @@ "Kryss av for om du vil skjule alle nåværende og fremtidige meldinger fra denne brukeren" "Denne meldingen vil bli rapportert til hjemmeserverens administratorer. De vil ikke kunne lese noen krypterte meldinger." "Begrunnelse for å rapportere dette innholdet" + "Kamera" "Ta bilde" + "Ta opp video" + "Vedlegg" + "Foto- og videobibliotek" + "Lokasjon" + "Avstemning" + "Tekstformatering" "Meldingshistorikken er for øyeblikket ikke tilgjengelig." "Meldingshistorikk er ikke tilgjengelig i dette rommet. Bekreft denne enheten for å se meldingshistorikken din." "Vil du invitere dem tilbake?" "Du er alene i denne chatten" + "Varsle hele rommet" + "Alle" "Send igjen" "Meldingen din ble ikke sendt" + "Legg til emoji" "Dette er begynnelsen på %1$s." "Dette er begynnelsen på denne samtalen." + "Anrop som ikke støttes. Spør om den som ringer kan bruke den nye Element X-appen." + "Vis mindre" + "Melding kopiert" + "Du har ikke tillatelse til å legge ut innlegg i dette rommet" + + "%1$d medlem reagerte med %2$s" + "%1$d medlemmer reagerte med %2$s" + + + "Du og %1$d medlem reagerte med%2$s" + "Du og %1$d medlemmer reagerte med%2$s" + + "Du reagerte med %1$s" + "Vis mindre" + "Vis mer" + "Vis sammendrag av reaksjoner" "Ny" "%1$d romendring" "%1$d romendringer" + "Gå til nytt rom" + "Dette rommet har blitt erstattet og er ikke lenger aktivt" + "Se gamle meldinger" + "Dette rommet er en fortsettelse av et annet rom" + + "%1$s, %2$s og %3$d annet" + "%1$s, %2$s og %3$d andre" + + + "%1$s skriver" + "%1$s skriver" + + "%1$s og %2$s" diff --git a/features/messages/impl/src/main/res/values-pt-rBR/translations.xml b/features/messages/impl/src/main/res/values-pt-rBR/translations.xml index 1c668ae3f6e..9688adc49d3 100644 --- a/features/messages/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/messages/impl/src/main/res/values-pt-rBR/translations.xml @@ -21,6 +21,7 @@ "Enquete" "Formatação de texto" "O histórico de mensagens não está disponível no momento." + "O histórico de mensagens não está disponível nesta sala. Verifique este dispositivo para ver seu histórico de mensagens." "Gostaria de convidá-los de volta?" "Você está sozinho neste chat" "Notificar a sala inteira" @@ -30,16 +31,31 @@ "Adicionar emoji" "Este é o início do %1$s." "Este é o início desta conversa." + "Chamada não suportada. Pergunte se o chamador pode usar o novo aplicativo Element X." "Mostrar menos" "Mensagem copiada" "Você não tem permissão para postar nesta sala" + + "%1$d membro reagiu com %2$s" + "%1$d membros reagiram com %2$s" + + + "Você e mais %1$d membro reagiram com %2$s" + "Você e mais %1$d membros reagiram com %2$s" + + "Você reagiu com %1$s" "Mostrar menos" "Mostrar mais" + "Mostrar resumo das reações" "Novo" "%1$d alteração na sala" "%1$d alterações na sala" + "Ir para a sala nova" + "Esta sala foi substituída e não está mais ativa" + "Ver mensagens antigas" + "Esta sala é a continuação de outra sala" "%1$s, %2$s e %3$d outro" "%1$s, %2$s e %3$d outros" 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 e3847e435a7..4759a6aeb95 100644 --- a/features/messages/impl/src/main/res/values-pt/translations.xml +++ b/features/messages/impl/src/main/res/values-pt/translations.xml @@ -9,7 +9,7 @@ "Viagens e Lugares" "Símbolos" "Bloquear utilizador" - "Ativar para esconder todas as atuais e futuras mensagens deste utilizador" + "Ativar para ocultar todas as atuais e futuras mensagens deste utilizador" "Esta mensagem será denunciada ao administrador do teu servidor. Porém, não lhe será possível ler quaisquer mensagens cifradas." "Razão de denúncia" "Câmara" @@ -35,13 +35,27 @@ "Mostrar menos" "Mensagem copiada" "Não tens permissão para publicar nesta sala" + + "%1$d membro reagiu com %2$s" + "%1$d membros reagiram com %2$s" + + + "Tu e %1$d membro reagiram com %2$s" + "Tu e %1$d membros reagiram com %2$s" + + "Reagiste com %1$s" "Mostrar menos" "Mostrar mais" + "Mostrar sumário de reações" "Novas" "%1$d alteração à sala" "%1$d alterações à sala" + "Ir para a nova sala" + "Esta sala foi substituída e já não se encontra ativa" + "Ver mensagens antigas" + "Esta sala é a continuação de uma outra" "%1$s, %2$s e %3$d outro" "%1$s, %2$s e %3$d outros" diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index 084829ba492..b8c3cc237fa 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -43,6 +43,10 @@ "%1$d изменения в комнате" "%1$d изменений в комнате" + "Перейти в новую комнату" + "Эта комната была заменена и больше не активна" + "Посмотреть старые сообщения" + "Эта комната является продолжением другой комнаты" "%1$s, %2$s и %3$d" "%1$s, %2$s и другие %3$d" diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index 215e7f4cc46..23c664c5d42 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -35,14 +35,30 @@ "Zobraziť menej" "Správa skopírovaná" "Nemáte povolenie uverejňovať príspevky v tejto miestnosti" + + "%1$d člen reagoval s %2$s" + "%1$d členovia reagovali s %2$s" + "%1$d členov reagovalo s %2$s" + + + "Vy a %1$d člen reagoval s %2$s" + "Vy a %1$d členovia reagovali s %2$s" + "Vy a %1$d členov reagovalo s %2$s" + + "Reagovali ste s %1$s" "Zobraziť menej" "Zobraziť viac" + "Zobraziť súhrn reakcií" "Nové" "%1$d zmena miestnosti" "%1$d zmeny miestnosti" "%1$d zmien miestnosti" + "Prejsť do novej miestnosti" + "Táto miestnosť bola nahradená a už nie je aktívna" + "Zobraziť staré správy" + "Táto miestnosť je pokračovaním inej miestnosti" "%1$s, %2$s a %3$d ďalší" "%1$s, %2$s a %3$d ďalší" diff --git a/features/messages/impl/src/main/res/values-sv/translations.xml b/features/messages/impl/src/main/res/values-sv/translations.xml index c2ca9678e33..bb803b396f4 100644 --- a/features/messages/impl/src/main/res/values-sv/translations.xml +++ b/features/messages/impl/src/main/res/values-sv/translations.xml @@ -31,6 +31,7 @@ "Lägg till emoji" "Det här är början på %1$s." "Detta är början på det här samtalet." + "Samtalet stöds inte. Fråga om den som ringer kan använda den nya Element X-appen." "Visa mindre" "Meddelande kopierat" "Du är inte behörig att göra inlägg i det här rummet" @@ -41,6 +42,10 @@ "%1$d rumsändring" "%1$d rumsändringar" + "Hoppa till nytt rum" + "Det här rummet har ersatts och är inte längre aktivt" + "Se gamla meddelanden" + "Det här rummet är en fortsättning på ett annat rum" "%1$s, %2$s och %3$d annan" "%1$s, %2$s och %3$d andra" diff --git a/features/messages/impl/src/main/res/values-tr/translations.xml b/features/messages/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..de962f72ee7 --- /dev/null +++ b/features/messages/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,54 @@ + + + "Aktiviteler" + "Bayraklar" + "Yiyecek ve İçecek" + "Hayvanlar ve Doğa" + "Nesneler" + "İfadeler ve İnsanlar" + "Seyahat ve Yerler" + "Simgeler" + "Kullanıcıyı engelle" + "Bu kullanıcıdan gelen mevcut ve gelecekteki tüm mesajları gizlemek isteyip istemediğinizi işaretleyin" + "Bu mesaj ana sunucunuzun yöneticisine bildirilecektir. Şifrelenmiş mesajları okuyamayacaklardır." + "Bu içeriğin bildirilme nedeni" + "Kamera" + "Fotoğraf çek" + "Video kaydet" + "Ek" + "Fotoğraf ve Video Kütüphanesi" + "Konum" + "Anket" + "Metin Biçimlendirme" + "Mesaj geçmişi şu anda kullanılamıyor." + "Mesaj geçmişi bu odada kullanılamıyor. Mesaj geçmişinizi görmek için bu cihazı doğrulayın." + "Onları geri davet etmek ister misin?" + "Bu sohbette yalnızsın" + "Tüm odaya bildir" + "Herkes" + "Tekrar gönder" + "Mesajınız gönderilemedi" + "Emoji ekle" + "Bu, %1$s odasının başlangıcıdır." + "Bu konuşmanın başlangıcıdır." + "Desteklenmeyen çağrı. Arayanın yeni Element X uygulamasını kullanıp kullanamayacağını sorun." + "Daha az göster" + "Mesaj kopyalandı" + "Bu odada gönderi yapma izniniz yok" + "Daha az göster" + "Daha fazla göster" + "Yeni" + + "%1$d oda değişikliği" + "%1$d oda değişikliği" + + + "%1$s, %2$s ve %3$d diğer" + "%1$s, %2$s ve %3$d diğer" + + + "%1$s yazıyor" + "%1$s yazıyor" + + "%1$s ve %2$s" + 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 bb11dec6d73..b8f9ec0b681 100644 --- a/features/messages/impl/src/main/res/values-uk/translations.xml +++ b/features/messages/impl/src/main/res/values-uk/translations.xml @@ -35,14 +35,30 @@ "Показувати менше" "Повідомлення скопійовано" "У вас немає дозволу на публікацію в цій кімнаті" + + "%1$d учасник відреагував з %2$s" + "%1$d учасники відреагували з %2$s" + "%1$d учасників відреагували з %2$s" + + + "Ви та ще %1$d учасник відреагували з %2$s" + "Ви та ще %1$d учасники відреагували з %2$s" + "Ви та ще %1$d учасників відреагували з %2$s" + + "Ви відреагували за допомогою %1$s" "Показувати менше" "Показати більше" + "Показати підсумок реакцій" "Нове" "%1$d зміна в кімнаті" "%1$d зміни в кімнаті" "%1$d змін у кімнаті" + "Перейти до нової кімнати" + "Цю кімнату замінили, і вона більше не активна." + "Перегляд давніших повідомлень" + "Ця кімната є продовженням іншої кімнати" "%1$s%2$s та %3$d інший" "%1$s%2$s та %3$d інші" diff --git a/features/messages/impl/src/main/res/values-ur/translations.xml b/features/messages/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..2b09c2bf0c7 --- /dev/null +++ b/features/messages/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,53 @@ + + + "سرگرمیاں" + "جھنڈے" + "طعم و مشروب" + "حیوانات و فطرت" + "اشیاء" + "مسکراہٹیں و لوگ" + "سفر و مقامات" + "علامتیں" + "صارف کو مسدود کریں" + "پڑتال کریں کہ کیا آپ اس صارف سے تمام موجودہ اور مستقبلی پیغامات چھپانا چاہتے ہیں۔" + "اس پیغام کی اطلاع آپکے منزلی خادم کے منتظم کو دی جائیگی۔ وہ کوئی مرموزکردہ پیغامات نہیں پڑھ سکیں گے۔" + "اس مواد کی اطلاع دینے کی وجہ" + "تصویرگر" + "تصویر لیں" + "ویڈیو ثبت کریں" + "منسلکہ" + "تصویر اور ویڈیو مکتب" + "مقام" + "رائے شماری" + "متن کی تنسیق" + "پیغام کی سرگزشت فی الحال دستیاب نہیں ہے" + "اس کمرے میں پیغام کی سرگزشت دستیاب نہیں ہے۔ اپنے پیغام کی سرگزشت دیکھنے کے لیے اس آلے کی توثیق کریں۔" + "کیا آپ انہیں واپس مدعو کرنا چاہیں گے؟" + "آپ اس گفتگو میں تنہا ہیں" + "پورے کمرے کو مطلع کریں" + "ہر کوئی" + "دوبارہ بھیجیں" + "آپ کا پیغام بھیجنے میں ناکام" + "رمزِ تعبیری شامل کریں" + "یہ %1$s کا آغاز ہے" + "یہ اس گفتگو کا آغاز ہے" + "کم دکھائیں" + "پیغام نقل کردہ" + "آپ کو اس کمرے میں پوسٹ کرنے کی اجازت نہیں ہے" + "کم دکھائیں" + "مزید دکھائیں" + "نیا" + + "%1$d کمرے کی تبدیلی" + "%1$d کمرے کی تبدیلیاں" + + + "%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/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 73d266f799e..2d208bef493 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -28,20 +28,34 @@ "Everyone" "Send again" "Your message failed to send" - "Add emoji" + "Add a reaction" "This is the beginning of %1$s." "This is the beginning of this conversation." "Unsupported call. Ask if the caller can use the new Element X app." "Show less" "Message copied" "You do not have permission to post to this room" + + "%1$d member reacted with %2$s" + "%1$d members reacted with %2$s" + + + "You and %1$d member reacted with %2$s" + "You and %1$d members reacted with %2$s" + + "You reacted with %1$s" "Show less" "Show more" + "Show reactions summary" "New" "%1$d room change" "%1$d room changes" + "Jump to new room" + "This room has been replaced and is no longer active" + "See old messages" + "This room is a continuation of another room" "%1$s, %2$s and %3$d other" "%1$s, %2$s and %3$d others" 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 92fc0d81712..32638d7b8dc 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 @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl import io.element.android.features.messages.impl.attachments.Attachment 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.timeline.item.TimelineItemDebugInfo import io.element.android.tests.testutils.lambda.lambdaError @@ -20,6 +21,7 @@ class FakeMessagesNavigator( private val onReportContentClickLambda: (eventId: EventId, senderId: UserId) -> Unit = { _, _ -> lambdaError() }, private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() }, private val onPreviewAttachmentLambda: (attachments: ImmutableList) -> Unit = { _ -> lambdaError() }, + private val onNavigateToRoomLambda: (roomId: RoomId) -> Unit = { _ -> lambdaError() } ) : MessagesNavigator { override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { onShowEventDebugInfoClickLambda(eventId, debugInfo) @@ -40,4 +42,8 @@ class FakeMessagesNavigator( override fun onPreviewAttachment(attachments: ImmutableList) { onPreviewAttachmentLambda(attachments) } + + override fun onNavigateToRoom(roomId: RoomId) { + onNavigateToRoomLambda(roomId) + } } 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 20b57fa343e..f24175c33ef 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 @@ -5,11 +5,11 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.messages.impl -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test +import androidx.lifecycle.Lifecycle import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.PinUnpinAction import io.element.android.features.messages.impl.actionlist.ActionListEvents @@ -18,6 +18,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState import io.element.android.features.messages.impl.fixtures.aMessageEvent +import io.element.android.features.messages.impl.link.aLinkState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState @@ -36,6 +37,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider import io.element.android.features.roomcall.api.aStandByCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -48,27 +50,32 @@ 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.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.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.permalink.PermalinkParser -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo 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_AVATAR_URL 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_CAPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 -import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.sync.FakeSyncService @@ -80,6 +87,7 @@ import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.FakeLifecycleOwner import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout @@ -87,12 +95,13 @@ import io.element.android.tests.testutils.lambda.assert 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.test import io.element.android.tests.testutils.testCoroutineDispatchers +import io.element.android.tests.testutils.testWithLifecycleOwner import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -107,14 +116,12 @@ class MessagesPresenterTest { @Test fun `present - initial state`() = runTest { val presenter = createMessagesPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = consumeItemsUntilTimeout().last() assertThat(initialState.roomId).isEqualTo(A_ROOM_ID) - assertThat(initialState.roomName).isEqualTo(AsyncData.Success("")) + assertThat(initialState.roomName).isEqualTo("") assertThat(initialState.roomAvatar) - .isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom))) + .isEqualTo(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)) assertThat(initialState.userEventPermissions.canSendMessage).isTrue() assertThat(initialState.userEventPermissions.canRedactOwn).isTrue() assertThat(initialState.hasNetworkConnection).isTrue() @@ -127,21 +134,21 @@ class MessagesPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - check that the room's unread flag is removed`() = runTest { - val room = FakeMatrixRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + markAsReadResult = { lambdaError() } + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - assertThat(room.markAsReadCalls).isEmpty() - val presenter = createMessagesPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { runCurrent() - assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false)) + assertThat(room.baseRoom.setUnreadFlagCalls).isEqualTo(listOf(false)) cancelAndIgnoreRemainingEvents() } } @@ -156,28 +163,30 @@ class MessagesPresenterTest { val timeline = FakeTimeline().apply { this.toggleReactionLambda = toggleReactionSuccess } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room, coroutineDispatchers = coroutineDispatchers) + presenter.testWithLifecycleOwner { skipItems(1) val initialState = awaitItem() initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId())) + advanceUntilIdle() assert(toggleReactionSuccess) .isCalledOnce() .with(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId())) // No crashes when sending a reaction failed - timeline.apply { toggleReactionLambda = toggleReactionFailure } + timeline.toggleReactionLambda = toggleReactionFailure initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId())) + advanceUntilIdle() assert(toggleReactionFailure) .isCalledOnce() .with(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId())) @@ -192,22 +201,23 @@ class MessagesPresenterTest { val timeline = FakeTimeline().apply { this.toggleReactionLambda = toggleReactionSuccess } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room, coroutineDispatchers = coroutineDispatchers) + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId())) initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId())) + advanceUntilIdle() assert(toggleReactionSuccess) .isCalledExactly(2) .withSequence( @@ -225,9 +235,7 @@ class MessagesPresenterTest { onForwardEventClickLambda = onForwardEventClickLambda, ) val presenter = createMessagesPresenter(navigator = navigator) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -240,9 +248,7 @@ class MessagesPresenterTest { val clipboardHelper = FakeClipboardHelper() val event = aMessageEvent() val presenter = createMessagesPresenter(clipboardHelper = clipboardHelper) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyText, event)) skipItems(2) @@ -254,22 +260,22 @@ class MessagesPresenterTest { fun `present - handle action copy link`() = runTest { val clipboardHelper = FakeClipboardHelper() val event = aMessageEvent() - val matrixRoom = FakeMatrixRoom( - eventPermalinkResult = { Result.success("a link") }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + eventPermalinkResult = { Result.success("a link") }, + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) val presenter = createMessagesPresenter( clipboardHelper = clipboardHelper, - matrixRoom = matrixRoom, + joinedRoom = room, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event)) skipItems(2) @@ -283,9 +289,7 @@ class MessagesPresenterTest { val presenter = createMessagesPresenter( messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) awaitItem() @@ -303,9 +307,7 @@ class MessagesPresenterTest { @Test fun `present - handle action reply to an event with no id does nothing`() = runTest { val presenter = createMessagesPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) skipItems(1) @@ -318,9 +320,7 @@ class MessagesPresenterTest { val presenter = createMessagesPresenter( messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() val mediaMessage = aMessageEvent( content = TimelineItemImageContent( @@ -360,9 +360,7 @@ class MessagesPresenterTest { val presenter = createMessagesPresenter( messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() val mediaMessage = aMessageEvent( content = TimelineItemVideoContent( @@ -403,9 +401,7 @@ class MessagesPresenterTest { val presenter = createMessagesPresenter( messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() val mediaMessage = aMessageEvent( content = TimelineItemFileContent( @@ -439,9 +435,7 @@ class MessagesPresenterTest { val presenter = createMessagesPresenter( messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) awaitItem() @@ -463,9 +457,7 @@ class MessagesPresenterTest { onEditPollClickLambda = onEditPollClickLambda ) val presenter = createMessagesPresenter(navigator = navigator) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditPoll, aMessageEvent(content = aTimelineItemPollContent()))) awaitItem() @@ -477,9 +469,7 @@ class MessagesPresenterTest { fun `present - handle action end poll`() = runTest { val timelineEventSink = EventsRecorder() val presenter = createMessagesPresenter(timelineEventSink = timelineEventSink) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent()))) delay(1) @@ -493,25 +483,25 @@ class MessagesPresenterTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val liveTimeline = FakeTimeline() - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), liveTimeline = liveTimeline, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) } liveTimeline.redactEventLambda = redactEventLambda val presenter = createMessagesPresenter( - matrixRoom = matrixRoom, + joinedRoom = joinedRoom, coroutineDispatchers = coroutineDispatchers, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() val messageEvent = aMessageEvent() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent)) @@ -529,9 +519,7 @@ class MessagesPresenterTest { onReportContentClickLambda = onReportContentClickLambda ) val presenter = createMessagesPresenter(navigator = navigator) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -542,9 +530,7 @@ class MessagesPresenterTest { @Test fun `present - handle dismiss action`() = runTest { val presenter = createMessagesPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.Dismiss) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -558,9 +544,7 @@ class MessagesPresenterTest { onShowEventDebugInfoClickLambda = onShowEventDebugInfoClickLambda ) val presenter = createMessagesPresenter(navigator = navigator) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -570,27 +554,27 @@ class MessagesPresenterTest { @Test fun `present - shows prompt to reinvite users in DM`() = runTest { - val room = FakeMatrixRoom( - sessionId = A_SESSION_ID, - isDirect = true, - activeMemberCount = 1L, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 1, activeMembersCount = 1)) + }, typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { val initialState = awaitItem() // Initially the composer doesn't have focus, so we don't show the alert assertThat(initialState.showReinvitePrompt).isFalse() // When the input field is focused we show the alert (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true - skipItems(1) + // Skip intermediate states + skipItems(2) val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isTrue() // If it's dismissed then we stop showing the alert @@ -603,24 +587,25 @@ class MessagesPresenterTest { @Test fun `present - doesn't show reinvite prompt in non-direct room`() = runTest { - val room = FakeMatrixRoom( - sessionId = A_SESSION_ID, - isDirect = false, - activeMemberCount = 1L, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(isDirect = false, joinedMembersCount = 1, activeMembersCount = 1)) + }, typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true + // Skip intermediate events + skipItems(1) val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isFalse() } @@ -628,24 +613,25 @@ class MessagesPresenterTest { @Test fun `present - doesn't show reinvite prompt if other party is present`() = runTest { - val room = FakeMatrixRoom( - sessionId = A_SESSION_ID, - isDirect = true, - activeMemberCount = 2L, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 2, activeMembersCount = 2)) + }, typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true + // Skip intermediate events + skipItems(1) val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isFalse() } @@ -654,28 +640,27 @@ class MessagesPresenterTest { @Test fun `present - handle reinviting other user when memberlist is ready`() = runTest { val inviteUserResult = lambdaRecorder { _: UserId -> Result.success(Unit) } - val room = FakeMatrixRoom( - sessionId = A_SESSION_ID, - inviteUserResult = inviteUserResult, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, + inviteUserResult = inviteUserResult, ) room.givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( persistentListOf( aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN), aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE), ) ) ) - val presenter = createMessagesPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) skipItems(1) @@ -690,18 +675,19 @@ class MessagesPresenterTest { @Test fun `present - handle reinviting other user when memberlist is error`() = runTest { val inviteUserResult = lambdaRecorder { _: UserId -> Result.success(Unit) } - val room = FakeMatrixRoom( - sessionId = A_SESSION_ID, - inviteUserResult = inviteUserResult, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, + inviteUserResult = inviteUserResult, ) room.givenRoomMembersState( - MatrixRoomMembersState.Error( + RoomMembersState.Error( failure = Throwable(), prevRoomMembers = persistentListOf( aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN), @@ -709,10 +695,8 @@ class MessagesPresenterTest { ) ) ) - val presenter = createMessagesPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) skipItems(1) @@ -728,20 +712,19 @@ class MessagesPresenterTest { @Test fun `present - handle reinviting other user when memberlist is not ready`() = runTest { - val room = FakeMatrixRoom( - sessionId = A_SESSION_ID, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - room.givenRoomMembersState(MatrixRoomMembersState.Unknown) - val presenter = createMessagesPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + room.givenRoomMembersState(RoomMembersState.Unknown) + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) skipItems(1) @@ -754,34 +737,35 @@ class MessagesPresenterTest { @Test fun `present - handle reinviting other user when inviting fails`() = runTest { - val room = FakeMatrixRoom( - sessionId = A_SESSION_ID, - inviteUserResult = { Result.failure(Throwable("Oops!")) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, + inviteUserResult = { Result.failure(RuntimeException("Oops!")) }, ) room.givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( persistentListOf( aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN), aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE), ) ) ) - val presenter = createMessagesPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) + val loadingState = consumeItemsUntilPredicate { state -> state.inviteProgress.isLoading() }.last() assertThat(loadingState.inviteProgress.isLoading()).isTrue() + val failureState = consumeItemsUntilPredicate { state -> state.inviteProgress.isFailure() }.last() @@ -791,24 +775,24 @@ class MessagesPresenterTest { @Test fun `present - permission to post`() = runTest { - val matrixRoom = FakeMatrixRoom( - canUserSendMessageResult = { _, messageEventType -> - when (messageEventType) { - MessageEventType.ROOM_MESSAGE -> Result.success(true) - MessageEventType.REACTION -> Result.success(true) - else -> lambdaError() - } - }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + canUserSendMessageResult = { _, messageEventType -> + when (messageEventType) { + MessageEventType.ROOM_MESSAGE -> Result.success(true) + MessageEventType.REACTION -> Result.success(true) + else -> lambdaError() + } + }, + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = matrixRoom) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { skipItems(1) val state = awaitItem() assertThat(state.userEventPermissions.canSendMessage).isTrue() @@ -817,24 +801,24 @@ class MessagesPresenterTest { @Test fun `present - no permission to post`() = runTest { - val matrixRoom = FakeMatrixRoom( - canUserSendMessageResult = { _, messageEventType -> - when (messageEventType) { - MessageEventType.ROOM_MESSAGE -> Result.success(false) - MessageEventType.REACTION -> Result.success(false) - else -> lambdaError() - } - }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + canUserSendMessageResult = { _, messageEventType -> + when (messageEventType) { + MessageEventType.ROOM_MESSAGE -> Result.success(false) + MessageEventType.REACTION -> Result.success(false) + else -> lambdaError() + } + }, + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = matrixRoom) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { // Default value assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue() assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse() @@ -843,18 +827,18 @@ class MessagesPresenterTest { @Test fun `present - permission to redact own`() = runTest { - val matrixRoom = FakeMatrixRoom( - canRedactOwnResult = { Result.success(true) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOtherResult = { Result.success(false) }, - canUserJoinCallResult = { Result.success(true) }, + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canRedactOwnResult = { Result.success(true) }, + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOtherResult = { Result.success(false) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = matrixRoom) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = joinedRoom) + presenter.testWithLifecycleOwner { val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOwn }.last() assertThat(initialState.userEventPermissions.canRedactOwn).isTrue() assertThat(initialState.userEventPermissions.canRedactOther).isFalse() @@ -864,18 +848,18 @@ class MessagesPresenterTest { @Test fun `present - permission to redact other`() = runTest { - val matrixRoom = FakeMatrixRoom( - canRedactOtherResult = { Result.success(true) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(false) }, - canUserJoinCallResult = { Result.success(true) }, + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canRedactOtherResult = { Result.success(true) }, + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(false) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = matrixRoom) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = joinedRoom) + presenter.testWithLifecycleOwner { val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOther }.last() assertThat(initialState.userEventPermissions.canRedactOwn).isFalse() assertThat(initialState.userEventPermissions.canRedactOther).isTrue() @@ -889,9 +873,7 @@ class MessagesPresenterTest { val presenter = createMessagesPresenter( messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() val poll = aMessageEvent( content = aTimelineItemPollContent() @@ -912,22 +894,22 @@ class MessagesPresenterTest { @Test fun `present - handle action pin`() = runTest { val successPinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) } - val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure(A_THROWABLE) } + val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure(AN_EXCEPTION) } val analyticsService = FakeAnalyticsService() val timeline = FakeTimeline() - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room, analyticsService = analyticsService) + presenter.testWithLifecycleOwner { val messageEvent = aMessageEvent( content = aTimelineItemTextContent() ) @@ -952,22 +934,22 @@ class MessagesPresenterTest { @Test fun `present - handle action unpin`() = runTest { val successUnpinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) } - val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure(A_THROWABLE) } + val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure(AN_EXCEPTION) } val timeline = FakeTimeline() val analyticsService = FakeAnalyticsService() - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) - val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val presenter = createMessagesPresenter(joinedRoom = room, analyticsService = analyticsService) + presenter.testWithLifecycleOwner { val messageEvent = aMessageEvent( content = aTimelineItemTextContent() ) @@ -1000,7 +982,7 @@ class MessagesPresenterTest { val presenter = createMessagesPresenter( messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, ) - presenter.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditCaption, messageEvent)) awaitItem() @@ -1030,7 +1012,7 @@ class MessagesPresenterTest { initialState = mapOf(FeatureFlags.MediaCaptionWarning.key to false) ) ) - presenter.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditCaption, messageEvent)) awaitItem() @@ -1057,7 +1039,7 @@ class MessagesPresenterTest { caption = null, ) ) - presenter.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.AddCaption, messageEvent)) awaitItem() @@ -1087,7 +1069,7 @@ class MessagesPresenterTest { caption = null, ) ) - presenter.test { + presenter.testWithLifecycleOwner { val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.AddCaption, messageEvent)) awaitItem() @@ -1114,19 +1096,21 @@ class MessagesPresenterTest { val timeline = FakeTimeline().apply { this.editCaptionLambda = editCaptionLambda } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ) val presenter = createMessagesPresenter( - matrixRoom = room, + joinedRoom = room, ) - presenter.test { + presenter.testWithLifecycleOwner { skipItems(1) val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.RemoveCaption, messageEvent)) @@ -1140,7 +1124,7 @@ class MessagesPresenterTest { content = aTimelineItemTextContent() ) val presenter = createMessagesPresenter() - presenter.test { + presenter.testWithLifecycleOwner { skipItems(1) val initialState = awaitItem() initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewInTimeline, messageEvent)) @@ -1148,18 +1132,104 @@ class MessagesPresenterTest { } } + @Test + fun `present - room with successor room includes successor info in state`() = runTest { + val successorRoomId = RoomId("!successor:server.org") + val successorReason = "This room has been moved to a new location" + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + initialRoomInfo = aRoomInfo( + successorRoom = SuccessorRoom( + roomId = successorRoomId, + reason = successorReason + ) + ) + ), + typingNoticeResult = { Result.success(Unit) }, + ) + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.successorRoom).isNotNull() + assertThat(initialState.successorRoom?.roomId).isEqualTo(successorRoomId) + assertThat(initialState.successorRoom?.reason).isEqualTo(successorReason) + } + } + + @Test + fun `present - room without successor room has null successor info in state`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + initialRoomInfo = aRoomInfo(successorRoom = null) + ), + typingNoticeResult = { Result.success(Unit) }, + ) + val presenter = createMessagesPresenter(joinedRoom = room) + presenter.testWithLifecycleOwner { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.successorRoom).isNull() + } + } + + @Test + fun `present - when room is encrypted and a DM, the DM user's identity state is fetched onResume`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + sessionId = A_SESSION_ID, + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true) + ).apply { + givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2)))) + }, + typingNoticeResult = { Result.success(Unit) }, + ) + val encryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(IdentityState.Verified) }) + + val presenter = createMessagesPresenter(joinedRoom = room, encryptionService = encryptionService) + val lifecycleOwner = FakeLifecycleOwner() + presenter.testWithLifecycleOwner(lifecycleOwner) { + val initialState = awaitItem() + assertThat(initialState.dmUserVerificationState).isNull() + + skipItems(1) + ensureAllEventsConsumed() + + lifecycleOwner.givenState(Lifecycle.State.RESUMED) + assertThat(awaitItem().dmUserVerificationState).isEqualTo(IdentityState.Verified) + } + } + private fun TestScope.createMessagesPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), - matrixRoom: MatrixRoom = FakeMatrixRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, + joinedRoom: FakeJoinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(id = roomId, name = "")) + }, + liveTimeline = FakeTimeline(), typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, - ).apply { - givenRoomInfo(aRoomInfo(id = roomId, name = "")) - }, + ), navigator: FakeMessagesNavigator = FakeMessagesNavigator(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), @@ -1172,10 +1242,14 @@ class MessagesPresenterTest { textEditorState = aTextEditorStateMarkdown(initialText = "", initialFocus = false) ) }, + roomMemberModerationPresenter: Presenter = Presenter { + aRoomMemberModerationState() + }, + encryptionService: FakeEncryptionService = FakeEncryptionService(), actionListEventSink: (ActionListEvents) -> Unit = {}, ): MessagesPresenter { return MessagesPresenter( - room = matrixRoom, + room = joinedRoom, composerPresenter = messageComposerPresenter, voiceMessageComposerPresenter = { aVoiceMessageComposerState() }, timelinePresenter = { aTimelineState(eventSink = timelineEventSink) }, @@ -1185,8 +1259,10 @@ class MessagesPresenterTest { reactionSummaryPresenter = { aReactionSummaryState() }, readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() }, identityChangeStatePresenter = { anIdentityChangeState() }, + linkPresenter = { aLinkState() }, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, roomCallStatePresenter = { aStandByCallState() }, + roomMemberModerationPresenter = roomMemberModerationPresenter, syncService = FakeSyncService(), snackbarDispatcher = SnackbarDispatcher(), navigator = navigator, @@ -1195,8 +1271,9 @@ class MessagesPresenterTest { buildMeta = aBuildMeta(), dispatchers = coroutineDispatchers, htmlConverterProvider = FakeHtmlConverterProvider(), - timelineController = TimelineController(matrixRoom), + timelineController = TimelineController(joinedRoom), permalinkParser = permalinkParser, + encryptionService = encryptionService, analyticsService = analyticsService, ) } 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 63491b0103e..51f5640ece6 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 @@ -41,6 +41,7 @@ import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMess import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineState @@ -50,19 +51,27 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +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.tombstone.SuccessorRoom +import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl +import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureCalledOnceWithParam +import io.element.android.tests.testutils.EnsureCalledOnceWithTwoParamsAndResult import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam -import io.element.android.tests.testutils.EnsureNeverCalledWithParamAndResult +import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams +import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParamsAndResult 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.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.setSafeContent import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test @@ -101,7 +110,7 @@ class MessagesViewTest { state = state, onRoomDetailsClick = callback, ) - rule.onNodeWithText(state.roomName.dataOrNull().orEmpty()).performClick() + rule.onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick() } } @@ -125,11 +134,15 @@ class MessagesViewTest { fun `clicking on an Event invoke expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( + timelineState = aTimelineState( + timelineItems = aTimelineItemList(aTimelineItemTextContent()), + ), eventSink = eventsRecorder ) val timelineItem = state.timelineState.timelineItems.first() - val callback = EnsureCalledOnceWithParam( - expectedParam = timelineItem, + val callback = EnsureCalledOnceWithTwoParamsAndResult( + expectedParam1 = true, + expectedParam2 = timelineItem, result = true, ) rule.setMessagesView( @@ -180,6 +193,9 @@ class MessagesViewTest { canSendReaction = userHasPermissionToSendReaction, canPinUnpin = userCanPinEvent, ), + timelineState = aTimelineState( + timelineItems = aTimelineItemList(aTimelineItemTextContent()), + ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event rule.setMessagesView( @@ -196,6 +212,7 @@ class MessagesViewTest { } @Test + @Config(qualifiers = "h1024dp") fun `clicking on a read receipt list emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( @@ -219,7 +236,7 @@ class MessagesViewTest { rule.setMessagesView( state = state, ) - rule.onNodeWithTag(TestTags.messageReadReceipts.value).performClick() + rule.onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle(ReadReceiptBottomSheetEvents.EventSelected(timelineItem)) } @@ -299,21 +316,42 @@ class MessagesViewTest { @Test @Config(qualifiers = "h1024dp") - fun `clicking on the sender of an Event invoke expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) + fun `clicking on the avatar of the sender of an Event emits the expected event`() { + val eventsRecorder = EventsRecorder() val state = aMessagesState( eventSink = eventsRecorder ) - val timelineItem = state.timelineState.timelineItems.first() - ensureCalledOnceWithParam( - param = (timelineItem as TimelineItem.Event).senderId - ) { callback -> - rule.setMessagesView( - state = state, - onUserDataClick = callback, + val timelineEvent = state.timelineState.timelineItems.filterIsInstance().first() + rule.setMessagesView(state = state) + rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + eventsRecorder.assertSingle( + MessagesEvents.OnUserClicked( + MatrixUser( + userId = timelineEvent.senderId, + displayName = timelineEvent.senderProfile.getDisplayName(), + avatarUrl = timelineEvent.senderProfile.getAvatarUrl() + ) ) - rule.onNodeWithTag(TestTags.timelineItemSenderInfo.value).performClick() - } + ) + } + + @Test + @Config(qualifiers = "h1024dp") + 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() + rule.setMessagesView(state = state) + rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + eventsRecorder.assertSingle( + MessagesEvents.OnUserClicked( + MatrixUser( + userId = timelineEvent.senderId, + displayName = timelineEvent.senderProfile.getDisplayName(), + avatarUrl = timelineEvent.senderProfile.getAvatarUrl() + ) + ) + ) } @Test @@ -347,13 +385,19 @@ class MessagesViewTest { fun `clicking on a reaction emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( - eventSink = eventsRecorder + timelineState = aTimelineState( + timelineItems = aTimelineItemList(aTimelineItemTextContent()), + ), + eventSink = eventsRecorder, ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event rule.setMessagesView( state = state, ) - rule.onAllNodesWithText("👍️").onFirst().performClick() + rule.onAllNodesWithText( + text = "👍️", + useUnmergedTree = true, + ).onFirst().performClick() eventsRecorder.assertSingle(MessagesEvents.ToggleReaction("👍️", timelineItem.eventOrTransactionId)) } @@ -361,6 +405,9 @@ class MessagesViewTest { fun `long clicking on a reaction emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( + timelineState = aTimelineState( + timelineItems = aTimelineItemList(aTimelineItemTextContent()), + ), reactionSummaryState = aReactionSummaryState( target = null, eventSink = eventsRecorder, @@ -370,7 +417,10 @@ class MessagesViewTest { rule.setMessagesView( state = state, ) - rule.onAllNodesWithText("👍️").onFirst().performTouchInput { longClick() } + rule.onAllNodesWithText( + text = "👍️", + useUnmergedTree = true, + ).onFirst().performTouchInput { longClick() } eventsRecorder.assertSingle(ReactionSummaryEvents.ShowReactionSummary(timelineItem.eventId!!, timelineItem.reactionsState.reactions, "👍️")) } @@ -378,6 +428,9 @@ class MessagesViewTest { fun `clicking on more reaction emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( + timelineState = aTimelineState( + timelineItems = aTimelineItemList(aTimelineItemTextContent()), + ), customReactionState = aCustomReactionState( eventSink = eventsRecorder, ), @@ -394,7 +447,11 @@ class MessagesViewTest { @Test fun `clicking on more reaction from action list emits the expected Event`() { val eventsRecorder = EventsRecorder() - val state = aMessagesState() + val state = aMessagesState( + timelineState = aTimelineState( + timelineItems = aTimelineItemList(aTimelineItemTextContent()), + ), + ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event val stateWithActionListState = state.copy( actionListState = anActionListState( @@ -506,25 +563,53 @@ class MessagesViewTest { rule.onNodeWithText("This is a pinned message").performClick() eventsRecorder.assertSingle(TimelineEvents.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)) } + + @Test + fun `clicking on successor room button emits expected event`() { + val eventsRecorder = EventsRecorder() + val successorRoomId = RoomId("!successor:server.org") + val state = aMessagesState( + successorRoom = SuccessorRoom( + roomId = successorRoomId, + reason = "This room has been upgraded" + ), + timelineState = aTimelineState(eventSink = eventsRecorder) + ) + rule.setMessagesView(state = state) + val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action) + // The bottomsheet subcompose seems to make the node to appear twice + rule.onAllNodesWithText(text).onFirst().performClick() + eventsRecorder.assertSingle(TimelineEvents.NavigateToRoom(successorRoomId)) + } + + @Test + fun `no banner shown when there is no successor room`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val state = aMessagesState( + successorRoom = null, + eventSink = eventsRecorder + ) + rule.setMessagesView(state = state) + rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message) + rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action) + } } private fun AndroidComposeTestRule.setMessagesView( state: MessagesState, onBackClick: () -> Unit = EnsureNeverCalled(), onRoomDetailsClick: () -> Unit = EnsureNeverCalled(), - onEventClick: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(), + onEventClick: (isLive: Boolean, event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithTwoParamsAndResult(), onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), - onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), + onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(), onSendLocationClick: () -> Unit = EnsureNeverCalled(), onCreatePollClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: () -> Unit = EnsureNeverCalled(), onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setSafeContent { // Cannot use the RichTextEditor, so simulate a LocalInspectionMode - CompositionLocalProvider( - LocalInspectionMode provides true - ) { + CompositionLocalProvider(LocalInspectionMode provides true) { MessagesView( state = state, onBackClick = onBackClick, @@ -536,7 +621,7 @@ private fun AndroidComposeTestRule.setMessa onCreatePollClick = onCreatePollClick, onJoinCallClick = onJoinCallClick, onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, - knockRequestsBannerView = {} + knockRequestsBannerView = {}, ) } } 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 8a5b388f729..a082b36b391 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 @@ -29,13 +29,13 @@ 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.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_CAPTION import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule @@ -81,8 +81,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -124,8 +122,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -153,7 +149,7 @@ class ActionListPresenterTest { val messageEvent = aMessageEvent( isMine = false, isEditable = false, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -167,8 +163,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -201,7 +195,7 @@ class ActionListPresenterTest { isMine = false, isEditable = false, isThreaded = true, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -248,7 +242,7 @@ class ActionListPresenterTest { val messageEvent = aMessageEvent( isMine = false, isEditable = false, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -262,8 +256,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -296,7 +288,7 @@ class ActionListPresenterTest { val messageEvent = aMessageEvent( isMine = false, isEditable = false, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -344,7 +336,7 @@ class ActionListPresenterTest { val messageEvent = aMessageEvent( isMine = false, isEditable = false, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -391,7 +383,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -405,8 +397,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -439,7 +429,7 @@ class ActionListPresenterTest { val messageEvent = aMessageEvent( isMine = true, isThreaded = true, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -486,7 +476,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -500,8 +490,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -525,6 +513,49 @@ class ActionListPresenterTest { } } + @Test + fun `present - compute for my message no permission`() = runTest { + val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) + ) + initialState.eventSink.invoke( + ActionListEvents.ComputeForMessage( + event = messageEvent, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = false, + canSendReaction = false, + canPinUnpin = false, + ) + ) + ) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + event = messageEvent, + sentTimeFull = "0 Full true", + displayEmojiReactions = false, + verifiedUserSendFailure = VerifiedUserSendFailure.None, + actions = persistentListOf( + TimelineItemAction.Forward, + TimelineItemAction.CopyLink, + TimelineItemAction.CopyText, + TimelineItemAction.ViewSource, + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + @Test fun `present - compute for a media item`() = runTest { val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true) @@ -747,8 +778,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -789,8 +818,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) } } @@ -804,7 +831,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -818,8 +845,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -852,7 +877,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -866,8 +891,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -893,7 +916,7 @@ class ActionListPresenterTest { @Test fun `present - compute message when event is already pinned`() = runTest { - val room = FakeMatrixRoom().apply { + val room = FakeBaseRoom().apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) } val presenter = createActionListPresenter( @@ -907,7 +930,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( @@ -921,8 +944,6 @@ class ActionListPresenterTest { ) ) ) - // val loadingState = awaitItem() - // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) val successState = awaitItem() assertThat(successState.target).isEqualTo( ActionListState.Target.Success( @@ -956,7 +977,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null) + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE) ) val redactedEvent = aMessageEvent( isMine = true, @@ -1006,7 +1027,7 @@ class ActionListPresenterTest { eventId = null, isMine = true, canBeRepliedTo = false, - content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null), + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE), ) initialState.eventSink.invoke( @@ -1251,7 +1272,7 @@ class ActionListPresenterTest { @Test fun `present - compute for verified user send failure`() = runTest { - val room = FakeMatrixRoom( + val room = FakeBaseRoom( userDisplayNameResult = { Result.success("Alice") } ) val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = false, room = room) @@ -1279,7 +1300,7 @@ class ActionListPresenterTest { private fun createActionListPresenter( isDeveloperModeEnabled: Boolean, isPinFeatureEnabled: Boolean, - room: MatrixRoom = FakeMatrixRoom(), + room: BaseRoom = FakeBaseRoom(), allowCaption: Boolean = true, ): ActionListPresenter { val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled) 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 63a759bd677..31065275b9a 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 @@ -29,11 +29,13 @@ 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.VideoInfo import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.test.A_CAPTION import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo @@ -105,16 +107,20 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media success scenario`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } - val room = FakeMatrixRoom( - progressCallbackValues = listOf( - Pair(0, 10), - Pair(5, 10), - Pair(10, 10) - ), - sendFileResult = sendFileResult, + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline( + progressCallbackValues = listOf( + Pair(0, 10), + Pair(5, 10), + Pair(10, 10) + ), + ).apply { + sendFileLambda = sendFileResult + }, ) val onDoneListener = lambdaRecorder { } val presenter = createAttachmentsPreviewPresenter( @@ -142,11 +148,14 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media after pre-processing success scenario`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } - val room = FakeMatrixRoom( - sendFileResult = sendFileResult, + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val onDoneListener = lambdaRecorder { } val processLatch = CompletableDeferred() @@ -177,11 +186,14 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media before pre-processing success scenario`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } - val room = FakeMatrixRoom( - sendFileResult = sendFileResult, + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val onDoneListener = lambdaRecorder { } val processLatch = CompletableDeferred() @@ -212,7 +224,7 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media with pre-processing failure after user sends media`() = runTest { - val room = FakeMatrixRoom() + val room = FakeJoinedRoom() val onDoneListener = lambdaRecorder { } val processLatch = CompletableDeferred() val presenter = createAttachmentsPreviewPresenter( @@ -238,7 +250,7 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media with pre-processing failure before user sends media`() = runTest { - val room = FakeMatrixRoom() + val room = FakeJoinedRoom() val onDoneListener = lambdaRecorder { } val processLatch = CompletableDeferred() val presenter = createAttachmentsPreviewPresenter( @@ -287,14 +299,16 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send image with caption success scenario`() = runTest { val sendImageResult = - lambdaRecorder> { _, _, _, _, _, _ -> + lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? -> Result.success(FakeMediaUploadHandler()) } val mediaPreProcessor = FakeMediaPreProcessor().apply { givenImageResult() } - val room = FakeMatrixRoom( - sendImageResult = sendImageResult, + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendImageLambda = sendImageResult + }, ) val onDoneListener = lambdaRecorder { } val presenter = createAttachmentsPreviewPresenter( @@ -320,6 +334,7 @@ class AttachmentsPreviewPresenterTest { value(A_CAPTION), any(), any(), + any(), ) onDoneListener.assertions().isCalledOnce() } @@ -328,14 +343,16 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send video with caption success scenario`() = runTest { val sendVideoResult = - lambdaRecorder> { _, _, _, _, _, _ -> + lambdaRecorder { _: File, _: File?, _: VideoInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? -> Result.success(FakeMediaUploadHandler()) } val mediaPreProcessor = FakeMediaPreProcessor().apply { givenVideoResult() } - val room = FakeMatrixRoom( - sendVideoResult = sendVideoResult, + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendVideoLambda = sendVideoResult + }, ) val onDoneListener = lambdaRecorder { } val presenter = createAttachmentsPreviewPresenter( @@ -361,6 +378,7 @@ class AttachmentsPreviewPresenterTest { value(A_CAPTION), any(), any(), + any(), ) onDoneListener.assertions().isCalledOnce() } @@ -369,14 +387,16 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send audio with caption success scenario`() = runTest { val sendAudioResult = - lambdaRecorder> { _, _, _, _, _ -> + lambdaRecorder> { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() } - val room = FakeMatrixRoom( - sendAudioResult = sendAudioResult, + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendAudioLambda = sendAudioResult + }, ) val onDoneListener = lambdaRecorder { } val presenter = createAttachmentsPreviewPresenter( @@ -399,6 +419,7 @@ class AttachmentsPreviewPresenterTest { value(A_CAPTION), any(), any(), + any(), ) onDoneListener.assertions().isCalledOnce() } @@ -407,11 +428,14 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media failure scenario without media queue`() = runTest { val failure = MediaPreProcessor.Failure(null) - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> - Result.failure(failure) - } - val room = FakeMatrixRoom( - sendFileResult = sendFileResult, + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> + Result.failure(failure) + } + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val presenter = createAttachmentsPreviewPresenter(room = room, mediaUploadOnSendQueueEnabled = false) moleculeFlow(RecompositionMode.Immediate) { @@ -435,12 +459,15 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - send media failure scenario with media queue`() = runTest { val failure = MediaPreProcessor.Failure(null) - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> - Result.failure(failure) - } + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> + Result.failure(failure) + } val onDoneListenerResult = lambdaRecorder {} - val room = FakeMatrixRoom( - sendFileResult = sendFileResult, + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val presenter = createAttachmentsPreviewPresenter(room = room, mediaUploadOnSendQueueEnabled = true, onDoneListener = onDoneListenerResult) moleculeFlow(RecompositionMode.Immediate) { @@ -486,7 +513,17 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - dismissing the progress dialog stops media upload with media queue`() = runTest { val onDoneListenerResult = lambdaRecorder {} - val presenter = createAttachmentsPreviewPresenter(mediaUploadOnSendQueueEnabled = true, onDoneListener = onDoneListenerResult) + val presenter = createAttachmentsPreviewPresenter( + room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendFileLambda = { _, _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + } + ), + mediaUploadOnSendQueueEnabled = true, + onDoneListener = onDoneListenerResult, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -509,7 +546,7 @@ class AttachmentsPreviewPresenterTest { localMedia: LocalMedia = aLocalMedia( uri = mockMediaUrl, ), - room: MatrixRoom = FakeMatrixRoom(), + room: JoinedRoom = FakeJoinedRoom(), permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(), mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(), temporaryUriDeleter: TemporaryUriDeleter = FakeTemporaryUriDeleter(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt new file mode 100644 index 00000000000..10290610dc7 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/SendActionStateTest.kt @@ -0,0 +1,26 @@ +/* + * 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.messages.impl.attachments + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.attachments.preview.SendActionState +import io.element.android.features.messages.impl.attachments.preview.aMediaUploadInfo +import io.element.android.libraries.mediaupload.api.MediaUploadInfo +import org.junit.Test + +class SendActionStateTest { + @Test + fun `mediaUploadInfo() should return the value from Uploading class`() { + val mediaUploadInfo: MediaUploadInfo = aMediaUploadInfo() + val state: SendActionState = SendActionState.Sending.Uploading( + progress = 0.5f, + mediaUploadInfo = aMediaUploadInfo() + ) + assertThat(state.mediaUploadInfo()).isEqualTo(mediaUploadInfo) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt index 115e14c82c2..40da23ae297 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt @@ -13,18 +13,20 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.RoomMembersState 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.encryption.FakeEncryptionService -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.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.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -44,12 +46,15 @@ class IdentityChangeStatePresenterTest { @Test fun `present - when the room emits identity change, the presenter emits new state`() = runTest { - val room = FakeMatrixRoom(isEncrypted = true) + val identityStateChanges = MutableStateFlow(emptyList()) + val room = FakeJoinedRoom(identityStateChangesFlow = identityStateChanges).apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + } val presenter = createIdentityChangeStatePresenter(room) presenter.test { val initialState = awaitItem() assertThat(initialState.roomMemberIdentityStateChanges).isEmpty() - room.emitIdentityStateChanges( + identityStateChanges.emit( listOf( IdentityStateChange( userId = A_USER_ID_2, @@ -67,15 +72,16 @@ class IdentityChangeStatePresenterTest { @Test fun `present - when the clear room emits identity change, the presenter does not emit new state`() = runTest { - val room = FakeMatrixRoom( - isEncrypted = false, + val identityStateChanges = MutableStateFlow(emptyList()) + val room = FakeJoinedRoom( + identityStateChangesFlow = identityStateChanges, enableEncryptionResult = { Result.success(Unit) } ) val presenter = createIdentityChangeStatePresenter(room) presenter.test { val initialState = awaitItem() assertThat(initialState.roomMemberIdentityStateChanges).isEmpty() - room.emitIdentityStateChanges( + identityStateChanges.emit( listOf( IdentityStateChange( userId = A_USER_ID_2, @@ -85,8 +91,9 @@ class IdentityChangeStatePresenterTest { ) // No item emitted. expectNoEvents() - // Room become encrypted. - room.enableEncryption() + // Room becomes encrypted. + room.givenRoomInfo(aRoomInfo(isEncrypted = true)) + val finalItem = awaitItem() assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) val value = finalItem.roomMemberIdentityStateChanges.first() @@ -99,9 +106,10 @@ class IdentityChangeStatePresenterTest { @Test fun `present - when the room emits identity change, the presenter emits new state with member details`() = runTest { - val room = FakeMatrixRoom(isEncrypted = true).apply { + val identityStateChanges = MutableStateFlow(emptyList()) + val room = FakeJoinedRoom(identityStateChangesFlow = identityStateChanges).apply { givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( listOf( aRoomMember( A_USER_ID_2, @@ -110,12 +118,13 @@ class IdentityChangeStatePresenterTest { ).toImmutableList() ) ) + givenRoomInfo(aRoomInfo(isEncrypted = true)) } val presenter = createIdentityChangeStatePresenter(room) presenter.test { val initialState = awaitItem() assertThat(initialState.roomMemberIdentityStateChanges).isEmpty() - room.emitIdentityStateChanges( + identityStateChanges.emit( listOf( IdentityStateChange( userId = A_USER_ID_2, @@ -143,13 +152,28 @@ class IdentityChangeStatePresenterTest { val presenter = createIdentityChangeStatePresenter(encryptionService = encryptionService) presenter.test { val initialState = awaitItem() - initialState.eventSink(IdentityChangeEvent.Submit(A_USER_ID)) + initialState.eventSink(IdentityChangeEvent.PinIdentity(A_USER_ID)) + lambda.assertions().isCalledOnce().with(value(A_USER_ID)) + } + } + + @Test + fun `present - when the user withdraws the identity, the presenter invokes the encryption service api`() = + runTest { + val lambda = lambdaRecorder> { Result.success(Unit) } + val encryptionService = FakeEncryptionService( + withdrawVerificationResult = lambda, + ) + val presenter = createIdentityChangeStatePresenter(encryptionService = encryptionService) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(IdentityChangeEvent.WithdrawVerification(A_USER_ID)) lambda.assertions().isCalledOnce().with(value(A_USER_ID)) } } private fun createIdentityChangeStatePresenter( - room: MatrixRoom = FakeMatrixRoom(), + room: JoinedRoom = FakeJoinedRoom(), encryptionService: EncryptionService = FakeEncryptionService(), ): IdentityChangeStatePresenter { return IdentityChangeStatePresenter( 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 new file mode 100644 index 00000000000..a96aa6c5903 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateViewTest.kt @@ -0,0 +1,108 @@ +/* + * 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.messages.impl.crypto.identity + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +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 +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.ui.room.IdentityRoomMember +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`() { + val eventsRecorder = EventsRecorder() + rule.setIdentityChangeStateView( + state = anIdentityChangeState( + listOf( + RoomMemberIdentityStateChange( + identityRoomMember = IdentityRoomMember(UserId("@alice:localhost"), "Alice", anAvatarData()), + identityState = IdentityState.PinViolation + ) + ), + eventsRecorder + ), + ) + + 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") + + rule.clickOn(res = CommonStrings.action_dismiss) + eventsRecorder.assertSingle(IdentityChangeEvent.PinIdentity(UserId("@alice:localhost"))) + } + + @Test + fun `show and resolve verification violation`() { + val eventsRecorder = EventsRecorder() + rule.setIdentityChangeStateView( + state = anIdentityChangeState( + listOf( + RoomMemberIdentityStateChange( + identityRoomMember = IdentityRoomMember(UserId("@alice:localhost"), "Alice", anAvatarData()), + identityState = IdentityState.VerificationViolation + ) + ), + eventsRecorder + ), + ) + + 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") + + 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`() { + rule.setIdentityChangeStateView( + state = anIdentityChangeState( + listOf( + RoomMemberIdentityStateChange( + identityRoomMember = IdentityRoomMember(UserId("@alice:localhost"), "Alice", anAvatarData()), + identityState = IdentityState.Verified + ), + RoomMemberIdentityStateChange( + identityRoomMember = IdentityRoomMember(UserId("@bob:localhost"), "Bob", anAvatarData()), + identityState = IdentityState.Pinned + ) + ), + ), + ) + + rule.onNodeWithText("identity was reset", substring = true).assertDoesNotExist() + } + + private fun AndroidComposeTestRule.setIdentityChangeStateView( + state: IdentityChangeState, + ) { + setContent { + IdentityChangeStateView( + state = state, + onLinkClick = { _, _ -> }, + ) + } + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenterTest.kt index 374eebdc3b7..fd2a2fafc29 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenterTest.kt @@ -13,13 +13,13 @@ 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.model.TimelineItem import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_TRANSACTION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest @@ -81,11 +81,13 @@ class ResolveVerifiedUserSendFailurePresenterTest { @Test fun `present - verified user unsigned device failure dismiss scenario`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( userDisplayNameResult = { userId -> Result.success(userId.value) }, ) + ) val presenter = createResolveVerifiedUserSendFailurePresenter(room) presenter.test { val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage() @@ -107,11 +109,13 @@ class ResolveVerifiedUserSendFailurePresenterTest { @Test fun `present - verified user unsigned device failure retry scenario`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( userDisplayNameResult = { userId -> Result.success(userId.value) }, ) + ) val presenter = createResolveVerifiedUserSendFailurePresenter(room) presenter.test { val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage() @@ -138,10 +142,12 @@ class ResolveVerifiedUserSendFailurePresenterTest { @Test fun `present - verified user unsigned device failure resolve and resend scenario`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( userDisplayNameResult = { userId -> Result.success(userId.value) }, + ), ignoreDeviceTrustAndResendResult = { _, _ -> Result.success(Unit) }, @@ -179,10 +185,12 @@ class ResolveVerifiedUserSendFailurePresenterTest { @Test fun `present - verified user unsigned device failure resolve and resend scenario with error`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( userDisplayNameResult = { userId -> Result.success(userId.value) }, + ), ignoreDeviceTrustAndResendResult = { _, _ -> Result.failure(Exception()) }, @@ -212,11 +220,13 @@ class ResolveVerifiedUserSendFailurePresenterTest { @Test fun `present - verified user changed identity failure retry scenario`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( userDisplayNameResult = { userId -> Result.success(userId.value) }, ) + ) val presenter = createResolveVerifiedUserSendFailurePresenter(room) presenter.test { val failedMessage = aVerifiedUserChangedIdentityMessage() @@ -243,10 +253,12 @@ class ResolveVerifiedUserSendFailurePresenterTest { @Test fun `present - verified user changed identity failure resolve and resend scenario`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( userDisplayNameResult = { userId -> Result.success(userId.value) }, + ), withdrawVerificationAndResendResult = { _, _ -> Result.success(Unit) }, @@ -284,10 +296,12 @@ class ResolveVerifiedUserSendFailurePresenterTest { @Test fun `present - verified user changed identity failure resolve and resend scenario with error`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( userDisplayNameResult = { userId -> Result.success(userId.value) }, + ), withdrawVerificationAndResendResult = { _, _ -> Result.failure(Exception()) }, @@ -337,7 +351,7 @@ class ResolveVerifiedUserSendFailurePresenterTest { } private fun createResolveVerifiedUserSendFailurePresenter( - room: MatrixRoom = FakeMatrixRoom(), + room: FakeJoinedRoom = FakeJoinedRoom(), ): ResolveVerifiedUserSendFailurePresenter { return ResolveVerifiedUserSendFailurePresenter( room = room, 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 a8190e8776d..6a83437758c 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 @@ -14,6 +14,7 @@ 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 @@ -54,7 +55,7 @@ class ResolveVerifiedUserSendFailureViewTest { private fun AndroidComposeTestRule.setResolveVerifiedUserSendFailureView( state: ResolveVerifiedUserSendFailureState, ) { - setContent { + setSafeContent { ResolveVerifiedUserSendFailureView(state = state) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt index 6fd41723401..d87b11c951c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt @@ -8,12 +8,22 @@ package io.element.android.features.messages.impl.draft 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.room.draft.ComposerDraft class FakeComposerDraftService : ComposerDraftService { - var loadDraftLambda: (RoomId, Boolean) -> ComposerDraft? = { _, _ -> null } - override suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? = loadDraftLambda(roomId, isVolatile) + var loadDraftLambda: (RoomId, ThreadId?, Boolean) -> ComposerDraft? = { _, _, _ -> null } + override suspend fun loadDraft( + roomId: RoomId, + threadRoot: ThreadId?, + isVolatile: Boolean + ): ComposerDraft? = loadDraftLambda(roomId, threadRoot, isVolatile) - var saveDraftLambda: (RoomId, ComposerDraft?, Boolean) -> Unit = { _, _, _ -> } - override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) = saveDraftLambda(roomId, draft, isVolatile) + var saveDraftLambda: (RoomId, ThreadId?, ComposerDraft?, Boolean) -> Unit = { _, _, _, _ -> } + override suspend fun updateDraft( + roomId: RoomId, + threadRoot: ThreadId?, + draft: ComposerDraft?, + isVolatile: Boolean + ) = saveDraftLambda(roomId, threadRoot, draft, isVolatile) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStoreTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStoreTest.kt index 176037358e4..9ec43f3621e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStoreTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStoreTest.kt @@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat 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.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID import kotlinx.coroutines.test.runTest import org.junit.Test @@ -21,27 +22,51 @@ class VolatileComposerDraftStoreTest { @Test fun `when storing a non-null draft and then loading it, it's loaded and removed`() = runTest { - val initialDraft = sut.loadDraft(roomId) + val initialDraft = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(initialDraft).isNull() - sut.updateDraft(roomId, draft) + sut.updateDraft(roomId = roomId, threadRoot = null, draft = draft) - val loadedDraft = sut.loadDraft(roomId) + val loadedDraft = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(loadedDraft).isEqualTo(draft) - val loadedDraftAfter = sut.loadDraft(roomId) + val loadedDraftAfter = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(loadedDraftAfter).isNull() + + // In thread: + val threadRoot = A_THREAD_ID + val initialThreadDraft = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(initialThreadDraft).isNull() + + sut.updateDraft(roomId = roomId, threadRoot = threadRoot, draft = draft) + + val loadedThreadDraft = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(loadedThreadDraft).isEqualTo(draft) + + val loadedThreadDraftAfter = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(loadedThreadDraftAfter).isNull() } @Test fun `when storing a null draft and then loading it, it's removing the previous one`() = runTest { - val initialDraft = sut.loadDraft(roomId) + val initialDraft = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(initialDraft).isNull() - sut.updateDraft(roomId, draft) - sut.updateDraft(roomId, null) + sut.updateDraft(roomId = roomId, threadRoot = null, draft = draft) + sut.updateDraft(roomId = roomId, threadRoot = null, draft = null) - val loadedDraft = sut.loadDraft(roomId) + val loadedDraft = sut.loadDraft(roomId = roomId, threadRoot = null) assertThat(loadedDraft).isNull() + + // In thread: + val threadRoot = A_THREAD_ID + val initialThreadDraft = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(initialThreadDraft).isNull() + + sut.updateDraft(roomId = roomId, threadRoot = threadRoot, draft = draft) + sut.updateDraft(roomId = roomId, threadRoot = threadRoot, draft = null) + + val loadedThreadDraft = sut.loadDraft(roomId = roomId, threadRoot = threadRoot) + assertThat(loadedThreadDraft).isNull() } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt index c52f01949da..a8dbab2cf21 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt @@ -38,7 +38,7 @@ internal fun aMessageEvent( isMine: Boolean = true, isEditable: Boolean = true, canBeRepliedTo: Boolean = true, - content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = null, isEdited = false), + content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = A_MESSAGE, isEdited = false), inReplyTo: InReplyToDetails? = null, isThreaded: Boolean = false, sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt index 1824eeb9871..757e682592e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt @@ -15,13 +15,13 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.AN_EVENT_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -49,8 +49,8 @@ class ForwardMessagesPresenterTest { val timeline = FakeTimeline().apply { this.forwardEventLambda = forwardEventLambda } - val room = FakeMatrixRoom(liveTimeline = timeline) - val presenter = aForwardMessagesPresenter(fakeMatrixRoom = room) + val room = FakeJoinedRoom(liveTimeline = timeline) + val presenter = aForwardMessagesPresenter(fakeRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -73,8 +73,8 @@ class ForwardMessagesPresenterTest { val timeline = FakeTimeline().apply { this.forwardEventLambda = forwardEventLambda } - val room = FakeMatrixRoom(liveTimeline = timeline) - val presenter = aForwardMessagesPresenter(fakeMatrixRoom = room) + val room = FakeJoinedRoom(liveTimeline = timeline) + val presenter = aForwardMessagesPresenter(fakeRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -91,13 +91,12 @@ class ForwardMessagesPresenterTest { } } - private fun CoroutineScope.aForwardMessagesPresenter( + private fun TestScope.aForwardMessagesPresenter( eventId: EventId = AN_EVENT_ID, - fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(), - coroutineScope: CoroutineScope = this, + fakeRoom: FakeJoinedRoom = FakeJoinedRoom(), ) = ForwardMessagesPresenter( eventId = eventId.value, - timelineProvider = LiveTimelineProvider(fakeMatrixRoom), - appCoroutineScope = coroutineScope, + timelineProvider = LiveTimelineProvider(fakeRoom), + sessionCoroutineScope = this, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/DefaultLinkCheckerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/DefaultLinkCheckerTest.kt new file mode 100644 index 00000000000..9604290e546 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/DefaultLinkCheckerTest.kt @@ -0,0 +1,51 @@ +/* + * 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.messages.impl.link + +import com.google.common.truth.Truth.assertThat +import io.element.android.wysiwyg.link.Link +import org.junit.Test + +class DefaultLinkCheckerTest { + private val sut = DefaultLinkChecker() + + @Test + fun `when url and text are identical, the link is safe`() { + assertThat(sut.isSafe(Link("url", "url"))).isTrue() + } + + @Test + fun `when url is not safe, the link is safe`() { + assertThat(sut.isSafe(Link("url", "https://example.org"))).isTrue() + } + + @Test + fun `when text is a url, and url is identical the link is safe`() { + assertThat(sut.isSafe(Link("https://example.org", "https://example.org"))).isTrue() + } + + @Test + fun `when url contains RtL char, the link is not safe`() { + assertThat(sut.isSafe(Link("https://example\u202E.org", "text"))).isFalse() + } + + @Test + fun `when text is not a url, the link is safe`() { + assertThat(sut.isSafe(Link("https://example.org", "url"))).isTrue() + } + + @Test + fun `when text is a url and hosts match, the link is safe`() { + assertThat(sut.isSafe(Link("https://example.org/some/path", "https://example.org"))).isTrue() + } + + @Test + fun `when text is a url and hosts do not match, the link is safe`() { + assertThat(sut.isSafe(Link("https://example.org", "https://evil.org"))).isFalse() + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/FakeLinkChecker.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/FakeLinkChecker.kt new file mode 100644 index 00000000000..a50f9109f48 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/FakeLinkChecker.kt @@ -0,0 +1,17 @@ +/* + * 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.messages.impl.link + +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.wysiwyg.link.Link + +class FakeLinkChecker( + private val isSafeResult: (Link) -> Boolean = { lambdaError() } +) : LinkChecker { + override fun isSafe(link: Link) = isSafeResult(link) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkPresenterTest.kt new file mode 100644 index 00000000000..87ab37e63d0 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkPresenterTest.kt @@ -0,0 +1,96 @@ +/* + * 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.messages.impl.link + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +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.wysiwyg.link.Link +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +val aLink = Link(url = "url", text = "text") + +class LinkPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.linkClick).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - safe link case`() = runTest { + val isSafeResult = lambdaRecorder { + true + } + val presenter = createPresenter( + linkChecker = FakeLinkChecker(isSafeResult = isSafeResult) + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.linkClick).isEqualTo(AsyncAction.Uninitialized) + initialState.eventSink(LinkEvents.OnLinkClick(aLink)) + assertThat(awaitItem().linkClick).isEqualTo(AsyncAction.Loading) + val state = awaitItem() + assertThat(state.linkClick).isEqualTo(AsyncAction.Success(aLink)) + isSafeResult.assertions().isCalledOnce().with(value(aLink)) + } + } + + @Test + fun `present - suspicious link case - cancel`() = runTest { + val presenter = createPresenter( + linkChecker = FakeLinkChecker(isSafeResult = { false }) + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.linkClick).isEqualTo(AsyncAction.Uninitialized) + initialState.eventSink(LinkEvents.OnLinkClick(aLink)) + assertThat(awaitItem().linkClick).isEqualTo(AsyncAction.Loading) + val state = awaitItem() + assertThat(state.linkClick).isEqualTo(ConfirmingLinkClick(aLink)) + state.eventSink(LinkEvents.Cancel) + val finalState = awaitItem() + assertThat(finalState.linkClick).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - suspicious link case - confirm`() = runTest { + val presenter = createPresenter( + linkChecker = FakeLinkChecker(isSafeResult = { false }) + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.linkClick).isEqualTo(AsyncAction.Uninitialized) + initialState.eventSink(LinkEvents.OnLinkClick(aLink)) + assertThat(awaitItem().linkClick).isEqualTo(AsyncAction.Loading) + val state = awaitItem() + assertThat(state.linkClick).isEqualTo(ConfirmingLinkClick(aLink)) + state.eventSink(LinkEvents.Confirm) + val finalState = awaitItem() + assertThat(finalState.linkClick).isEqualTo(AsyncAction.Success(aLink)) + } + } + + private fun createPresenter( + linkChecker: LinkChecker = FakeLinkChecker(), + ) = LinkPresenter( + linkChecker = linkChecker, + ) +} 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 new file mode 100644 index 00000000000..2936cbb31de --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkViewTest.kt @@ -0,0 +1,89 @@ +/* + * 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.messages.impl.link + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.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`() { + val eventsRecorder = EventsRecorder() + rule.setLinkView( + aLinkState( + linkClick = ConfirmingLinkClick(aLink), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle( + LinkEvents.Cancel + ) + } + + @Test + fun `clicking on continue emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setLinkView( + aLinkState( + linkClick = ConfirmingLinkClick(aLink), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_continue) + eventsRecorder.assertSingle( + LinkEvents.Confirm + ) + } + + @Test + fun `success state invokes the callback and emits the expected event`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnceWithParam(aLink) { callback -> + rule.setLinkView( + aLinkState( + linkClick = AsyncAction.Success(aLink), + eventSink = eventsRecorder, + ), + onLinkValid = callback, + ) + } + eventsRecorder.assertSingle( + LinkEvents.Cancel + ) + } +} + +private fun AndroidComposeTestRule.setLinkView( + state: LinkState, + onLinkValid: (Link) -> Unit = EnsureNeverCalledWithParam(), +) { + setContent { + LinkView( + state = state, + onLinkValid = onLinkValid, + ) + } +} 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 b7ba51192bd..1a7a1d930ed 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 @@ -18,6 +18,8 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.Interaction +import io.element.android.features.location.api.LocationService +import io.element.android.features.location.test.FakeLocationService import io.element.android.features.messages.impl.FakeMessagesNavigator import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.attachments.Attachment @@ -25,6 +27,7 @@ import io.element.android.features.messages.impl.draft.ComposerDraftService import io.element.android.features.messages.impl.draft.FakeComposerDraftService import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor import io.element.android.features.messages.impl.timeline.TimelineController +import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper import io.element.android.features.messages.impl.utils.TextPillificationHelper import io.element.android.libraries.core.mimetype.MimeTypes @@ -34,16 +37,18 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.IntentionalMention -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo @@ -59,13 +64,14 @@ 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_ID_3 import io.element.android.libraries.matrix.test.A_USER_ID_4 -import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.timeline.FakeTimeline -import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider @@ -80,6 +86,7 @@ import io.element.android.libraries.permissions.test.FakePermissionsPresenterFac import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.Suggestion @@ -96,8 +103,8 @@ import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -127,7 +134,7 @@ class MessageComposerPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -142,7 +149,7 @@ class MessageComposerPresenterTest { @Test fun `present - toggle fullscreen`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -158,7 +165,7 @@ class MessageComposerPresenterTest { @Test fun `present - change message`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -172,16 +179,15 @@ class MessageComposerPresenterTest { @Test fun `present - change mode to edit`() = runTest { - val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean -> + val loadDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: Boolean -> ComposerDraft(A_MESSAGE, A_MESSAGE, ComposerDraftType.NewMessage) } - val updateDraftLambda = lambdaRecorder { _: RoomId, _: ComposerDraft?, _: Boolean -> } + val updateDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: ComposerDraft?, _: Boolean -> } val draftService = FakeComposerDraftService().apply { this.loadDraftLambda = loadDraftLambda this.saveDraftLambda = updateDraftLambda } val presenter = createPresenter( - coroutineScope = this, draftService = draftService, ) moleculeFlow(RecompositionMode.Immediate) { @@ -202,29 +208,28 @@ class MessageComposerPresenterTest { .isCalledExactly(2) .withSequence( // Automatic load of draft - listOf(value(A_ROOM_ID), value(false)), + listOf(value(A_ROOM_ID), value(null), value(false)), // Load of volatile draft when closing edit mode - listOf(value(A_ROOM_ID), value(true)) + listOf(value(A_ROOM_ID), value(null), value(true)) ) assert(updateDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), any(), value(true)) + .with(value(A_ROOM_ID), value(null), any(), value(true)) } } @Test fun `present - change mode to edit caption`() = runTest { - val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean -> + val loadDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: Boolean -> ComposerDraft(A_MESSAGE, A_MESSAGE, ComposerDraftType.NewMessage) } - val updateDraftLambda = lambdaRecorder { _: RoomId, _: ComposerDraft?, _: Boolean -> } + val updateDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: ComposerDraft?, _: Boolean -> } val draftService = FakeComposerDraftService().apply { this.loadDraftLambda = loadDraftLambda this.saveDraftLambda = updateDraftLambda } val presenter = createPresenter( - coroutineScope = this, draftService = draftService, ) moleculeFlow(RecompositionMode.Immediate) { @@ -245,13 +250,13 @@ class MessageComposerPresenterTest { .isCalledExactly(2) .withSequence( // Automatic load of draft - listOf(value(A_ROOM_ID), value(false)), + listOf(value(A_ROOM_ID), value(null), value(false)), // Load of volatile draft when closing edit mode - listOf(value(A_ROOM_ID), value(true)) + listOf(value(A_ROOM_ID), value(null), value(true)) ) assert(updateDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), any(), value(true)) + .with(value(A_ROOM_ID), value(null), any(), value(true)) } } @@ -263,13 +268,12 @@ class MessageComposerPresenterTest { val timeline = FakeTimeline().apply { this.editCaptionLambda = editCaptionLambda } - val fakeMatrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) } ) val presenter = createPresenter( - coroutineScope = this, - room = fakeMatrixRoom, + room = joinedRoom, isRichTextEditorEnabled = false, ) val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("") }) @@ -300,16 +304,15 @@ class MessageComposerPresenterTest { @Test fun `present - change mode to reply after edit`() = runTest { - val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean -> + val loadDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: Boolean -> ComposerDraft(A_MESSAGE, A_MESSAGE, ComposerDraftType.NewMessage) } - val updateDraftLambda = lambdaRecorder { _: RoomId, _: ComposerDraft?, _: Boolean -> } + val updateDraftLambda = lambdaRecorder { _: RoomId, _: ThreadId?, _: ComposerDraft?, _: Boolean -> } val draftService = FakeComposerDraftService().apply { this.loadDraftLambda = loadDraftLambda this.saveDraftLambda = updateDraftLambda } val presenter = createPresenter( - coroutineScope = this, draftService = draftService, ) moleculeFlow(RecompositionMode.Immediate) { @@ -331,17 +334,17 @@ class MessageComposerPresenterTest { assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) assert(updateDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), any(), value(true)) + .with(value(A_ROOM_ID), value(null), any(), value(true)) } } @Test fun `present - change mode to reply`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -357,7 +360,7 @@ class MessageComposerPresenterTest { @Test fun `present - cancel reply`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -377,9 +380,10 @@ class MessageComposerPresenterTest { @Test fun `present - send message with rich text enabled`() = runTest { val presenter = createPresenter( - coroutineScope = this, - room = FakeMatrixRoom( - sendMessageResult = { _, _, _ -> Result.success(Unit) }, + room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendMessageLambda = { _, _, _ -> Result.success(Unit) } + }, typingNoticeResult = { Result.success(Unit) } ), ) @@ -410,10 +414,11 @@ class MessageComposerPresenterTest { fun `present - send message with plain text enabled`() = runTest { val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("") }) val presenter = createPresenter( - coroutineScope = this, isRichTextEditorEnabled = false, - room = FakeMatrixRoom( - sendMessageResult = { _, _, _ -> Result.success(Unit) }, + room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendMessageLambda = { _, _, _ -> Result.success(Unit) } + }, typingNoticeResult = { Result.success(Unit) } ), ) @@ -450,13 +455,12 @@ class MessageComposerPresenterTest { val timeline = FakeTimeline().apply { this.editMessageLambda = editMessageLambda } - val fakeMatrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) } ) val presenter = createPresenter( - this, - fakeMatrixRoom, + joinedRoom, ) moleculeFlow(RecompositionMode.Immediate) { val state = presenter.present() @@ -505,14 +509,13 @@ class MessageComposerPresenterTest { val roomEditMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List -> Result.success(Unit) } - val fakeMatrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, editMessageLambda = roomEditMessageLambda, ) val presenter = createPresenter( - this, - fakeMatrixRoom, + joinedRoom, ) moleculeFlow(RecompositionMode.Immediate) { val state = presenter.present() @@ -562,13 +565,12 @@ class MessageComposerPresenterTest { val timeline = FakeTimeline().apply { this.editMessageLambda = editMessageLambda } - val fakeMatrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, ) val presenter = createPresenter( - this, - fakeMatrixRoom, + joinedRoom, ) moleculeFlow(RecompositionMode.Immediate) { val state = presenter.present() @@ -608,19 +610,18 @@ class MessageComposerPresenterTest { @Test fun `present - reply message`() = runTest { - val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List, _: Boolean -> + val replyMessageLambda = lambdaRecorder { _: ReplyParameters, _: String, _: String?, _: List, _: Boolean -> Result.success(Unit) } val timeline = FakeTimeline().apply { this.replyMessageLambda = replyMessageLambda } - val fakeMatrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) } ) val presenter = createPresenter( - this, - fakeMatrixRoom, + joinedRoom, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -657,7 +658,7 @@ class MessageComposerPresenterTest { @Test fun `present - Open attachments menu`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -670,7 +671,7 @@ class MessageComposerPresenterTest { @Test fun `present - Dismiss attachments menu`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -685,7 +686,7 @@ class MessageComposerPresenterTest { @Test fun `present - Pick image from gallery`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } @@ -693,7 +694,6 @@ class MessageComposerPresenterTest { onPreviewAttachmentLambda = onPreviewAttachmentLambda ) val presenter = createPresenter( - coroutineScope = this, room = room, navigator = navigator, ) @@ -726,7 +726,7 @@ class MessageComposerPresenterTest { @Test fun `present - Pick video from gallery`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } @@ -734,7 +734,6 @@ class MessageComposerPresenterTest { onPreviewAttachmentLambda = onPreviewAttachmentLambda ) val presenter = createPresenter( - coroutineScope = this, room = room, navigator = navigator, ) @@ -768,7 +767,7 @@ class MessageComposerPresenterTest { @Test fun `present - Pick media from gallery & cancel does nothing`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter() with(pickerProvider) { givenResult(null) // Simulate a user canceling the flow givenMimeType(MimeTypes.Images) @@ -784,7 +783,7 @@ class MessageComposerPresenterTest { @Test fun `present - Pick file from storage will open the preview`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) val onPreviewAttachmentLambda = lambdaRecorder { _: ImmutableList -> } @@ -792,7 +791,6 @@ class MessageComposerPresenterTest { onPreviewAttachmentLambda = onPreviewAttachmentLambda ) val presenter = createPresenter( - coroutineScope = this, room = room, navigator = navigator, ) @@ -807,10 +805,10 @@ class MessageComposerPresenterTest { @Test fun `present - create poll`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) - val presenter = createPresenter(this, room = room) + val presenter = createPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -826,10 +824,10 @@ class MessageComposerPresenterTest { @Test fun `present - share location`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) - val presenter = createPresenter(this, room = room) + val presenter = createPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -845,7 +843,7 @@ class MessageComposerPresenterTest { @Test fun `present - Take photo`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() } @@ -854,7 +852,6 @@ class MessageComposerPresenterTest { onPreviewAttachmentLambda = onPreviewAttachmentLambda ) val presenter = createPresenter( - coroutineScope = this, room = room, permissionPresenter = permissionPresenter, navigator = navigator, @@ -870,7 +867,7 @@ class MessageComposerPresenterTest { @Test fun `present - Take photo with permission request`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) val permissionPresenter = FakePermissionsPresenter() @@ -879,7 +876,6 @@ class MessageComposerPresenterTest { onPreviewAttachmentLambda = onPreviewAttachmentLambda ) val presenter = createPresenter( - coroutineScope = this, room = room, permissionPresenter = permissionPresenter, navigator = navigator, @@ -897,7 +893,7 @@ class MessageComposerPresenterTest { @Test fun `present - Record video`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() } @@ -906,7 +902,6 @@ class MessageComposerPresenterTest { onPreviewAttachmentLambda = onPreviewAttachmentLambda ) val presenter = createPresenter( - coroutineScope = this, room = room, permissionPresenter = permissionPresenter, navigator = navigator, @@ -922,7 +917,7 @@ class MessageComposerPresenterTest { @Test fun `present - Record video with permission request`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ) val permissionPresenter = FakePermissionsPresenter() @@ -931,7 +926,6 @@ class MessageComposerPresenterTest { onPreviewAttachmentLambda = onPreviewAttachmentLambda ) val presenter = createPresenter( - coroutineScope = this, room = room, permissionPresenter = permissionPresenter, navigator = navigator, @@ -952,7 +946,7 @@ class MessageComposerPresenterTest { @Test fun `present - errors are tracked`() = runTest { val testException = Exception("Test error") - val presenter = createPresenter(this) + val presenter = createPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -964,7 +958,7 @@ class MessageComposerPresenterTest { @Test fun `present - ToggleTextFormatting toggles text formatting`() = runTest { - val presenter = createPresenter(this, isRichTextEditorEnabled = false) + val presenter = createPresenter(isRichTextEditorEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -997,27 +991,21 @@ class MessageComposerPresenterTest { val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN) val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN) var canUserTriggerRoomNotificationResult = true - val room = FakeMatrixRoom( - isDirect = false, - canUserTriggerRoomNotificationResult = { Result.success(canUserTriggerRoomNotificationResult) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom(canUserTriggerRoomNotificationResult = { Result.success(canUserTriggerRoomNotificationResult) }), typingNoticeResult = { Result.success(Unit) } ).apply { givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( persistentListOf(currentUser, invitedUser, bob, david), ) ) + givenRoomInfo(aRoomInfo(isDirect = false)) } - val flagsService = FakeFeatureFlagService( - mapOf( - FeatureFlags.Mentions.key to true, - ) - ) - val presenter = createPresenter(this, room, featureFlagService = flagsService) + val presenter = createPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() // A null suggestion (no suggestion was received) returns nothing @@ -1059,29 +1047,26 @@ class MessageComposerPresenterTest { val invitedUser = aRoomMember(userId = A_USER_ID_3, membership = RoomMembershipState.INVITE) val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN) val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN) - val room = FakeMatrixRoom( - isDirect = true, - activeMemberCount = 2, - isEncrypted = true, - canUserTriggerRoomNotificationResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom(canUserTriggerRoomNotificationResult = { Result.success(true) }), typingNoticeResult = { Result.success(Unit) } ).apply { givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( persistentListOf(currentUser, invitedUser, bob, david), ) ) - } - val flagsService = FakeFeatureFlagService( - mapOf( - FeatureFlags.Mentions.key to true, + givenRoomInfo( + aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) ) - ) - val presenter = createPresenter(this, room, featureFlagService = flagsService) + } + val presenter = createPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() // An empty suggestion returns the joined members that are not the current user, but not the room @@ -1094,7 +1079,6 @@ class MessageComposerPresenterTest { fun `present - InsertSuggestion`() = runTest { val presenter = createPresenter( - coroutineScope = this, permalinkBuilder = FakePermalinkBuilder( permalinkForUserLambda = { Result.success("https://matrix.to/#/${A_USER_ID_2.value}") @@ -1116,25 +1100,25 @@ class MessageComposerPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - send messages with intentional mentions`() = runTest { - val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List, _: Boolean -> + val replyMessageLambda = lambdaRecorder { _: ReplyParameters, _: String, _: String?, _: List, _: Boolean -> Result.success(Unit) } val editMessageLambda = lambdaRecorder { _: EventOrTransactionId, _: String, _: String?, _: List -> Result.success(Unit) } + val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List -> + Result.success(Unit) + } val timeline = FakeTimeline().apply { this.replyMessageLambda = replyMessageLambda this.editMessageLambda = editMessageLambda + sendMessageLambda = sendMessageResult } - val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List -> - Result.success(Unit) - } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( liveTimeline = timeline, - sendMessageResult = sendMessageResult, typingNoticeResult = { Result.success(Unit) } ) - val presenter = createPresenter(room = room, coroutineScope = this) + val presenter = createPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -1197,7 +1181,16 @@ class MessageComposerPresenterTest { @Test fun `present - send uri`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter( + room = FakeJoinedRoom( + typingNoticeResult = { Result.success(Unit) }, + liveTimeline = FakeTimeline().apply { + sendFileLambda = { _, _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + } + ), + ) moleculeFlow(RecompositionMode.Immediate) { val state = presenter.present() remember(state, state.textEditorState.messageHtml()) { state } @@ -1211,10 +1204,10 @@ class MessageComposerPresenterTest { @Test fun `present - handle typing notice event`() = runTest { val typingNoticeResult = lambdaRecorder> { Result.success(Unit) } - val room = FakeMatrixRoom( - typingNoticeResult = typingNoticeResult + val room = FakeJoinedRoom( + typingNoticeResult = typingNoticeResult, ) - val presenter = createPresenter(room = room, coroutineScope = this) + val presenter = createPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -1222,6 +1215,7 @@ class MessageComposerPresenterTest { typingNoticeResult.assertions().isNeverCalled() initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(true)) initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(false)) + advanceUntilIdle() typingNoticeResult.assertions().isCalledExactly(2) .withSequence( listOf(value(true)), @@ -1233,13 +1227,13 @@ class MessageComposerPresenterTest { @Test fun `present - handle typing notice event when sending typing notice is disabled`() = runTest { val typingNoticeResult = lambdaRecorder> { Result.success(Unit) } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( typingNoticeResult = typingNoticeResult ) val store = InMemorySessionPreferencesStore( isSendTypingNotificationsEnabled = false ) - val presenter = createPresenter(room = room, sessionPreferencesStore = store, coroutineScope = this) + val presenter = createPresenter(room = room, sessionPreferencesStore = store) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -1253,18 +1247,18 @@ class MessageComposerPresenterTest { @Test fun `present - when there is no draft, nothing is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> null } + val loadDraftLambda = lambdaRecorder { _, _, _ -> null } val composerDraftService = FakeComposerDraftService().apply { this.loadDraftLambda = loadDraftLambda } - val presenter = createPresenter(draftService = composerDraftService, coroutineScope = this) + val presenter = createPresenter(draftService = composerDraftService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitFirstItem() assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) ensureAllEventsConsumed() } @@ -1272,7 +1266,7 @@ class MessageComposerPresenterTest { @Test fun `present - when there is a draft for new message with plain text, it is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> + val loadDraftLambda = lambdaRecorder { _, _, _ -> ComposerDraft(plainText = A_MESSAGE, htmlText = null, draftType = ComposerDraftType.NewMessage) } val composerDraftService = FakeComposerDraftService().apply { @@ -1282,19 +1276,18 @@ class MessageComposerPresenterTest { val presenter = createPresenter( draftService = composerDraftService, permalinkBuilder = permalinkBuilder, - coroutineScope = this ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - awaitFirstItem().also { state -> + skipItems(2) + awaitItem().also { state -> assertThat(state.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_MESSAGE) assertThat(state.textEditorState.messageHtml()).isNull() } - assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) ensureAllEventsConsumed() } @@ -1302,7 +1295,7 @@ class MessageComposerPresenterTest { @Test fun `present - when there is a draft for new message with rich text, it is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> + val loadDraftLambda = lambdaRecorder { _, _, _ -> ComposerDraft( plainText = A_MESSAGE, htmlText = A_MESSAGE, @@ -1316,26 +1309,26 @@ class MessageComposerPresenterTest { val presenter = createPresenter( draftService = composerDraftService, permalinkBuilder = permalinkBuilder, - coroutineScope = this ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - awaitFirstItem().also { state -> + skipItems(1) + awaitItem().also { state -> assertThat(state.showTextFormatting).isTrue() assertThat(state.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_MESSAGE) assertThat(state.textEditorState.messageHtml()).isEqualTo(A_MESSAGE) } assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) ensureAllEventsConsumed() } } @Test fun `present - when there is a draft for edit, it is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> + val loadDraftLambda = lambdaRecorder { _, _, _ -> ComposerDraft( plainText = A_MESSAGE, htmlText = null, @@ -1349,12 +1342,12 @@ class MessageComposerPresenterTest { val presenter = createPresenter( draftService = composerDraftService, permalinkBuilder = permalinkBuilder, - coroutineScope = this ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - awaitFirstItem().also { state -> + skipItems(2) + awaitItem().also { state -> assertThat(state.showTextFormatting).isFalse() assertThat(state.mode).isEqualTo(anEditMode()) assertThat(state.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_MESSAGE) @@ -1362,7 +1355,7 @@ class MessageComposerPresenterTest { } assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) ensureAllEventsConsumed() } @@ -1370,7 +1363,7 @@ class MessageComposerPresenterTest { @Test fun `present - when there is a draft for reply, it is restored`() = runTest { - val loadDraftLambda = lambdaRecorder { _, _ -> + val loadDraftLambda = lambdaRecorder { _, _, _ -> ComposerDraft( plainText = A_MESSAGE, htmlText = null, @@ -1386,7 +1379,7 @@ class MessageComposerPresenterTest { val timeline = FakeTimeline().apply { this.loadReplyDetailsLambda = loadReplyDetailsLambda } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, ) @@ -1395,12 +1388,12 @@ class MessageComposerPresenterTest { room = room, draftService = composerDraftService, permalinkBuilder = permalinkBuilder, - coroutineScope = this ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - awaitFirstItem().also { state -> + skipItems(2) + awaitItem().also { state -> assertThat(state.showTextFormatting).isFalse() assertThat(state.mode).isEqualTo(aReplyMode()) assertThat(state.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_MESSAGE) @@ -1408,7 +1401,7 @@ class MessageComposerPresenterTest { } assert(loadDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(false)) + .with(value(A_ROOM_ID), value(null), value(false)) assert(loadReplyDetailsLambda) .isCalledOnce() @@ -1420,11 +1413,11 @@ class MessageComposerPresenterTest { @Test fun `present - when save draft event is invoked and composer is empty then service is called with null draft`() = runTest { - val saveDraftLambda = lambdaRecorder { _, _, _ -> } + val saveDraftLambda = lambdaRecorder { _, _, _, _ -> } val composerDraftService = FakeComposerDraftService().apply { this.saveDraftLambda = saveDraftLambda } - val presenter = createPresenter(draftService = composerDraftService, coroutineScope = this) + val presenter = createPresenter(draftService = composerDraftService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -1433,13 +1426,13 @@ class MessageComposerPresenterTest { advanceUntilIdle() assert(saveDraftLambda) .isCalledOnce() - .with(value(A_ROOM_ID), value(null), value(false)) + .with(value(A_ROOM_ID), value(null), value(null), value(false)) } } @Test fun `present - when save draft event is invoked and composer is not empty then service is called`() = runTest { - val saveDraftLambda = lambdaRecorder { _, _, _ -> } + val saveDraftLambda = lambdaRecorder { _, _, _, _ -> } val composerDraftService = FakeComposerDraftService().apply { this.saveDraftLambda = saveDraftLambda } @@ -1448,7 +1441,6 @@ class MessageComposerPresenterTest { isRichTextEditorEnabled = false, draftService = composerDraftService, permalinkBuilder = permalinkBuilder, - coroutineScope = this ) moleculeFlow(RecompositionMode.Immediate) { val state = presenter.present() @@ -1487,27 +1479,32 @@ class MessageComposerPresenterTest { .withSequence( listOf( value(A_ROOM_ID), + value(null), value(ComposerDraft(plainText = A_MESSAGE, htmlText = null, draftType = ComposerDraftType.NewMessage)), value(false) ), listOf( value(A_ROOM_ID), + value(null), value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.NewMessage)), value(false) ), listOf( value(A_ROOM_ID), + value(null), value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.NewMessage)), // The volatile draft created when switching to edit mode. value(true) ), listOf( value(A_ROOM_ID), + value(null), value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.Edit(AN_EVENT_ID))), value(false) ), listOf( value(A_ROOM_ID), + value(null), // When moving from edit mode, text composer is cleared, so the draft is null value(null), value(false) @@ -1524,28 +1521,31 @@ class MessageComposerPresenterTest { return normalState } - private fun createPresenter( - coroutineScope: CoroutineScope, - room: MatrixRoom = FakeMatrixRoom( + private fun TestScope.createPresenter( + room: JoinedRoom = FakeJoinedRoom( typingNoticeResult = { Result.success(Unit) } ), navigator: MessagesNavigator = FakeMessagesNavigator(), - pickerProvider: PickerProvider = this.pickerProvider, - featureFlagService: FeatureFlagService = this.featureFlagService, + pickerProvider: PickerProvider = this@MessageComposerPresenterTest.pickerProvider, + featureFlagService: FeatureFlagService = this@MessageComposerPresenterTest.featureFlagService, + locationService: LocationService = FakeLocationService(true), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), - mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor, - snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher, + mediaPreProcessor: MediaPreProcessor = this@MessageComposerPresenterTest.mediaPreProcessor, + snackbarDispatcher: SnackbarDispatcher = this@MessageComposerPresenterTest.snackbarDispatcher, permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(), permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(), permalinkParser: PermalinkParser = FakePermalinkParser(), - mentionSpanProvider: MentionSpanProvider = MentionSpanProvider(permalinkParser), - roomMemberProfilesCache: RoomMemberProfilesCache = RoomMemberProfilesCache(), + mentionSpanProvider: MentionSpanProvider = MentionSpanProvider( + permalinkParser = permalinkParser, + mentionSpanFormatter = FakeMentionSpanFormatter(), + mentionSpanTheme = MentionSpanTheme(A_USER_ID) + ), textPillificationHelper: TextPillificationHelper = FakeTextPillificationHelper(), isRichTextEditorEnabled: Boolean = true, draftService: ComposerDraftService = FakeComposerDraftService(), ) = MessageComposerPresenter( navigator = navigator, - appCoroutineScope = coroutineScope, + sessionCoroutineScope = this, room = room, mediaPickerProvider = pickerProvider, featureFlagService = featureFlagService, @@ -1554,6 +1554,7 @@ class MessageComposerPresenterTest { mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()), snackbarDispatcher = snackbarDispatcher, analyticsService = analyticsService, + locationService = locationService, messageComposerContext = DefaultMessageComposerContext(), richTextEditorStateFactory = TestRichTextEditorStateFactory(), roomAliasSuggestionsDataSource = FakeRoomAliasSuggestionsDataSource(), @@ -1564,7 +1565,6 @@ class MessageComposerPresenterTest { draftService = draftService, mentionSpanProvider = mentionSpanProvider, pillificationHelper = textPillificationHelper, - roomMemberProfilesCache = roomMemberProfilesCache, suggestionsProcessor = SuggestionsProcessor(), ).apply { isTesting = true @@ -1572,8 +1572,7 @@ class MessageComposerPresenterTest { } private suspend fun ReceiveTurbine.awaitFirstItem(): T { - // Skip 2 item if Mentions feature is enabled, else 1 - skipItems(if (FeatureFlags.Mentions.defaultValue(aBuildMeta())) 2 else 1) + skipItems(1) return awaitItem() } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessorTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessorTest.kt index 20581ddab58..6d7b4f8c7ca 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessorTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessorTest.kt @@ -9,7 +9,7 @@ package io.element.android.features.messages.impl.messagecomposer.suggestions import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_USER_ID @@ -35,7 +35,7 @@ class SuggestionsProcessorTest { fun `processing null suggestion will return empty suggestion`() = runTest { val result = suggestionsProcessor.process( suggestion = null, - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember())), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember())), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, @@ -47,7 +47,7 @@ class SuggestionsProcessorTest { fun `processing Command will return empty suggestion`() = runTest { val result = suggestionsProcessor.process( suggestion = aCommandSuggestion, - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember())), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember())), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, @@ -59,7 +59,7 @@ class SuggestionsProcessorTest { fun `processing Custom will return empty suggestion`() = runTest { val result = suggestionsProcessor.process( suggestion = aCustomSuggestion, - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember())), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember())), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, @@ -71,7 +71,7 @@ class SuggestionsProcessorTest { fun `processing Mention suggestion with not loaded members will return empty suggestion`() = runTest { val result = suggestionsProcessor.process( suggestion = aMentionSuggestion(""), - roomMembersState = MatrixRoomMembersState.Unknown, + roomMembersState = RoomMembersState.Unknown, roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, @@ -83,7 +83,7 @@ class SuggestionsProcessorTest { fun `processing Mention suggestion with no members will return empty suggestion`() = runTest { val result = suggestionsProcessor.process( suggestion = aMentionSuggestion(""), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), + roomMembersState = RoomMembersState.Ready(persistentListOf()), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, @@ -95,7 +95,7 @@ class SuggestionsProcessorTest { fun `processing Room suggestion with no aliases will return empty suggestion`() = runTest { val result = suggestionsProcessor.process( suggestion = aRoomSuggestion(""), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), + roomMembersState = RoomMembersState.Ready(persistentListOf()), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, @@ -108,7 +108,7 @@ class SuggestionsProcessorTest { val aRoomSummary = aRoomSummary(canonicalAlias = A_ROOM_ALIAS) val result = suggestionsProcessor.process( suggestion = aRoomSuggestion("ALI"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), + roomMembersState = RoomMembersState.Ready(persistentListOf()), roomAliasSuggestions = listOf( RoomAliasSuggestion( roomAlias = A_ROOM_ALIAS, @@ -133,11 +133,11 @@ class SuggestionsProcessorTest { } @Test - fun `processing Room suggestion with aliases will return a suggestion`() = runTest { + fun `processing Room suggestion with aliases will return a suggestion when matching on alias`() = runTest { val aRoomSummary = aRoomSummary(canonicalAlias = A_ROOM_ALIAS) val result = suggestionsProcessor.process( suggestion = aRoomSuggestion("ali"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), + roomMembersState = RoomMembersState.Ready(persistentListOf()), roomAliasSuggestions = listOf( RoomAliasSuggestion( roomAlias = A_ROOM_ALIAS, @@ -166,12 +166,61 @@ class SuggestionsProcessorTest { val aRoomSummary = aRoomSummary(canonicalAlias = A_ROOM_ALIAS) val result = suggestionsProcessor.process( suggestion = aRoomSuggestion("tot"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), + roomMembersState = RoomMembersState.Ready(persistentListOf()), roomAliasSuggestions = listOf( RoomAliasSuggestion( roomAlias = A_ROOM_ALIAS, roomId = aRoomSummary.roomId, - roomName = aRoomSummary.info.name, + roomName = "Element", + roomAvatarUrl = aRoomSummary.info.avatarUrl, + ) + ), + currentUserId = A_USER_ID, + canSendRoomMention = { true }, + ) + assertThat(result).isEmpty() + } + + @Test + fun `processing Room suggestion will return a suggestion when matching on room name`() = runTest { + val aRoomSummary = aRoomSummary(canonicalAlias = A_ROOM_ALIAS) + val result = suggestionsProcessor.process( + suggestion = aRoomSuggestion("lement"), + roomMembersState = RoomMembersState.Ready(persistentListOf()), + roomAliasSuggestions = listOf( + RoomAliasSuggestion( + roomAlias = A_ROOM_ALIAS, + roomId = aRoomSummary.roomId, + roomName = "Element", + roomAvatarUrl = aRoomSummary.info.avatarUrl, + ) + ), + currentUserId = A_USER_ID, + canSendRoomMention = { true }, + ) + assertThat(result).isEqualTo( + listOf( + ResolvedSuggestion.Alias( + roomAlias = A_ROOM_ALIAS, + roomId = aRoomSummary.roomId, + roomName = "Element", + roomAvatarUrl = aRoomSummary.info.avatarUrl, + ) + ) + ) + } + + @Test + fun `processing Room suggestion will not return a suggestion when room has no name`() = runTest { + val aRoomSummary = aRoomSummary(canonicalAlias = A_ROOM_ALIAS) + val result = suggestionsProcessor.process( + suggestion = aRoomSuggestion("lement"), + roomMembersState = RoomMembersState.Ready(persistentListOf()), + roomAliasSuggestions = listOf( + RoomAliasSuggestion( + roomAlias = A_ROOM_ALIAS, + roomId = aRoomSummary.roomId, + roomName = null, roomAvatarUrl = aRoomSummary.info.avatarUrl, ) ), @@ -186,7 +235,7 @@ class SuggestionsProcessorTest { val aRoomMember = aRoomMember(userId = UserId("@alice:server.org"), displayName = null) val result = suggestionsProcessor.process( suggestion = aMentionSuggestion("ali"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember)), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember)), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, @@ -203,7 +252,7 @@ class SuggestionsProcessorTest { val aRoomMember = aRoomMember(userId = UserId("@alice:server.org"), displayName = null) val result = suggestionsProcessor.process( suggestion = aMentionSuggestion("ali"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember)), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember)), roomAliasSuggestions = emptyList(), currentUserId = UserId("@alice:server.org"), canSendRoomMention = { true }, @@ -216,7 +265,7 @@ class SuggestionsProcessorTest { val aRoomMember = aRoomMember(userId = UserId("@alice:server.org"), displayName = "alice") val result = suggestionsProcessor.process( suggestion = aMentionSuggestion("bo"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember)), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember)), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, @@ -229,7 +278,7 @@ class SuggestionsProcessorTest { val aRoomMember = aRoomMember(userId = UserId("@alice:server.org"), membership = RoomMembershipState.INVITE) val result = suggestionsProcessor.process( suggestion = aMentionSuggestion("ali"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember)), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember)), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, @@ -242,7 +291,7 @@ class SuggestionsProcessorTest { val aRoomMember = aRoomMember(userId = UserId("@alice:server.org"), displayName = "bob") val result = suggestionsProcessor.process( suggestion = aMentionSuggestion("bo"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember)), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember)), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, @@ -259,7 +308,7 @@ class SuggestionsProcessorTest { val aRoomMember = aRoomMember(userId = UserId("@alice:server.org"), displayName = "ro") val result = suggestionsProcessor.process( suggestion = aMentionSuggestion("ro"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember)), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember)), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, @@ -277,7 +326,7 @@ class SuggestionsProcessorTest { val aRoomMember = aRoomMember(userId = UserId("@alice:server.org"), displayName = "ro") val result = suggestionsProcessor.process( suggestion = aMentionSuggestion("ro"), - roomMembersState = MatrixRoomMembersState.Ready(persistentListOf(aRoomMember)), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember)), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { false }, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt index fdd01f5ad1e..f1fcd7e7873 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt @@ -12,14 +12,14 @@ import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProv import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2 -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.timeline.FakeTimeline @@ -54,15 +54,15 @@ class PinnedMessagesBannerPresenterTest { @Test fun `present - loading state`() = runTest { - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(FakeTimeline()) } + val room = FakeJoinedRoom( + createTimelineResult = { Result.success(FakeTimeline()) } ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) } val presenter = createPinnedMessagesBannerPresenter(room = room) presenter.test { skipItems(2) - val loadingState = awaitItem() + val loadingState = awaitItem() as PinnedMessagesBannerState.Loading assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1)) assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1) assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0) @@ -85,8 +85,8 @@ class PinnedMessagesBannerPresenterTest { ) ) ) - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) } + val room = FakeJoinedRoom( + createTimelineResult = { Result.success(pinnedEventsTimeline) } ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2))) } @@ -124,8 +124,8 @@ class PinnedMessagesBannerPresenterTest { ) ) ) - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) } + val room = FakeJoinedRoom( + createTimelineResult = { Result.success(pinnedEventsTimeline) } ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2))) } @@ -159,15 +159,16 @@ class PinnedMessagesBannerPresenterTest { @Test fun `present - timeline failed`() = runTest { - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.failure(Exception()) } + val room = FakeJoinedRoom( + createTimelineResult = { Result.failure(Exception()) } ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) } val presenter = createPinnedMessagesBannerPresenter(room = room) presenter.test { skipItems(2) - awaitItem().also { loadingState -> + awaitItem().also { state -> + val loadingState = state as PinnedMessagesBannerState.Loading assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1)) assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1) assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0) @@ -179,7 +180,7 @@ class PinnedMessagesBannerPresenterTest { } private fun TestScope.createPinnedMessagesBannerPresenter( - room: MatrixRoom = FakeMatrixRoom(), + room: JoinedRoom = FakeJoinedRoom(), itemFactory: PinnedMessagesBannerItemFactory = PinnedMessagesBannerItemFactory( coroutineDispatchers = testCoroutineDispatchers(), formatter = FakePinnedMessagesBannerFormatter( @@ -194,7 +195,8 @@ class PinnedMessagesBannerPresenterTest { syncService = syncService, featureFlagService = FakeFeatureFlagService( initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled) - ) + ), + dispatchers = testCoroutineDispatchers(), ) timelineProvider.launchIn(backgroundScope) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index 3799eb2c741..535b11c5220 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -12,6 +12,7 @@ import im.vector.app.features.analytics.plan.PinUnpinAction import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator +import io.element.android.features.messages.impl.link.aLinkState import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState @@ -19,14 +20,15 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.test.AN_EVENT_ID -import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_UNIQUE_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.timeline.FakeTimeline @@ -38,6 +40,7 @@ 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 io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -49,11 +52,13 @@ import org.junit.Test class PinnedMessagesListPresenterTest { @Test fun `present - initial state feature disabled`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, ) + ) val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = false) presenter.test { val initialState = awaitItem() @@ -64,13 +69,15 @@ class PinnedMessagesListPresenterTest { @Test fun `present - initial state feature enabled`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) } + ) val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) presenter.test { val initialState = awaitItem() @@ -81,14 +88,16 @@ class PinnedMessagesListPresenterTest { @Test fun `present - timeline failure state`() = runTest { - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.failure(RuntimeException()) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) - } + }, + createTimelineResult = { Result.failure(RuntimeException()) }, + ) val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) presenter.test { skipItems(3) @@ -100,14 +109,16 @@ class PinnedMessagesListPresenterTest { @Test fun `present - empty state`() = runTest { - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(FakeTimeline()) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf())) - } + }, + createTimelineResult = { Result.success(FakeTimeline()) }, + ) val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) presenter.test { skipItems(3) @@ -120,14 +131,16 @@ class PinnedMessagesListPresenterTest { @Test fun `present - filled state`() = runTest { val pinnedEventsTimeline = createPinnedMessagesTimeline() - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) - } + }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, + ) val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) presenter.test { skipItems(3) @@ -144,17 +157,19 @@ class PinnedMessagesListPresenterTest { @Test fun `present - unpin event`() = runTest { val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) } - val failureUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.failure(A_THROWABLE) } + val failureUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.failure(AN_EXCEPTION) } val pinnedEventsTimeline = createPinnedMessagesTimeline() val analyticsService = FakeAnalyticsService() - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) - } + }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, + ) val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true, analyticsService = analyticsService) presenter.test { skipItems(3) @@ -193,14 +208,16 @@ class PinnedMessagesListPresenterTest { this.onViewInTimelineClickLambda = onViewInTimelineClickLambda } val pinnedEventsTimeline = createPinnedMessagesTimeline() - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) - } + }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, + ) val presenter = createPinnedMessagesListPresenter(room = room, navigator = navigator, isFeatureEnabled = true) presenter.test { skipItems(3) @@ -222,14 +239,16 @@ class PinnedMessagesListPresenterTest { this.onShowEventDebugInfoClickLambda = onShowEventDebugInfoClickLambda } val pinnedEventsTimeline = createPinnedMessagesTimeline() - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) - } + }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, + ) val presenter = createPinnedMessagesListPresenter(room = room, navigator = navigator, isFeatureEnabled = true) presenter.test { skipItems(3) @@ -251,14 +270,16 @@ class PinnedMessagesListPresenterTest { this.onForwardEventClickLambda = onForwardEventClickLambda } val pinnedEventsTimeline = createPinnedMessagesTimeline() - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, canRedactOtherResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, ).apply { givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) - } + }, + createTimelineResult = { Result.success(pinnedEventsTimeline) }, + ) val presenter = createPinnedMessagesListPresenter(room = room, navigator = navigator, isFeatureEnabled = true) presenter.test { skipItems(3) @@ -292,7 +313,7 @@ class PinnedMessagesListPresenterTest { private fun TestScope.createPinnedMessagesListPresenter( navigator: PinnedMessagesListNavigator = FakePinnedMessagesListNavigator(), - room: MatrixRoom = FakeMatrixRoom(), + room: JoinedRoom = FakeJoinedRoom(), syncService: SyncService = FakeSyncService(), isFeatureEnabled: Boolean = true, analyticsService: AnalyticsService = FakeAnalyticsService(), @@ -302,7 +323,8 @@ class PinnedMessagesListPresenterTest { syncService = syncService, featureFlagService = FakeFeatureFlagService( initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled) - ) + ), + dispatchers = testCoroutineDispatchers(), ) timelineProvider.launchIn(backgroundScope) return PinnedMessagesListPresenter( @@ -313,8 +335,9 @@ class PinnedMessagesListPresenterTest { timelineProtectionPresenter = { aTimelineProtectionState() }, snackbarDispatcher = SnackbarDispatcher(), actionListPresenter = { anActionListState() }, + linkPresenter = { aLinkState() }, analyticsService = analyticsService, - appCoroutineScope = this, + sessionCoroutineScope = this, ) } } 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 3257a6b5039..5e9ad97f877 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 @@ -21,7 +21,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent -import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder @@ -29,6 +29,7 @@ import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.setSafeContent +import io.element.android.wysiwyg.link.Link import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -98,16 +99,18 @@ private fun AndroidComposeTestRule.setPinne state: PinnedMessagesListState, onBackClick: () -> Unit = EnsureNeverCalled(), onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), - onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), - onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), + onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), + onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(), + onLinkLongClick: (Link) -> Unit = EnsureNeverCalledWithParam(), ) { - setSafeContent { + setSafeContent(clearAndroidUiDispatcher = true) { PinnedMessagesListView( state = state, onBackClick = onBackClick, onEventClick = onEventClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenterTest.kt index 6406b1d64d9..abaa6a5653f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenterTest.kt @@ -15,10 +15,10 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher 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.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.test.runTest @@ -78,10 +78,10 @@ class ReportMessagePresenterTest { val reportContentResult = lambdaRecorder> { _, _, _ -> Result.success(Unit) } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( reportContentResult = reportContentResult ) - val presenter = createReportMessagePresenter(matrixRoom = room) + val presenter = createReportMessagePresenter(joinedRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -100,10 +100,10 @@ class ReportMessagePresenterTest { val reportContentResult = lambdaRecorder> { _, _, _ -> Result.success(Unit) } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( reportContentResult = reportContentResult ) - val presenter = createReportMessagePresenter(matrixRoom = room) + val presenter = createReportMessagePresenter(joinedRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -120,10 +120,10 @@ class ReportMessagePresenterTest { val reportContentResult = lambdaRecorder> { _, _, _ -> Result.failure(Exception("Failed to report content")) } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( reportContentResult = reportContentResult ) - val presenter = createReportMessagePresenter(matrixRoom = room) + val presenter = createReportMessagePresenter(joinedRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -141,11 +141,11 @@ class ReportMessagePresenterTest { private fun createReportMessagePresenter( inputs: ReportMessagePresenter.Inputs = ReportMessagePresenter.Inputs(AN_EVENT_ID, A_USER_ID), - matrixRoom: MatrixRoom = FakeMatrixRoom(), + joinedRoom: JoinedRoom = FakeJoinedRoom(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), ) = ReportMessagePresenter( inputs = inputs, - room = matrixRoom, + room = joinedRoom, snackbarDispatcher = snackbarDispatcher, ) } 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 2f4740dfc91..d2d59146741 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 @@ -11,9 +11,12 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode 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 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 @@ -23,25 +26,29 @@ import org.robolectric.RobolectricTestRunner class DefaultHtmlConverterProviderTest { @get:Rule val composeTestRule = createComposeRule() + private val provider = DefaultHtmlConverterProvider( + mentionSpanProvider = MentionSpanProvider( + permalinkParser = FakePermalinkParser(), + mentionSpanFormatter = FakeMentionSpanFormatter(), + mentionSpanTheme = MentionSpanTheme(A_USER_ID) + ) + ) + @Test fun `calling provide without calling Update first should throw an exception`() { - val provider = DefaultHtmlConverterProvider(mentionSpanProvider = MentionSpanProvider(FakePermalinkParser())) - - val exception = runCatching { provider.provide() }.exceptionOrNull() + val exception = runCatchingExceptions { provider.provide() }.exceptionOrNull() assertThat(exception).isInstanceOf(IllegalStateException::class.java) } @Test fun `calling provide after calling Update first should return an HtmlConverter`() { - val provider = DefaultHtmlConverterProvider(mentionSpanProvider = MentionSpanProvider(FakePermalinkParser())) composeTestRule.setContent { CompositionLocalProvider(LocalInspectionMode provides true) { - provider.Update(currentUserId = A_USER_ID) + provider.Update() } } - - val htmlConverter = runCatching { provider.provide() }.getOrNull() + val htmlConverter = runCatchingExceptions { provider.provide() }.getOrNull() assertThat(htmlConverter).isNotNull() } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt new file mode 100644 index 00000000000..6340ce56a5c --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt @@ -0,0 +1,55 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.impl.timeline + +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultMarkAsFullyReadTest { + @Test + fun `When room is not found, then no exception is thrown`() = runTest { + val markAsFullyRead = DefaultMarkAsFullyRead( + FakeMatrixClient( + sessionCoroutineScope = backgroundScope, + ).apply { + givenGetRoomResult(A_ROOM_ID, null) + } + ) + markAsFullyRead.invoke(A_ROOM_ID) + runCurrent() + } + + @Test + fun `When room is found, the expected method is invoked`() = runTest { + val markAsReadResult = lambdaRecorder> { Result.success(Unit) } + val baseRoom = FakeBaseRoom( + markAsReadResult = markAsReadResult + ) + val markAsFullyRead = DefaultMarkAsFullyRead( + FakeMatrixClient( + sessionCoroutineScope = backgroundScope, + ).apply { + givenGetRoomResult(A_ROOM_ID, baseRoom) + } + ) + markAsFullyRead.invoke(A_ROOM_ID) + runCurrent() + markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.FULLY_READ)) + baseRoom.assertDestroyed() + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt new file mode 100644 index 00000000000..895676a1260 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt @@ -0,0 +1,19 @@ +/* + * 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.messages.impl.timeline + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeMarkAsFullyRead( + private val invokeResult: (RoomId) -> Unit = { lambdaError() } +) : MarkAsFullyRead { + override fun invoke(roomId: RoomId) { + invokeResult(roomId) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt index a9c8f085d8a..ab183442f23 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.tests.testutils.lambda.lambdaError @@ -29,11 +29,11 @@ class TimelineControllerTest { fun `test switching between live and detached timeline`() = runTest { val liveTimeline = FakeTimeline(name = "live") val detachedTimeline = FakeTimeline(name = "detached") - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) } + createTimelineResult = { Result.success(detachedTimeline) } ) - val sut = TimelineController(matrixRoom) + val sut = TimelineController(joinedRoom) sut.activeTimelineFlow().test { awaitItem().also { state -> @@ -61,9 +61,9 @@ class TimelineControllerTest { val detachedTimeline1 = FakeTimeline(name = "detached 1") val detachedTimeline2 = FakeTimeline(name = "detached 2") var callNumber = 0 - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { + createTimelineResult = { callNumber++ when (callNumber) { 1 -> Result.success(detachedTimeline1) @@ -72,7 +72,7 @@ class TimelineControllerTest { } } ) - val sut = TimelineController(matrixRoom) + val sut = TimelineController(joinedRoom) sut.activeTimelineFlow().test { awaitItem().also { state -> @@ -97,10 +97,10 @@ class TimelineControllerTest { @Test fun `test switching to live when already in live should have no effect`() = runTest { val liveTimeline = FakeTimeline(name = "live") - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline ) - val sut = TimelineController(matrixRoom) + val sut = TimelineController(joinedRoom) sut.activeTimelineFlow().test { awaitItem().also { state -> assertThat(state).isEqualTo(liveTimeline) @@ -115,11 +115,11 @@ class TimelineControllerTest { fun `test closing the TimelineController should close the detached timeline`() = runTest { val liveTimeline = FakeTimeline(name = "live") val detachedTimeline = FakeTimeline(name = "detached") - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) } + createTimelineResult = { Result.success(detachedTimeline) } ) - val sut = TimelineController(matrixRoom) + val sut = TimelineController(joinedRoom) sut.activeTimelineFlow().test { awaitItem().also { state -> assertThat(state).isEqualTo(liveTimeline) @@ -144,10 +144,10 @@ class TimelineControllerTest { ) ) ) - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline ) - val sut = TimelineController(matrixRoom) + val sut = TimelineController(joinedRoom) assertThat(sut.timelineItems().first()).hasSize(1) } @@ -165,11 +165,11 @@ class TimelineControllerTest { val detachedTimeline = FakeTimeline(name = "detached").apply { sendMessageLambda = lambdaForDetached } - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) } + createTimelineResult = { Result.success(detachedTimeline) } ) - val sut = TimelineController(matrixRoom) + val sut = TimelineController(joinedRoom) sut.activeTimelineFlow().test { sut.focusOnEvent(AN_EVENT_ID) awaitItem().also { state -> @@ -190,11 +190,11 @@ class TimelineControllerTest { fun `test last forward pagination on a detached timeline should switch to live timeline`() = runTest { val liveTimeline = FakeTimeline(name = "live") val detachedTimeline = FakeTimeline(name = "detached") - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) } + createTimelineResult = { Result.success(detachedTimeline) } ) - val sut = TimelineController(matrixRoom) + val sut = TimelineController(joinedRoom) sut.activeTimelineFlow().test { awaitItem().also { state -> 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 60a52741429..2cc7d9764ad 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 @@ -29,8 +29,10 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.features.roomcall.api.aStandByCallState 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.UniqueId -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembersState +import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline @@ -40,10 +42,12 @@ import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent @@ -57,9 +61,11 @@ import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf @@ -75,7 +81,9 @@ import java.util.Date import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -@OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest { +@Suppress("LargeClass") +@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) +class TimelinePresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -118,9 +126,26 @@ import kotlin.time.Duration.Companion.seconds } } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) { + fun `present - on scroll finished mark a room as read if the first visible index is 0 - read private`() { + `present - on scroll finished mark a room as read if the first visible index is 0`( + isSendPublicReadReceiptsEnabled = false, + expectedReceiptType = ReceiptType.READ_PRIVATE, + ) + } + + @Test + fun `present - on scroll finished mark a room as read if the first visible index is 0 - read`() { + `present - on scroll finished mark a room as read if the first visible index is 0`( + isSendPublicReadReceiptsEnabled = true, + expectedReceiptType = ReceiptType.READ, + ) + } + + private fun `present - on scroll finished mark a room as read if the first visible index is 0`( + isSendPublicReadReceiptsEnabled: Boolean, + expectedReceiptType: ReceiptType, + ) = runTest(StandardTestDispatcher()) { val timeline = FakeTimeline( timelineItems = flowOf( listOf( @@ -128,11 +153,15 @@ import kotlin.time.Duration.Companion.seconds ) ) ) - val room = FakeMatrixRoom( + val markAsReadResult = lambdaRecorder> { Result.success(Unit) } + val room = FakeJoinedRoom( liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) }, + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + markAsReadResult = markAsReadResult, + ) ) - val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false) + val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled) val presenter = createTimelinePresenter( timeline = timeline, room = room, @@ -144,11 +173,32 @@ import kotlin.time.Duration.Companion.seconds val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) runCurrent() - assertThat(room.markAsReadCalls).isNotEmpty() + assert(markAsReadResult) + .isCalledOnce() + .with(value(expectedReceiptType)) cancelAndIgnoreRemainingEvents() } } + @Test + fun `present - once presenter is disposed, room is marked as fully read`() = runTest { + val invokeResult = lambdaRecorder { } + val presenter = createTimelinePresenter( + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + ) + ), + markAsFullyRead = FakeMarkAsFullyRead( + invokeResult = invokeResult, + ) + ) + presenter.test { + awaitFirstItem() + } + invokeResult.assertions().isCalledOnce().with(value(A_ROOM_ID)) + } + @Test fun `present - on scroll finished send read receipt if an event is before the index`() = runTest { val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType -> @@ -481,10 +531,10 @@ import kotlin.time.Duration.Companion.seconds val liveTimeline = FakeTimeline( timelineItems = flowOf(emptyList()) ) - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( liveTimeline = liveTimeline, - timelineFocusedOnEventResult = { Result.success(detachedTimeline) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, + createTimelineResult = { Result.success(detachedTimeline) }, + baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }), ) val presenter = createTimelinePresenter( room = room, @@ -523,7 +573,7 @@ import kotlin.time.Duration.Companion.seconds process(listOf(aMessageEvent(eventId = AN_EVENT_ID))) } val presenter = createTimelinePresenter( - room = FakeMatrixRoom( + room = FakeJoinedRoom( liveTimeline = FakeTimeline( timelineItems = flowOf( listOf( @@ -534,7 +584,7 @@ import kotlin.time.Duration.Companion.seconds ) ) ), - canUserSendMessageResult = { _, _ -> Result.success(true) }, + baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }), ), timelineItemIndexer = timelineItemIndexer, ) @@ -557,12 +607,12 @@ import kotlin.time.Duration.Companion.seconds @Test fun `present - focus on event error case`() = runTest { val presenter = createTimelinePresenter( - room = FakeMatrixRoom( + room = FakeJoinedRoom( liveTimeline = FakeTimeline( timelineItems = flowOf(emptyList()), ), - timelineFocusedOnEventResult = { Result.failure(Throwable("An error")) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, + createTimelineResult = { Result.failure(RuntimeException("An error")) }, + baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }), ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -628,11 +678,11 @@ import kotlin.time.Duration.Companion.seconds ) ) ) - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) }, + baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }), ).apply { - givenRoomMembersState(MatrixRoomMembersState.Unknown) + givenRoomMembersState(RoomMembersState.Unknown) } val avatarUrl = "https://domain.com/avatar.jpg" @@ -647,7 +697,7 @@ import kotlin.time.Duration.Companion.seconds assertThat(event.readReceiptState.receipts.first().avatarData.url).isNull() room.givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( persistentListOf(aRoomMember(userId = A_USER_ID, avatarUrl = avatarUrl)) ) ) @@ -657,15 +707,82 @@ import kotlin.time.Duration.Companion.seconds } } + @Test + fun `present - timeline room info includes predecessor room when room has predecessor`() = runTest { + val predecessorRoomId = RoomId("!predecessor:server.org") + val predecessorEventId = EventId("\$predecessorEvent:server.org") + val predecessorRoom = PredecessorRoom( + roomId = predecessorRoomId, + lastEventId = predecessorEventId + ) + + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + predecessorRoomResult = { predecessorRoom } + ), + ) + + val presenter = createTimelinePresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + assertThat(initialState.timelineRoomInfo.predecessorRoom).isNotNull() + assertThat(initialState.timelineRoomInfo.predecessorRoom?.roomId).isEqualTo(predecessorRoomId) + assertThat(initialState.timelineRoomInfo.predecessorRoom?.lastEventId).isEqualTo(predecessorEventId) + } + } + + @Test + fun `present - timeline room info no predecessor`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + predecessorRoomResult = { null } + ), + ) + val presenter = createTimelinePresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + assertThat(initialState.timelineRoomInfo.predecessorRoom).isNull() + } + } + + @Test + fun `present - timeline event navigate to room`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + ), + ) + val onNavigateToRoomLambda = lambdaRecorder {} + val navigator = FakeMessagesNavigator( + onNavigateToRoomLambda = onNavigateToRoomLambda + ) + val presenter = createTimelinePresenter(room = room, messagesNavigator = navigator) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvents.NavigateToRoom(A_ROOM_ID)) + assert(onNavigateToRoomLambda) + .isCalledOnce() + .with( + value(A_ROOM_ID) + ) + } + } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { return awaitItem() } private fun TestScope.createTimelinePresenter( timeline: Timeline = FakeTimeline(), - room: FakeMatrixRoom = FakeMatrixRoom( + room: FakeJoinedRoom = FakeJoinedRoom( liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) } + baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }), ), redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), @@ -673,12 +790,13 @@ import kotlin.time.Duration.Companion.seconds sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), + markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), room = room, dispatchers = testCoroutineDispatchers(), - appScope = this, + sessionCoroutineScope = this, navigator = messagesNavigator, redactedVoiceMessageManager = redactedVoiceMessageManager, endPollAction = endPollAction, @@ -689,6 +807,7 @@ import kotlin.time.Duration.Companion.seconds resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, typingNotificationPresenter = { aTypingNotificationState() }, roomCallStatePresenter = { aStandByCallState() }, + markAsFullyRead = markAsFullyRead, ) } } 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 61e742f8726..c4a2351990a 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 @@ -11,18 +11,22 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.messages.impl.timeline.components.aCriticalShield import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState +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.UserId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam @@ -30,7 +34,9 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.setSafeContent +import io.element.android.wysiwyg.link.Link import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -114,8 +120,6 @@ class TimelineViewTest { rule.onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertList( listOf( - TimelineEvents.OnScrollFinished(0), - TimelineEvents.OnScrollFinished(0), TimelineEvents.OnScrollFinished(0), TimelineEvents.ShowShieldDialog(MessageShield.UnverifiedIdentity(true)), ) @@ -135,13 +139,44 @@ class TimelineViewTest { rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(TimelineEvents.HideShieldDialog) } + + @Test + fun `scrolling near to the start of the loaded items triggers a pre-fetch`() { + val eventsRecorder = EventsRecorder() + val items = List(200) { + aTimelineItemEvent( + eventId = EventId("\$event_$it"), + content = aTimelineItemUnknownContent(), + ) + }.toPersistentList() + + rule.setTimelineView( + state = aTimelineState( + timelineItems = items, + eventSink = eventsRecorder, + focusedEventIndex = -1, + isLive = false, + ), + ) + + rule.onNodeWithTag("timeline").performScrollToIndex(180) + + rule.mainClock.advanceTimeBy(1000) + + eventsRecorder.assertList( + listOf( + TimelineEvents.OnScrollFinished(firstIndex = 0), + TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS), + ) + ) + } } private fun AndroidComposeTestRule.setTimelineView( state: TimelineState, timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), - onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), - onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), + onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), + onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(), onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onMessageLongClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onSwipeToReply: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), @@ -152,7 +187,7 @@ private fun AndroidComposeTestRule.setTimel onJoinCallClick: () -> Unit = EnsureNeverCalled(), forceJumpToBottomVisibility: Boolean = false, ) { - setSafeContent { + setSafeContent(clearAndroidUiDispatcher = true) { TimelineView( state = state, timelineProtectionState = timelineProtectionState, 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 b89a3830bee..3935d6d6156 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 @@ -47,7 +47,10 @@ class TimelineItemPollViewTest { ) } val answer = content.answerItems[answerIndex].answer - rule.onNode(hasText(answer.text)).performClick() + rule.onNode( + matcher = hasText(answer.text), + useUnmergedTree = true, + ).performClick() eventsRecorder.assertSingle(TimelineEvents.SelectPollAnswer(content.eventId!!, answer.id)) } 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 fd081ac4897..ee52aff5789 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 @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline.components.event import android.text.SpannableString +import android.text.SpannedString import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule @@ -18,16 +19,22 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent +import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias 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_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.room.aRoomMember -import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache +import io.element.android.libraries.matrix.ui.messages.RoomNamesCache +import io.element.android.libraries.textcomposer.mentions.DefaultMentionSpanUpdater +import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanUpdater import io.element.android.libraries.textcomposer.mentions.MentionSpan +import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme +import io.element.android.libraries.textcomposer.mentions.MentionSpanUpdater +import io.element.android.libraries.textcomposer.mentions.MentionType import io.element.android.libraries.textcomposer.mentions.getMentionSpans +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.wysiwyg.view.spans.CustomMentionSpan import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest @@ -40,101 +47,100 @@ import org.junit.runner.RunWith 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`() = runTest { val charSequence = "Hello @alice:example.com" - - val result = rule.getText(aTextContentWithFormattedBody(charSequence)) + val mentionSpanUpdater = aMentionSpanUpdater() + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans()).isEmpty() + assert(formatLambda).isNeverCalled() } @Test fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runTest { val charSequence = SpannableString("Hello @alice:example.com") - - val result = rule.getText(aTextContentWithFormattedBody(charSequence)) + val mentionSpanUpdater = aMentionSpanUpdater() + 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`() = runTest { val charSequence = "Hello @alice:example.com" - - val result = rule.getText(aTextContentWithFormattedBody(body = charSequence, formattedBody = null)) + val mentionSpanUpdater = aMentionSpanUpdater() + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null)) assertThat(result.getMentionSpans()).isEmpty() assertThat(result.toString()).isEqualTo(charSequence) + assert(formatLambda).isNeverCalled() } @Test - fun `getTextWithResolvedMentions - with Room mention does nothing`() = runTest { + fun `getTextWithResolvedMentions - with Room mention format correctly`() = runTest { + val mentionType = MentionType.Room(roomIdOrAlias = A_ROOM_ID_2.toRoomIdOrAlias()) val charSequence = buildSpannedString { append("Hello ") - inSpans(aMentionSpan(rawValue = A_ROOM_ID_2.value, type = MentionSpan.Type.ROOM)) { + inSpans(MentionSpan(mentionType)) { append(A_ROOM_ID.value) } } + val mentionSpanUpdater = aMentionSpanUpdater() + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) - val result = rule.getText(aTextContentWithFormattedBody(charSequence)) - - assertThat(result.getMentionSpans().firstOrNull()?.text).isEmpty() + val expectedDisplayText = mentionType.toString() + assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) assertThat(result).isEqualTo(charSequence) + assert(formatLambda).isCalledOnce() } @Test fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runTest { + val mentionType = MentionType.User(userId = A_USER_ID) val charSequence = buildSpannedString { append("Hello ") - inSpans(aMentionSpan(rawValue = A_USER_ID.value)) { + inSpans(MentionSpan(mentionType)) { append("@NotAlice") } } + val mentionSpanUpdater = aMentionSpanUpdater() + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) - val result = rule.getText(aTextContentWithFormattedBody(charSequence)) - - assertThat(result.getMentionSpans().firstOrNull()?.text).isEqualTo("alice") + val expectedDisplayText = mentionType.toString() + assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) + assert(formatLambda).isCalledOnce() } @Test fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runTest { + val mentionType = MentionType.User(userId = A_USER_ID) val charSequence = buildSpannedString { append("Hello ") - inSpans(CustomMentionSpan(aMentionSpan(rawValue = A_USER_ID.value))) { + inSpans(CustomMentionSpan(MentionSpan(mentionType))) { append("@NotAlice") } } - - val result = rule.getText(aTextContentWithFormattedBody(charSequence)) - - assertThat(result.getMentionSpans().firstOrNull()?.text).isEqualTo("alice") - } - - @Test - fun `getTextWithResolvedMentions - replaces MentionSpan's text with user id if no display name is cached`() = runTest { - val charSequence = buildSpannedString { - append("Hello ") - inSpans(aMentionSpan(rawValue = A_USER_ID_2.value)) { - append("@NotAlice") - } - } - - val result = rule.getText(aTextContentWithFormattedBody(charSequence)) - - assertThat(result.getMentionSpans().firstOrNull()?.text).isEqualTo(A_USER_ID_2.value) + val mentionSpanUpdater = aMentionSpanUpdater() + val expectedDisplayText = mentionType.toString() + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) + assert(formatLambda).isCalledOnce() } private suspend fun AndroidComposeTestRule.getText( + mentionSpanUpdater: MentionSpanUpdater, content: TimelineItemTextBasedContent, ): CharSequence { val completable = CompletableDeferred() setContent { - val roomMemberProfilesCache = RoomMemberProfilesCache().apply { - replace(listOf(aRoomMember(userId = A_USER_ID, displayName = A_USER_NAME))) - } CompositionLocalProvider( - LocalRoomMemberProfilesCache provides roomMemberProfilesCache, + LocalMentionSpanUpdater provides mentionSpanUpdater ) { completable.complete(getTextWithResolvedMentions(content = content)) } @@ -142,21 +148,20 @@ class TimelineTextViewTest { return completable.await() } - private fun aMentionSpan( - rawValue: String, - text: String = "", - type: MentionSpan.Type = MentionSpan.Type.USER - ) = MentionSpan( - text = text, - rawValue = rawValue, - type = type, - ) + private fun aMentionSpanUpdater(): MentionSpanUpdater { + return DefaultMentionSpanUpdater( + formatter = mentionSpanFormatter, + theme = mentionSpanTheme, + roomMemberProfilesCache = RoomMemberProfilesCache(), + roomNamesCache = RoomNamesCache(), + ) + } private fun aTextContentWithFormattedBody(formattedBody: CharSequence?, body: String = "") = TimelineItemTextContent( body = body, htmlDocument = null, - formattedBody = formattedBody, + formattedBody = formattedBody ?: SpannedString(body), isEdited = false ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenterTest.kt index ba9bc0cdd79..115c8a96909 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenterTest.kt @@ -12,12 +12,12 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembersState 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_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.tests.testutils.WarmUpRule import kotlinx.collections.immutable.persistentListOf @@ -32,8 +32,8 @@ class ReactionSummaryPresenterTest { private val aggregatedReaction = anAggregatedReaction(userId = A_USER_ID, key = "👍", isHighlighted = true) private val roomMember = aRoomMember(userId = A_USER_ID, avatarUrl = AN_AVATAR_URL, displayName = A_USER_NAME) private val summaryEvent = ReactionSummaryEvents.ShowReactionSummary(AN_EVENT_ID, listOf(aggregatedReaction), aggregatedReaction.key) - private val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember))) + private val room = FakeBaseRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(persistentListOf(roomMember))) } private val presenter = ReactionSummaryPresenter(room) 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 86937f0fea9..c6c8c7c1e35 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 @@ -89,9 +89,8 @@ class TimelineItemContentMessageFactoryTest { val expected = TimelineItemTextContent( body = "body", htmlDocument = null, - plainText = "body", isEdited = false, - formattedBody = null, + formattedBody = SpannedString("body"), ) assertThat(result).isEqualTo(expected) } @@ -123,9 +122,8 @@ class TimelineItemContentMessageFactoryTest { val expected = TimelineItemTextContent( body = "body", htmlDocument = null, - plainText = "body", isEdited = false, - formattedBody = null, + formattedBody = "body", ) assertThat(result).isEqualTo(expected) } @@ -141,9 +139,8 @@ class TimelineItemContentMessageFactoryTest { val expected = TimelineItemTextContent( body = "body", htmlDocument = null, - plainText = "body", isEdited = false, - formattedBody = null, + formattedBody = SpannedString("body"), ) assertThat(result).isEqualTo(expected) } @@ -159,7 +156,6 @@ class TimelineItemContentMessageFactoryTest { val expected = TimelineItemTextContent( body = "https://www.example.org", htmlDocument = null, - plainText = "https://www.example.org", isEdited = false, formattedBody = buildSpannedString { inSpans(URLSpan("https://www.example.org")) { @@ -222,7 +218,7 @@ class TimelineItemContentMessageFactoryTest { senderDisambiguatedDisplayName = "Bob", eventId = AN_EVENT_ID, ) - assertThat((result as TimelineItemTextContent).formattedBody).isNull() + assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(SpannedString("body")) } @Test @@ -636,8 +632,7 @@ class TimelineItemContentMessageFactoryTest { val expected = TimelineItemNoticeContent( body = "body", htmlDocument = null, - plainText = "body", - formattedBody = null, + formattedBody = SpannedString("body"), isEdited = false, ) assertThat(result).isEqualTo(expected) @@ -670,8 +665,7 @@ class TimelineItemContentMessageFactoryTest { val expected = TimelineItemEmoteContent( body = "* Bob body", htmlDocument = null, - plainText = "* Bob body", - formattedBody = null, + formattedBody = SpannedString("* Bob body"), isEdited = false, ) assertThat(result).isEqualTo(expected) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt index b55d9313af5..49f30716a86 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt @@ -8,12 +8,19 @@ package io.element.android.features.messages.impl.timeline.protection import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewService +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.test.AN_EVENT_ID -import io.element.android.libraries.preferences.api.store.AppPreferencesStore -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -32,11 +39,11 @@ class TimelineProtectionPresenterTest { } @Test - fun `present - protected`() = runTest { - val appPreferencesStore = InMemoryAppPreferencesStore(hideImagesAndVideos = true) - val presenter = createPresenter(appPreferencesStore) + fun `present - media preview value off`() = runTest { + val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Off, hideInviteAvatar = false) + val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig)) + val presenter = createPresenter(mediaPreviewService = mediaPreviewService) presenter.test { - skipItems(1) val initialState = awaitItem() assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf())) // ShowContent with null should have no effect. @@ -47,9 +54,43 @@ class TimelineProtectionPresenterTest { } } + @Test + fun `present - media preview value private in public room`() = runTest { + val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Private, hideInviteAvatar = false) + val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig)) + val room = FakeBaseRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Public)) + val presenter = createPresenter(mediaPreviewService = mediaPreviewService, room = room) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf())) + // ShowContent with null should have no effect. + initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null)) + initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID)) + val finalState = awaitItem() + assertThat(finalState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID))) + } + } + + @Test + fun `present - media preview value private in non public room`() = runTest { + val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Private, hideInviteAvatar = false) + val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig)) + val room = FakeBaseRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite)) + val presenter = createPresenter(mediaPreviewService = mediaPreviewService, room = room) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderAll) + // ShowContent with null should have no effect. + initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null)) + initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID)) + } + } + private fun createPresenter( - appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + room: BaseRoom = FakeBaseRoom(), + mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(), ) = TimelineProtectionPresenter( - appPreferencesStore = appPreferencesStore, + mediaPreviewService = mediaPreviewService, + room = room, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt index 505829772bd..5b1f7f8654f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt @@ -13,19 +13,20 @@ import app.cash.turbine.Event 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.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.RoomMembersState 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_ID_3 import io.element.android.libraries.matrix.test.A_USER_ID_4 -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.tests.testutils.WarmUpRule import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -50,12 +51,13 @@ class TypingNotificationPresenterTest { @Test fun `present - typing notification disabled`() = runTest { - val room = FakeMatrixRoom() + val typingMembersFlow = MutableStateFlow>(emptyList()) + val room = FakeJoinedRoom(roomTypingMembersFlow = typingMembersFlow) val sessionPreferencesStore = InMemorySessionPreferencesStore( - isRenderTypingNotificationsEnabled = false + isRenderTypingNotificationsEnabled = false, ) val presenter = createPresenter( - matrixRoom = room, + joinedRoom = room, sessionPreferencesStore = sessionPreferencesStore, ) moleculeFlow(RecompositionMode.Immediate) { @@ -65,7 +67,7 @@ class TypingNotificationPresenterTest { val initialState = awaitItem() assertThat(initialState.renderTypingNotifications).isFalse() assertThat(initialState.typingMembers).isEmpty() - room.givenRoomTypingMembers(listOf(A_USER_ID_2)) + typingMembersFlow.emit(listOf(A_USER_ID_2)) expectNoEvents() // Preferences changes sessionPreferencesStore.setRenderTypingNotifications(true) @@ -89,14 +91,15 @@ class TypingNotificationPresenterTest { @Test fun `present - state is updated when a member is typing, member is not known`() = runTest { - val room = FakeMatrixRoom() - val presenter = createPresenter(matrixRoom = room) + val typingMembersFlow = MutableStateFlow>(emptyList()) + val room = FakeJoinedRoom(roomTypingMembersFlow = typingMembersFlow) + val presenter = createPresenter(joinedRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.typingMembers).isEmpty() - room.givenRoomTypingMembers(listOf(A_USER_ID_2)) + typingMembersFlow.emit(listOf(A_USER_ID_2)) val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( @@ -105,7 +108,7 @@ class TypingNotificationPresenterTest { ) ) // User stops typing - room.givenRoomTypingMembers(emptyList()) + typingMembersFlow.emit(emptyList()) skipItems(1) val finalState = awaitItem() assertThat(finalState.typingMembers).isEmpty() @@ -115,9 +118,10 @@ class TypingNotificationPresenterTest { @Test fun `present - state is updated when a member is typing, member is known`() = runTest { val aKnownRoomMember = createKnownRoomMember(userId = A_USER_ID_2) - val room = FakeMatrixRoom().apply { + val typingMembersFlow = MutableStateFlow>(emptyList()) + val room = FakeJoinedRoom(roomTypingMembersFlow = typingMembersFlow).apply { givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( listOf( createKnownRoomMember(A_USER_ID), aKnownRoomMember, @@ -127,13 +131,13 @@ class TypingNotificationPresenterTest { ) ) } - val presenter = createPresenter(matrixRoom = room) + val presenter = createPresenter(joinedRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.typingMembers).isEmpty() - room.givenRoomTypingMembers(listOf(A_USER_ID_2)) + typingMembersFlow.emit(listOf(A_USER_ID_2)) val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( @@ -142,7 +146,7 @@ class TypingNotificationPresenterTest { ) ) // User stops typing - room.givenRoomTypingMembers(emptyList()) + typingMembersFlow.emit(emptyList()) skipItems(1) val finalState = awaitItem() assertThat(finalState.typingMembers).isEmpty() @@ -152,14 +156,15 @@ class TypingNotificationPresenterTest { @Test fun `present - state is updated when a member is typing, member is not known, then known`() = runTest { val aKnownRoomMember = createKnownRoomMember(A_USER_ID_2) - val room = FakeMatrixRoom() - val presenter = createPresenter(matrixRoom = room) + val typingMembersFlow = MutableStateFlow>(emptyList()) + val room = FakeJoinedRoom(roomTypingMembersFlow = typingMembersFlow) + val presenter = createPresenter(joinedRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.typingMembers).isEmpty() - room.givenRoomTypingMembers(listOf(A_USER_ID_2)) + typingMembersFlow.emit(listOf(A_USER_ID_2)) val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( @@ -169,7 +174,7 @@ class TypingNotificationPresenterTest { ) // User is getting known room.givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( listOf(aKnownRoomMember).toImmutableList() ) ) @@ -185,19 +190,20 @@ class TypingNotificationPresenterTest { @Test fun `present - reserveSpace becomes true once we get the first typing notification with room members`() = runTest { - val room = FakeMatrixRoom() - val presenter = createPresenter(matrixRoom = room) + val typingMembersFlow = MutableStateFlow>(emptyList()) + val room = FakeJoinedRoom(roomTypingMembersFlow = typingMembersFlow) + val presenter = createPresenter(joinedRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.typingMembers).isEmpty() - room.givenRoomTypingMembers(listOf(A_USER_ID_2)) + typingMembersFlow.emit(listOf(A_USER_ID_2)) skipItems(1) val updatedTypingState = awaitItem() assertThat(updatedTypingState.reserveSpace).isTrue() // User stops typing - room.givenRoomTypingMembers(emptyList()) + typingMembersFlow.emit(emptyList()) // Is still true for all future events val futureEvents = cancelAndConsumeRemainingEvents() for (event in futureEvents) { @@ -209,14 +215,14 @@ class TypingNotificationPresenterTest { } private fun createPresenter( - matrixRoom: MatrixRoom = FakeMatrixRoom().apply { + joinedRoom: JoinedRoom = FakeJoinedRoom().apply { givenRoomInfo(aRoomInfo(id = roomId, name = "")) }, sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore( isRenderTypingNotificationsEnabled = true ), ) = TypingNotificationPresenter( - room = matrixRoom, + room = joinedRoom, sessionPreferencesStore = sessionPreferencesStore, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt index 013ce22cc2d..0d571a24591 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt @@ -10,17 +10,21 @@ package io.element.android.features.messages.impl.utils import android.net.Uri import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser -import io.element.android.libraries.matrix.test.room.aRoomMember -import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache -import io.element.android.libraries.textcomposer.mentions.MentionSpan +import io.element.android.libraries.textcomposer.mentions.MentionSpanFormatter import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme +import io.element.android.libraries.textcomposer.mentions.MentionType import io.element.android.libraries.textcomposer.mentions.getMentionSpans import org.junit.Test import org.junit.runner.RunWith @@ -30,90 +34,201 @@ class DefaultTextPillificationHelperTest { @Test fun `pillify - adds pills for user ids`() { val text = "A @user:server.com" + val formatter = FakeMentionSpanFormatter() + val userId = UserId("@user:server.com") val helper = aTextPillificationHelper( - permalinkparser = FakePermalinkParser(result = { - PermalinkData.UserLink(UserId("@user:server.com")) + permalinkParser = FakePermalinkParser(result = { + PermalinkData.UserLink(userId) }), permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/@user:server.com") }), + mentionSpanFormatter = formatter, ) val pillified = helper.pillify(text) val mentionSpans = pillified.getMentionSpans() assertThat(mentionSpans).hasSize(1) - val mentionSpan = mentionSpans.firstOrNull() - assertThat(mentionSpan?.type).isEqualTo(MentionSpan.Type.USER) - assertThat(mentionSpan?.rawValue).isEqualTo("@user:server.com") - assertThat(mentionSpan?.text).isEqualTo("@user:server.com") + val mentionSpan = mentionSpans.first() + assertThat(mentionSpan.type).isInstanceOf(MentionType.User::class.java) + val userType = mentionSpan.type as MentionType.User + assertThat(userType.userId).isEqualTo(userId) + val formatted = formatter.formatDisplayText(MentionType.User(userId)) + assertThat(mentionSpan.displayText.toString()).isEqualTo(formatted) } @Test - fun `pillify - uses the cached display name for user mentions`() { - val text = "A @user:server.com" + fun `pillify - adds pills for room aliases`() { + val text = "A #room:server.com" + val roomAlias = RoomAlias("#room:server.com") + val formatter = FakeMentionSpanFormatter() val helper = aTextPillificationHelper( - permalinkparser = FakePermalinkParser(result = { - PermalinkData.UserLink(UserId("@user:server.com")) + permalinkParser = FakePermalinkParser(result = { + PermalinkData.RoomLink(RoomIdOrAlias.Alias(roomAlias)) }), - permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { - Result.success("https://matrix.to/#/@user:server.com") + permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { + Result.success("https://matrix.to/#/#room:server.com") }), - roomMemberProfilesCache = RoomMemberProfilesCache().apply { - replace(listOf(aRoomMember(userId = UserId("@user:server.com"), displayName = "Alice"))) - }, + mentionSpanFormatter = formatter, ) val pillified = helper.pillify(text) val mentionSpans = pillified.getMentionSpans() assertThat(mentionSpans).hasSize(1) - val mentionSpan = mentionSpans.firstOrNull() - assertThat(mentionSpan?.type).isEqualTo(MentionSpan.Type.USER) - assertThat(mentionSpan?.rawValue).isEqualTo("@user:server.com") - assertThat(mentionSpan?.text).isEqualTo("Alice") + val mentionSpan = mentionSpans.first() + assertThat(mentionSpan.type).isInstanceOf(MentionType.Room::class.java) + val roomType = mentionSpan.type as MentionType.Room + assertThat(roomType.roomIdOrAlias).isEqualTo(roomAlias.toRoomIdOrAlias()) + val formatted = formatter.formatDisplayText(MentionType.Room(roomAlias.toRoomIdOrAlias())) + assertThat(mentionSpan.displayText.toString()).isEqualTo(formatted) } @Test - fun `pillify - adds pills for room aliases`() { - val text = "A #room:server.com" + fun `pillify - adds pills for @room mentions`() { + val text = "An @room mention" + val formatter = FakeMentionSpanFormatter() val helper = aTextPillificationHelper( - permalinkparser = FakePermalinkParser(result = { - PermalinkData.RoomLink(RoomIdOrAlias.Alias(RoomAlias("#room:server.com"))) - }), - permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { - Result.success("https://matrix.to/#/#room:server.com") + permalinkParser = FakePermalinkParser(result = { + PermalinkData.FallbackLink(Uri.EMPTY) }), + mentionSpanFormatter = formatter, ) val pillified = helper.pillify(text) val mentionSpans = pillified.getMentionSpans() assertThat(mentionSpans).hasSize(1) - val mentionSpan = mentionSpans.firstOrNull() - assertThat(mentionSpan?.type).isEqualTo(MentionSpan.Type.ROOM) - assertThat(mentionSpan?.rawValue).isEqualTo("#room:server.com") - assertThat(mentionSpan?.text).isEqualTo("#room:server.com") + val mentionSpan = mentionSpans.first() + assertThat(mentionSpan.type).isEqualTo(MentionType.Everyone) + val formatted = formatter.formatDisplayText(MentionType.Everyone) + assertThat(mentionSpan.displayText.toString()).isEqualTo(formatted) } @Test - fun `pillify - adds pills for @room mentions`() { - val text = "An @room mention" - val helper = aTextPillificationHelper(permalinkparser = FakePermalinkParser(result = { - PermalinkData.FallbackLink(Uri.EMPTY) - })) + fun `pillify - adds pills for message permalinks`() { + val text = "Check this message: https://matrix.to/#/!roomid:server.com/$123" + val roomId = RoomId("!roomid:server.com") + val eventId = EventId("$123") + val formatter = FakeMentionSpanFormatter() + val helper = aTextPillificationHelper( + permalinkParser = FakePermalinkParser(result = { + PermalinkData.RoomLink( + roomIdOrAlias = RoomIdOrAlias.Id(roomId), + eventId = eventId + ) + }), + permalinkBuilder = FakePermalinkBuilder(), + mentionSpanFormatter = formatter, + ) val pillified = helper.pillify(text) val mentionSpans = pillified.getMentionSpans() assertThat(mentionSpans).hasSize(1) - val mentionSpan = mentionSpans.firstOrNull() - assertThat(mentionSpan?.type).isEqualTo(MentionSpan.Type.EVERYONE) - assertThat(mentionSpan?.rawValue).isEqualTo("@room") - assertThat(mentionSpan?.text).isEqualTo("@room") + val mentionSpan = mentionSpans.first() + assertThat(mentionSpan.type).isInstanceOf(MentionType.Message::class.java) + val messageType = mentionSpan.type as MentionType.Message + assertThat(messageType.roomIdOrAlias).isEqualTo(roomId.toRoomIdOrAlias()) + assertThat(messageType.eventId).isEqualTo(eventId) + val formatted = formatter.formatDisplayText(MentionType.Message(roomId.toRoomIdOrAlias(), eventId)) + assertThat(mentionSpan.displayText.toString()).isEqualTo(formatted) + } + + @Test + fun `pillify - with pillifyPermalinks false does not add pills for permalinks`() { + val text = "Check this message: https://matrix.to/#/!roomid:server.com/$123" + val roomId = RoomId("!roomid:server.com") + val eventId = EventId("$123") + val formatter = FakeMentionSpanFormatter() + val helper = aTextPillificationHelper( + permalinkParser = FakePermalinkParser(result = { + PermalinkData.RoomLink( + roomIdOrAlias = RoomIdOrAlias.Id(roomId), + eventId = eventId + ) + }), + permalinkBuilder = FakePermalinkBuilder(), + mentionSpanFormatter = formatter, + ) + val pillified = helper.pillify(text, pillifyPermalinks = false) + val mentionSpans = pillified.getMentionSpans() + assertThat(mentionSpans).isEmpty() + } + + @Test + fun `pillify - with pillifyPermalinks false still adds pills for matrix patterns`() { + val text = "A @user:server.com mention and a permalink https://matrix.to/#/!roomid:server.com/$123" + val userId = UserId("@user:server.com") + val formatter = FakeMentionSpanFormatter() + val helper = aTextPillificationHelper( + permalinkParser = FakePermalinkParser(result = { + PermalinkData.UserLink(userId) + }), + permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { + Result.success("https://matrix.to/#/@user:server.com") + }), + mentionSpanFormatter = formatter, + ) + val pillified = helper.pillify(text, pillifyPermalinks = false) + val mentionSpans = pillified.getMentionSpans() + assertThat(mentionSpans).hasSize(1) + val mentionSpan = mentionSpans.first() + assertThat(mentionSpan.type).isInstanceOf(MentionType.User::class.java) + val userType = mentionSpan.type as MentionType.User + assertThat(userType.userId).isEqualTo(userId) + } + + @Test + fun `pillify - with pillifyPermalinks true adds pills for both matrix patterns and permalinks`() { + val text = "A @user:server.com mention and a permalink https://matrix.to/#/!roomid:server.com/$123" + val userId = UserId("@user:server.com") + val roomId = RoomId("!roomid:server.com") + val eventId = EventId("$123") + val formatter = FakeMentionSpanFormatter() + val permalinkParser = FakePermalinkParser(result = { url -> + if (url.contains("matrix.to")) { + PermalinkData.RoomLink( + roomIdOrAlias = RoomIdOrAlias.Id(roomId), + eventId = eventId + ) + } else { + PermalinkData.UserLink(userId) + } + }) + val helper = aTextPillificationHelper( + permalinkParser = permalinkParser, + permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { + Result.success("https://matrix.to/#/@user:server.com") + }), + mentionSpanFormatter = formatter, + ) + val pillified = helper.pillify(text, pillifyPermalinks = true) + val mentionSpans = pillified.getMentionSpans() + assertThat(mentionSpans).hasSize(2) + + // Check that we have both a user mention and a message mention + val types = mentionSpans.map { it.type::class.java } + assertThat(types).contains(MentionType.User::class.java) + assertThat(types).contains(MentionType.Message::class.java) + + // Verify the user mention + val userMention = mentionSpans.first { it.type is MentionType.User }.type as MentionType.User + assertThat(userMention.userId).isEqualTo(userId) + + // Verify the message mention + val messageMention = mentionSpans.first { it.type is MentionType.Message }.type as MentionType.Message + assertThat(messageMention.roomIdOrAlias).isEqualTo(roomId.toRoomIdOrAlias()) + assertThat(messageMention.eventId).isEqualTo(eventId) } private fun aTextPillificationHelper( - permalinkparser: PermalinkParser = FakePermalinkParser(), + permalinkParser: PermalinkParser = FakePermalinkParser(), permalinkBuilder: FakePermalinkBuilder = FakePermalinkBuilder(), - mentionSpanProvider: MentionSpanProvider = MentionSpanProvider(permalinkparser), - roomMemberProfilesCache: RoomMemberProfilesCache = RoomMemberProfilesCache(), - ) = DefaultTextPillificationHelper( - mentionSpanProvider = mentionSpanProvider, - permalinkBuilder = permalinkBuilder, - permalinkParser = permalinkparser, - roomMemberProfilesCache = roomMemberProfilesCache, - ) + mentionSpanFormatter: MentionSpanFormatter = FakeMentionSpanFormatter(), + ): TextPillificationHelper { + val mentionSpanProvider = MentionSpanProvider( + permalinkParser = permalinkParser, + mentionSpanFormatter = mentionSpanFormatter, + mentionSpanTheme = MentionSpanTheme(A_USER_ID), + ) + return DefaultTextPillificationHelper( + mentionSpanProvider = mentionSpanProvider, + permalinkBuilder = permalinkBuilder, + permalinkParser = permalinkParser, + ) + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/FakeMentionSpanFormatter.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/FakeMentionSpanFormatter.kt new file mode 100644 index 00000000000..a8128bf6227 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/FakeMentionSpanFormatter.kt @@ -0,0 +1,19 @@ +/* + * 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.messages.impl.utils + +import io.element.android.libraries.textcomposer.mentions.MentionSpanFormatter +import io.element.android.libraries.textcomposer.mentions.MentionType + +class FakeMentionSpanFormatter( + private val formatLambda: (MentionType) -> CharSequence = { type -> type.toString() }, +) : MentionSpanFormatter { + override fun formatDisplayText(mentionType: MentionType): CharSequence { + return formatLambda(mentionType) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/FakeTextPillificationHelper.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/FakeTextPillificationHelper.kt index d8c62bca40e..bbc7c7a728e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/FakeTextPillificationHelper.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/FakeTextPillificationHelper.kt @@ -8,9 +8,9 @@ package io.element.android.features.messages.impl.utils class FakeTextPillificationHelper( - private val pillifyLambda: (CharSequence) -> CharSequence = { it } + private val pillifyLambda: (CharSequence, Boolean) -> CharSequence = { text, _ -> text } ) : TextPillificationHelper { - override fun pillify(text: CharSequence): CharSequence { - return pillifyLambda(text) + override fun pillify(text: CharSequence, pillifyPermalinks: Boolean): CharSequence { + return pillifyLambda(text, pillifyPermalinks) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index e9ffd00b23b..7d0ceb55272 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -21,8 +21,10 @@ import io.element.android.features.messages.impl.messagecomposer.aReplyMode import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.media.AudioInfo +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor @@ -44,6 +46,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList 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 @@ -60,14 +63,16 @@ class VoiceMessageComposerPresenterTest { ) private val analyticsService = FakeAnalyticsService() private val sendVoiceMessageResult = - lambdaRecorder, ProgressCallback?, Result> { _, _, _, _ -> + lambdaRecorder, ProgressCallback?, ReplyParameters?, Result> { _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } - private val matrixRoom = FakeMatrixRoom( - sendVoiceMessageResult = sendVoiceMessageResult + private val joinedRoom = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendVoiceMessageLambda = sendVoiceMessageResult + }, ) private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() } - private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom, InMemorySessionPreferencesStore()) + private val mediaSender = MediaSender(mediaPreProcessor, joinedRoom, InMemorySessionPreferencesStore()) private val messageComposerContext = FakeMessageComposerContext() companion object { @@ -294,7 +299,6 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState()) - val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isCalledOnce() @@ -316,7 +320,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) skipItems(1) // Sending state - + advanceUntilIdle() // Now reply with a voice message messageComposerContext.composerMode = aReplyMode() awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) @@ -652,7 +656,7 @@ class VoiceMessageComposerPresenterTest { permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(), ): VoiceMessageComposerPresenter { return VoiceMessageComposerPresenter( - this, + backgroundScope, voiceRecorder, analyticsService, mediaSender, diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/FakeHtmlConverterProvider.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/FakeHtmlConverterProvider.kt index 38f10071e98..b9ea0d24164 100644 --- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/FakeHtmlConverterProvider.kt +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/FakeHtmlConverterProvider.kt @@ -9,14 +9,13 @@ package io.element.android.features.messages.test.timeline import androidx.compose.runtime.Composable import io.element.android.features.messages.api.timeline.HtmlConverterProvider -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.wysiwyg.utils.HtmlConverter class FakeHtmlConverterProvider( private val transform: (String) -> CharSequence = { it }, ) : HtmlConverterProvider { @Composable - override fun Update(currentUserId: UserId) = Unit + override fun Update() = Unit override fun provide(): HtmlConverter { return object : HtmlConverter { diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt index f89d81685f2..3d8fc3a0aa9 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt @@ -33,7 +33,9 @@ class MigrationPresenter @Inject constructor( @Composable override fun present(): MigrationState { - val migrationStoreVersion by migrationStore.applicationMigrationVersion().collectAsState(initial = null) + val migrationStoreVersion by remember { + migrationStore.applicationMigrationVersion() + }.collectAsState(initial = null) var migrationAction: AsyncData by remember { mutableStateOf(AsyncData.Uninitialized) } // Uncomment this block to run the migration everytime diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04.kt index 3de2bb11e5e..6023663dac6 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04.kt @@ -9,6 +9,7 @@ package io.element.android.features.migration.impl.migrations import android.content.Context import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import javax.inject.Inject @@ -26,6 +27,6 @@ class AppMigration04 @Inject constructor( override val order: Int = 4 override suspend fun migrate() { - runCatching { context.getDatabasePath(NOTIFICATION_FILE_NAME).delete() } + runCatchingExceptions { context.getDatabasePath(NOTIFICATION_FILE_NAME).delete() } } } diff --git a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/NetworkMonitor.kt b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/NetworkMonitor.kt index d9a26dd5bbf..196eb893105 100644 --- a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/NetworkMonitor.kt +++ b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/NetworkMonitor.kt @@ -9,6 +9,14 @@ package io.element.android.features.networkmonitor.api import kotlinx.coroutines.flow.StateFlow +/** + * Monitors the network status of the device, providing the current network connectivity status as a flow. + * + * **Note:** network connectivity does not imply internet connectivity. The device can be connected to a network that can't reach the homeserver. + */ interface NetworkMonitor { + /** + * A flow containing the current network connectivity status. + */ val connectivity: StateFlow } diff --git a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/NetworkStatus.kt b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/NetworkStatus.kt index 59deb7a8d0a..aebcb5a1d13 100644 --- a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/NetworkStatus.kt +++ b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/NetworkStatus.kt @@ -7,7 +7,19 @@ package io.element.android.features.networkmonitor.api +/** + * Network connectivity status of the device. + * + * **Note:** this is *network* connectivity status, not *internet* connectivity status. + */ enum class NetworkStatus { - Online, - Offline + /** + * The device is connected to a network. + */ + Connected, + + /** + * The device is not connected to any networks. + */ + Disconnected } diff --git a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/Indicator.kt b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/Indicator.kt index 992e9bf53cf..95c46d3fd04 100644 --- a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/Indicator.kt +++ b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/Indicator.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -25,6 +24,8 @@ 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.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.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text @@ -37,24 +38,29 @@ internal fun Indicator( Row( modifier .fillMaxWidth() - .background(MaterialTheme.colorScheme.secondaryContainer) + .background(ElementTheme.colors.bgSubtlePrimary) .statusBarsPadding() .padding(vertical = 6.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - val tint = MaterialTheme.colorScheme.primary Icon( imageVector = CompoundIcons.Offline(), contentDescription = null, - tint = tint, + tint = ElementTheme.colors.iconPrimary, modifier = Modifier.size(16.sp.toDp()), ) Spacer(modifier = Modifier.width(8.dp)) Text( text = stringResource(CommonStrings.common_offline), style = ElementTheme.typography.fontBodyMdMedium, - color = tint, + color = ElementTheme.colors.textPrimary, ) } } + +@PreviewsDayNight +@Composable +internal fun IndicatorPreview() = ElementPreview { + Indicator() +} diff --git a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt index eb0d4a7429d..a69cf4bcbc6 100644 --- a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt +++ b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt @@ -12,7 +12,6 @@ package io.element.android.features.networkmonitor.impl import android.content.Context import android.net.ConnectivityManager import android.net.Network -import android.net.NetworkCapabilities import android.net.NetworkRequest import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.networkmonitor.api.NetworkMonitor @@ -20,6 +19,7 @@ import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.awaitClose @@ -39,6 +39,7 @@ import javax.inject.Inject @SingleIn(AppScope::class) class DefaultNetworkMonitor @Inject constructor( @ApplicationContext context: Context, + @AppCoroutineScope appCoroutineScope: CoroutineScope, ) : NetworkMonitor { private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java) @@ -55,20 +56,18 @@ class DefaultNetworkMonitor @Inject constructor( override fun onLost(network: Network) { if (activeNetworksCount.decrementAndGet() == 0) { - trySendBlocking(NetworkStatus.Offline) + trySendBlocking(NetworkStatus.Disconnected) } } override fun onAvailable(network: Network) { if (activeNetworksCount.incrementAndGet() > 0) { - trySendBlocking(NetworkStatus.Online) + trySendBlocking(NetworkStatus.Connected) } } } trySendBlocking(connectivityManager.activeNetworkStatus()) - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build() + val request = NetworkRequest.Builder().build() connectivityManager.registerNetworkCallback(request, callback) Timber.d("Subscribe") @@ -85,17 +84,6 @@ class DefaultNetworkMonitor @Inject constructor( .stateIn(appCoroutineScope, SharingStarted.WhileSubscribed(), connectivityManager.activeNetworkStatus()) private fun ConnectivityManager.activeNetworkStatus(): NetworkStatus { - return activeNetwork?.let { - getNetworkCapabilities(it)?.getNetworkStatus() - } ?: NetworkStatus.Offline - } - - private fun NetworkCapabilities.getNetworkStatus(): NetworkStatus { - val hasInternet = hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - return if (hasInternet) { - NetworkStatus.Online - } else { - NetworkStatus.Offline - } + return if (activeNetwork != null) NetworkStatus.Connected else NetworkStatus.Disconnected } } diff --git a/features/networkmonitor/test/src/main/kotlin/io/element/android/features/networkmonitor/test/FakeNetworkMonitor.kt b/features/networkmonitor/test/src/main/kotlin/io/element/android/features/networkmonitor/test/FakeNetworkMonitor.kt index 0566512df72..298e06aa5bd 100644 --- a/features/networkmonitor/test/src/main/kotlin/io/element/android/features/networkmonitor/test/FakeNetworkMonitor.kt +++ b/features/networkmonitor/test/src/main/kotlin/io/element/android/features/networkmonitor/test/FakeNetworkMonitor.kt @@ -11,6 +11,6 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import kotlinx.coroutines.flow.MutableStateFlow -class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Online) : NetworkMonitor { +class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Connected) : NetworkMonitor { override val connectivity = MutableStateFlow(initialStatus) } diff --git a/features/onboarding/api/build.gradle.kts b/features/onboarding/api/build.gradle.kts deleted file mode 100644 index 2079f2221bf..00000000000 --- a/features/onboarding/api/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ -plugins { - id("io.element.android-library") -} - -android { - namespace = "io.element.android.features.onboarding.api" -} - -dependencies { - implementation(projects.libraries.architecture) -} diff --git a/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt b/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt deleted file mode 100644 index 378176a2a70..00000000000 --- a/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.onboarding.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 - -interface OnBoardingEntryPoint : FeatureEntryPoint { - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder - - interface NodeBuilder { - fun callback(callback: Callback): NodeBuilder - fun build(): Node - } - - interface Callback : Plugin { - fun onSignUp() - fun onSignIn() - fun onSignInWithQrCode() - fun onReportProblem() - } -} diff --git a/features/onboarding/impl/build.gradle.kts b/features/onboarding/impl/build.gradle.kts deleted file mode 100644 index 13e37e0a540..00000000000 --- a/features/onboarding/impl/build.gradle.kts +++ /dev/null @@ -1,51 +0,0 @@ -import extension.setupAnvil - -/* - * Copyright 2022-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. - */ - -plugins { - id("io.element.android-compose-library") - id("kotlin-parcelize") -} - -android { - namespace = "io.element.android.features.onboarding.impl" - - testOptions { - unitTests { - isIncludeAndroidResources = true - } - } -} - -setupAnvil() - -dependencies { - implementation(projects.appconfig) - implementation(projects.libraries.core) - implementation(projects.libraries.androidutils) - implementation(projects.libraries.architecture) - implementation(projects.libraries.designsystem) - implementation(projects.libraries.featureflag.api) - implementation(projects.libraries.matrix.api) - implementation(projects.libraries.testtags) - implementation(projects.libraries.uiStrings) - api(projects.features.onboarding.api) - - testImplementation(libs.test.junit) - testImplementation(libs.androidx.compose.ui.test.junit) - testImplementation(libs.androidx.test.ext.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.robolectric) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.featureflag.test) - testImplementation(projects.tests.testutils) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) -} diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/DefaultOnBoardingEntryPoint.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/DefaultOnBoardingEntryPoint.kt deleted file mode 100644 index 9f31a10ab7b..00000000000 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/DefaultOnBoardingEntryPoint.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.onboarding.impl - -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.onboarding.api.OnBoardingEntryPoint -import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultOnBoardingEntryPoint @Inject constructor() : OnBoardingEntryPoint { - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): OnBoardingEntryPoint.NodeBuilder { - return object : OnBoardingEntryPoint.NodeBuilder { - val plugins = ArrayList() - - override fun callback(callback: OnBoardingEntryPoint.Callback): OnBoardingEntryPoint.NodeBuilder { - plugins += callback - return this - } - - override fun build(): Node { - return parentNode.createNode(buildContext, plugins) - } - } - } -} diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt deleted file mode 100644 index f26a6fdd78a..00000000000 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.onboarding.impl - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import com.bumble.appyx.core.plugin.plugins -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.onboarding.api.OnBoardingEntryPoint -import io.element.android.libraries.di.AppScope - -@ContributesNode(AppScope::class) -class OnBoardingNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: OnBoardingPresenter, -) : Node( - buildContext = buildContext, - plugins = plugins -) { - private fun onSignIn() { - plugins().forEach { it.onSignIn() } - } - - private fun onSignUp() { - plugins().forEach { it.onSignUp() } - } - - private fun onSignInWithQrCode() { - plugins().forEach { it.onSignInWithQrCode() } - } - - private fun onReportProblem() { - plugins().forEach { it.onReportProblem() } - } - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - OnBoardingView( - state = state, - modifier = modifier, - onSignIn = ::onSignIn, - onCreateAccount = ::onSignUp, - onSignInWithQrCode = ::onSignInWithQrCode, - onReportProblem = ::onReportProblem, - ) - } -} diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt deleted file mode 100644 index 8d33f603937..00000000000 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.onboarding.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import io.element.android.appconfig.OnBoardingConfig -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 javax.inject.Inject - -/** - * Note: this Presenter is ignored regarding code coverage because it cannot reach the coverage threshold. - * When this presenter get more code in it, please remove the ignore rule in the kover configuration. - */ -class OnBoardingPresenter @Inject constructor( - private val buildMeta: BuildMeta, - private val featureFlagService: FeatureFlagService, -) : Presenter { - @Composable - override fun present(): OnBoardingState { - val canLoginWithQrCode by produceState(initialValue = false) { - value = featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin) - } - return OnBoardingState( - productionApplicationName = buildMeta.productionApplicationName, - canLoginWithQrCode = canLoginWithQrCode, - canCreateAccount = OnBoardingConfig.CAN_CREATE_ACCOUNT, - ) - } -} diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt deleted file mode 100644 index 6ffb80c2bd0..00000000000 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.onboarding.impl - -data class OnBoardingState( - val productionApplicationName: String, - val canLoginWithQrCode: Boolean, - val canCreateAccount: Boolean, -) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt deleted file mode 100644 index 25dc697782c..00000000000 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.onboarding.impl - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -open class OnBoardingStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - anOnBoardingState(), - anOnBoardingState(canLoginWithQrCode = true), - anOnBoardingState(canCreateAccount = true), - anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true), - ) -} - -fun anOnBoardingState( - productionApplicationName: String = "Element", - canLoginWithQrCode: Boolean = false, - canCreateAccount: Boolean = false -) = OnBoardingState( - productionApplicationName = productionApplicationName, - canLoginWithQrCode = canLoginWithQrCode, - canCreateAccount = canCreateAccount -) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt deleted file mode 100644 index 2d84a618fc7..00000000000 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.onboarding.impl - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.QrCode -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.BiasAlignment -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 androidx.compose.ui.unit.sp -import io.element.android.compound.theme.ElementTheme -import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom -import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize -import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule -import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage -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 -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TextButton -import io.element.android.libraries.testtags.TestTags -import io.element.android.libraries.testtags.testTag -import io.element.android.libraries.ui.strings.CommonStrings - -// Refs: -// FTUE: -// - https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0 -// ElementX: -// - https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=1816-97419 -@Composable -fun OnBoardingView( - state: OnBoardingState, - onSignInWithQrCode: () -> Unit, - onSignIn: () -> Unit, - onCreateAccount: () -> Unit, - onReportProblem: () -> Unit, - modifier: Modifier = Modifier, -) { - OnBoardingPage( - modifier = modifier, - content = { - OnBoardingContent(state = state) - }, - footer = { - OnBoardingButtons( - state = state, - onSignInWithQrCode = onSignInWithQrCode, - onSignIn = onSignIn, - onCreateAccount = onCreateAccount, - onReportProblem = onReportProblem, - ) - } - ) -} - -@Composable -private fun OnBoardingContent(state: OnBoardingState) { - Box( - modifier = Modifier.fillMaxSize(), - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = BiasAlignment( - horizontalBias = 0f, - verticalBias = -0.4f - ) - ) { - ElementLogoAtom( - size = ElementLogoAtomSize.Large, - modifier = Modifier.padding(top = ElementLogoAtomSize.Large.shadowRadius / 2) - ) - } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = BiasAlignment( - horizontalBias = 0f, - verticalBias = 0.6f - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth(), - horizontalAlignment = CenterHorizontally, - ) { - Text( - text = stringResource(id = R.string.screen_onboarding_welcome_title), - color = ElementTheme.materialColors.primary, - style = ElementTheme.typography.fontHeadingLgBold, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(id = R.string.screen_onboarding_welcome_message, state.productionApplicationName), - color = ElementTheme.materialColors.secondary, - style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = 17.sp), - textAlign = TextAlign.Center - ) - } - } - } -} - -@Composable -private fun OnBoardingButtons( - state: OnBoardingState, - onSignInWithQrCode: () -> Unit, - onSignIn: () -> Unit, - onCreateAccount: () -> Unit, - onReportProblem: () -> Unit, -) { - ButtonColumnMolecule { - val signInButtonStringRes = if (state.canLoginWithQrCode || state.canCreateAccount) { - R.string.screen_onboarding_sign_in_manually - } else { - CommonStrings.action_continue - } - if (state.canLoginWithQrCode) { - Button( - text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code), - leadingIcon = IconSource.Vector(Icons.Default.QrCode), - onClick = onSignInWithQrCode, - modifier = Modifier.fillMaxWidth() - ) - } - Button( - text = stringResource(id = signInButtonStringRes), - onClick = onSignIn, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.onBoardingSignIn) - ) - if (state.canCreateAccount) { - TextButton( - text = stringResource(id = R.string.screen_onboarding_sign_up), - onClick = onCreateAccount, - modifier = Modifier - .fillMaxWidth() - ) - } - // Add a report problem text button. Use a Text since we need a special theme here. - Text( - modifier = Modifier - .padding(16.dp) - .clickable(onClick = onReportProblem), - text = stringResource(id = CommonStrings.common_report_a_problem), - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, - ) - } -} - -@PreviewsDayNight -@Composable -internal fun OnBoardingViewPreview( - @PreviewParameter(OnBoardingStateProvider::class) state: OnBoardingState -) = ElementPreview { - OnBoardingView( - state = state, - onSignInWithQrCode = {}, - onSignIn = {}, - onCreateAccount = {}, - onReportProblem = {}, - ) -} diff --git a/features/onboarding/impl/src/main/res/values-be/translations.xml b/features/onboarding/impl/src/main/res/values-be/translations.xml deleted file mode 100644 index ab1c5aba875..00000000000 --- a/features/onboarding/impl/src/main/res/values-be/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Увайсці ўручную" - "Увайсці з QR-кодам" - "Стварыць уліковы запіс" - "Сардэчна запрашаем у самы хуткі %1$s. Перавага ў хуткасці і прастаце." - "Сардэчна запрашаем у %1$s. Зараджаны, для хуткасці і прастаты." - "Будзьце ў сваім element" - diff --git a/features/onboarding/impl/src/main/res/values-bg/translations.xml b/features/onboarding/impl/src/main/res/values-bg/translations.xml deleted file mode 100644 index 67a3fe4a28f..00000000000 --- a/features/onboarding/impl/src/main/res/values-bg/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Влизане ръчно" - "Влизане с QR код" - "Създаване на акаунт" - "Добре дошли в най-бързия %1$s досега. Супер зареден за скорост и простота." - "Добре дошли в %1$s. Супер зареден за скорост и простота." - "Бъдете в стихията си" - diff --git a/features/onboarding/impl/src/main/res/values-cs/translations.xml b/features/onboarding/impl/src/main/res/values-cs/translations.xml deleted file mode 100644 index 057fdff4ee4..00000000000 --- a/features/onboarding/impl/src/main/res/values-cs/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Ruční přihlášení" - "Přihlásit se pomocí QR kódu" - "Vytvořit účet" - "Vítejte u dosud nejrychlejšího %1$su. Vylepšený pro rychlost a jednoduchost." - "Vítejte v %1$s. Vylepšený, pro rychlost a jednoduchost." - "Buďte ve svém živlu" - diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml deleted file mode 100644 index bd5e9c0c544..00000000000 --- a/features/onboarding/impl/src/main/res/values-de/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Manuell anmelden" - "Mit QR-Code anmelden" - "Konto erstellen" - "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" - diff --git a/features/onboarding/impl/src/main/res/values-el/translations.xml b/features/onboarding/impl/src/main/res/values-el/translations.xml deleted file mode 100644 index bdaf30f11b4..00000000000 --- a/features/onboarding/impl/src/main/res/values-el/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Σύνδεση χειροκίνητα" - "Συνδέσου με κωδικό QR" - "Δημιουργία λογαριασμού" - "Καλώς ήλθατε στο γρηγορότερο %1$s όλων των εποχών. Υπερτροφοδοτούμενο με ταχύτητα και απλότητα." - "Καλώς ήρθες στο %1$s. Υπερφορτισμένο, για ταχύτητα και απλότητα." - "Μείνε στο element σου" - diff --git a/features/onboarding/impl/src/main/res/values-es/translations.xml b/features/onboarding/impl/src/main/res/values-es/translations.xml deleted file mode 100644 index c794e3b9abb..00000000000 --- a/features/onboarding/impl/src/main/res/values-es/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Iniciar sesión manualmente" - "Iniciar sesión con un código QR" - "Crear cuenta" - "Bienvenido al %1$s más rápido de todos los tiempos. Diseñado para la velocidad y la simplicidad." - "Bienvenido a %1$s. Vitaminado, para mayor rapidez y sencillez." - "Siéntete en tu Elemento" - diff --git a/features/onboarding/impl/src/main/res/values-et/translations.xml b/features/onboarding/impl/src/main/res/values-et/translations.xml deleted file mode 100644 index 5242c6f69cf..00000000000 --- a/features/onboarding/impl/src/main/res/values-et/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Logi sisse käsitsi" - "Logi sisse QR-koodi alusel" - "Loo kasutajakonto" - "Läbi aegade kiireim ja mugavaim %1$s." - "Tere tulemast kasutama kiiret ja lihtsat suhtlusrakendust %1$s." - "Ole oma elemendis" - diff --git a/features/onboarding/impl/src/main/res/values-fa/translations.xml b/features/onboarding/impl/src/main/res/values-fa/translations.xml deleted file mode 100644 index e4fb34e5659..00000000000 --- a/features/onboarding/impl/src/main/res/values-fa/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "ورود دستی" - "ورود با کد QR" - "ایجاد حساب" - "به سریع‌ترین %1$s خوش آمدید. بازطرّاحی شده برای سرعت و سادگی." - "به %1$s خوش آمدید. بازطرّاحی شده برای سرعت و سادگی." - "در المنتتان باشید" - diff --git a/features/onboarding/impl/src/main/res/values-fi/translations.xml b/features/onboarding/impl/src/main/res/values-fi/translations.xml deleted file mode 100644 index fb34bae0481..00000000000 --- a/features/onboarding/impl/src/main/res/values-fi/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Kirjaudu sisään manuaalisesti" - "Kirjaudu sisään QR-koodilla" - "Luo tili" - "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/onboarding/impl/src/main/res/values-fr/translations.xml b/features/onboarding/impl/src/main/res/values-fr/translations.xml deleted file mode 100644 index 0d8a7fa347d..00000000000 --- a/features/onboarding/impl/src/main/res/values-fr/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Se connecter manuellement" - "Se connecter avec un QR code" - "Créer un compte" - "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/onboarding/impl/src/main/res/values-hu/translations.xml b/features/onboarding/impl/src/main/res/values-hu/translations.xml deleted file mode 100644 index 308478f88b2..00000000000 --- a/features/onboarding/impl/src/main/res/values-hu/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Kézi bejelentkezés" - "Bejelentkezés QR-kóddal" - "Fiók létrehozása" - "Ü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/onboarding/impl/src/main/res/values-in/translations.xml b/features/onboarding/impl/src/main/res/values-in/translations.xml deleted file mode 100644 index e9b7b479a8b..00000000000 --- a/features/onboarding/impl/src/main/res/values-in/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Masuk secara manual" - "Masuk dengan kode QR" - "Buat akun" - "Selamat datang di %1$s tercepat yang pernah ada. Berdaya besar untuk kecepatan dan kesederhanaan." - "Selamat datang di %1$s. Berdaya penuh, untuk kecepatan dan kesederhanaan." - "Berada di elemen Anda" - diff --git a/features/onboarding/impl/src/main/res/values-it/translations.xml b/features/onboarding/impl/src/main/res/values-it/translations.xml deleted file mode 100644 index e134337d64f..00000000000 --- a/features/onboarding/impl/src/main/res/values-it/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Accedi manualmente" - "Accedi con codice QR" - "Crea account" - "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/onboarding/impl/src/main/res/values-ka/translations.xml b/features/onboarding/impl/src/main/res/values-ka/translations.xml deleted file mode 100644 index e75edfdefa7..00000000000 --- a/features/onboarding/impl/src/main/res/values-ka/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "ხელით შესვლა" - "შესვლა QR კოდით" - "ანგარიშის შექმნა" - "კეთილი იყოს თქვენი მობრძანება უსწრაფეს %1$s-ში. დამუხტულია სიჩქარისა და სიმარტივისათვის." - "კეთილი იყოს თქვენი მობრძანება %1$s-ში! დამუხტული სიჩქარისა და სიმარტივისთვის." - "იყავი შენს element-ში" - diff --git a/features/onboarding/impl/src/main/res/values-nb/translations.xml b/features/onboarding/impl/src/main/res/values-nb/translations.xml deleted file mode 100644 index d53a5a10e1a..00000000000 --- a/features/onboarding/impl/src/main/res/values-nb/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "Velkommen til %1$s. Supercharged, for hastighet og enkelhet." - "Vær i ditt rette element" - diff --git a/features/onboarding/impl/src/main/res/values-nl/translations.xml b/features/onboarding/impl/src/main/res/values-nl/translations.xml deleted file mode 100644 index 86bbf73ade3..00000000000 --- a/features/onboarding/impl/src/main/res/values-nl/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Handmatig inloggen" - "Inloggen met QR-code" - "Account aanmaken" - "Welkom bij de snelste %1$s ooit. Supercharged, voor snelheid en eenvoud." - "Welkom bij %1$s. Supercharged, voor snelheid en eenvoud." - "Wees in je element" - diff --git a/features/onboarding/impl/src/main/res/values-pl/translations.xml b/features/onboarding/impl/src/main/res/values-pl/translations.xml deleted file mode 100644 index 5ac4d81c27f..00000000000 --- a/features/onboarding/impl/src/main/res/values-pl/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Zaloguj się ręcznie" - "Zaloguj się za pomocą kodu QR" - "Utwórz konto" - "Witamy w %1$s. Szybszy i prostszy niż kiedykolwiek." - "Witamy w %1$s. Doładowany, dla szybkości i prostoty." - "Be in your element" - diff --git a/features/onboarding/impl/src/main/res/values-pt-rBR/translations.xml b/features/onboarding/impl/src/main/res/values-pt-rBR/translations.xml deleted file mode 100644 index 965c66c78cb..00000000000 --- a/features/onboarding/impl/src/main/res/values-pt-rBR/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Iniciar sessão manualmente" - "Iniciar sessão com código QR" - "Criar conta" - "Bem-vindo ao mais rápido %1$s de todos os tempos. Turbinado para velocidade e simplicidade." - "Bem-vindo ao %1$s. Turbinado, para velocidade e simplicidade" - "Esteja no seu elemento" - diff --git a/features/onboarding/impl/src/main/res/values-pt/translations.xml b/features/onboarding/impl/src/main/res/values-pt/translations.xml deleted file mode 100644 index 311343ef9f6..00000000000 --- a/features/onboarding/impl/src/main/res/values-pt/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Iniciar sessão manualmente" - "Iniciar sessão com código QR" - "Criar conta" - "Bem-vindo(a) à %1$s mais rápida de sempre. Super rápida e simples." - "Bem-vindo(a) à %1$s. Revitalizado, rápido e simples." - "A liberdade do teu elemento" - diff --git a/features/onboarding/impl/src/main/res/values-ro/translations.xml b/features/onboarding/impl/src/main/res/values-ro/translations.xml deleted file mode 100644 index 72756807297..00000000000 --- a/features/onboarding/impl/src/main/res/values-ro/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Conectați-vă manual" - "Conectați-vă cu un cod QR" - "Creați un cont" - "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" - diff --git a/features/onboarding/impl/src/main/res/values-ru/translations.xml b/features/onboarding/impl/src/main/res/values-ru/translations.xml deleted file mode 100644 index 80b05a158fb..00000000000 --- a/features/onboarding/impl/src/main/res/values-ru/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Войти вручную" - "Войти QR-кодом" - "Создать учетную запись" - "Добро пожаловать в самый быстрый клиент %1$s. Ориентирован на скорость и простоту." - "Добро пожаловать в %1$s. Ориентирован на скорость и простоту." - "Чувствуйте себя как дома с Element" - diff --git a/features/onboarding/impl/src/main/res/values-sk/translations.xml b/features/onboarding/impl/src/main/res/values-sk/translations.xml deleted file mode 100644 index 7ed548d025f..00000000000 --- a/features/onboarding/impl/src/main/res/values-sk/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Prihlásiť sa manuálne" - "Prihlásiť sa pomocou QR kódu" - "Vytvoriť účet" - "Vitajte v najrýchlejšom %1$s vôbec. Nadupaný pre rýchlosť a jednoduchosť." - "Vitajte v %1$s. Nadupaný, pre rýchlosť a jednoduchosť." - "Buďte vo svojom elemente" - diff --git a/features/onboarding/impl/src/main/res/values-sv/translations.xml b/features/onboarding/impl/src/main/res/values-sv/translations.xml deleted file mode 100644 index 6ffadf355b7..00000000000 --- a/features/onboarding/impl/src/main/res/values-sv/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Logga in manuellt" - "Logga in med QR-kod" - "Skapa konto" - "Välkommen till den snabbaste %1$s någonsin. Superladdad för snabbhet och enkelhet." - "Välkommen till %1$s. Superladdad, för snabbhet och enkelhet." - "Var i ditt rätta element" - diff --git a/features/onboarding/impl/src/main/res/values-uk/translations.xml b/features/onboarding/impl/src/main/res/values-uk/translations.xml deleted file mode 100644 index 8c834d9bd05..00000000000 --- a/features/onboarding/impl/src/main/res/values-uk/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Увійти вручну" - "Увійти за допомогою QR-коду" - "Створити обліковий запис" - "Ласкаво просимо до найшвидшого %1$s. Заряджений для швидкості та простоти." - "Ласкаво просимо до %1$s. Заряджений, для швидкості та простоти." - "Будьте у своєму element" - diff --git a/features/onboarding/impl/src/main/res/values-uz/translations.xml b/features/onboarding/impl/src/main/res/values-uz/translations.xml deleted file mode 100644 index b69af4adb76..00000000000 --- a/features/onboarding/impl/src/main/res/values-uz/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Qo\'lda tizimga kiring" - "QR kod bilan tizimga kiring" - "Hisob yaratish" - "Eng tezkor %1$sga xush kelibsiz. Tezlik va oddylik uchun super zaryadlangan." - "%1$sga Xush kelibsiz. Tezlik va oddylik uchun o\'ta zaryadlangan." - "Elementingizda bo\'ling" - diff --git a/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml b/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml deleted file mode 100644 index 8de629fca87..00000000000 --- a/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "手動登入" - "使用 QR code 登入" - "建立帳號" - "歡迎使用有史以來最快的 %1$s。速度超快,操作簡便。" - "歡迎使用 %1$s。速度超快且簡單。" - "Be in your element" - diff --git a/features/onboarding/impl/src/main/res/values-zh/translations.xml b/features/onboarding/impl/src/main/res/values-zh/translations.xml deleted file mode 100644 index f8b53c33693..00000000000 --- a/features/onboarding/impl/src/main/res/values-zh/translations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "手动登录" - "使用二维码登录" - "创建账户" - "欢迎使用 %1$s,快而简约的消息应用。" - "欢迎使用 %1$s,速度与简洁的极致。" - "融入您的 Element" - diff --git a/features/onboarding/impl/src/main/res/values/localazy.xml b/features/onboarding/impl/src/main/res/values/localazy.xml deleted file mode 100644 index 181f27dfb5c..00000000000 --- a/features/onboarding/impl/src/main/res/values/localazy.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - "Sign in manually" - "Sign in with QR code" - "Create account" - "Welcome to our new app. Supercharged for speed and simplicity." - "Welcome to our new app. Supercharged for speed and simplicity." - "The Revivalists" - diff --git a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt deleted file mode 100644 index e98fbff3db2..00000000000 --- a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.onboarding.impl - -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.appconfig.OnBoardingConfig -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.tests.testutils.WarmUpRule -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class OnBoardingPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state`() = runTest { - val buildMeta = aBuildMeta( - applicationName = "A", - productionApplicationName = "B", - desktopApplicationName = "C", - ) - val featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.QrCodeLogin.key to true), - buildMeta = buildMeta, - ) - val presenter = OnBoardingPresenter( - buildMeta = buildMeta, - featureFlagService = featureFlagService, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.canLoginWithQrCode).isFalse() - assertThat(initialState.productionApplicationName).isEqualTo("B") - assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) - assertThat(awaitItem().canLoginWithQrCode).isTrue() - } - } -} diff --git a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt deleted file mode 100644 index 25cb0756976..00000000000 --- a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.onboarding.impl - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import 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 - -@RunWith(AndroidJUnit4::class) -class OnboardingViewTest { - @get:Rule - val rule = createAndroidComposeRule() - - @Test - fun `when can create account - clicking on create account calls the expected callback`() { - ensureCalledOnce { callback -> - rule.setOnboardingView( - state = anOnBoardingState(canCreateAccount = true), - onCreateAccount = callback, - ) - rule.clickOn(R.string.screen_onboarding_sign_up) - } - } - - @Test - fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() { - ensureCalledOnce { callback -> - rule.setOnboardingView( - state = anOnBoardingState(canLoginWithQrCode = true), - onSignInWithQrCode = callback, - ) - rule.clickOn(R.string.screen_onboarding_sign_in_with_qr_code) - } - } - - @Test - fun `when can login with QR code - clicking on sign in manually calls the expected callback`() { - ensureCalledOnce { callback -> - rule.setOnboardingView( - state = anOnBoardingState(canLoginWithQrCode = true), - onSignIn = callback, - ) - rule.clickOn(R.string.screen_onboarding_sign_in_manually) - } - } - - @Test - fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`() { - ensureCalledOnce { callback -> - rule.setOnboardingView( - state = anOnBoardingState( - canLoginWithQrCode = false, - canCreateAccount = false, - ), - onSignIn = callback, - ) - rule.clickOn(CommonStrings.action_continue) - } - } - - @Test - fun `clicking on report a problem calls the sign in callback`() { - ensureCalledOnce { callback -> - rule.setOnboardingView( - state = anOnBoardingState(), - onReportProblem = callback, - ) - rule.clickOn(CommonStrings.common_report_a_problem) - } - } - - private fun AndroidComposeTestRule.setOnboardingView( - state: OnBoardingState, - onSignInWithQrCode: () -> Unit = EnsureNeverCalled(), - onSignIn: () -> Unit = EnsureNeverCalled(), - onCreateAccount: () -> Unit = EnsureNeverCalled(), - onReportProblem: () -> Unit = EnsureNeverCalled(), - ) { - setContent { - OnBoardingView( - state = state, - onSignInWithQrCode = onSignInWithQrCode, - onSignIn = onSignIn, - onCreateAccount = onCreateAccount, - onReportProblem = onReportProblem, - ) - } - } -} diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt index 4a2ba6360e3..59d13fed1e1 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt @@ -15,16 +15,18 @@ 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.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.RadioButtonUnchecked import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.StrokeCap 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.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.poll.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.Icon @@ -34,20 +36,48 @@ import io.element.android.libraries.designsystem.theme.progressIndicatorTrackCol import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.ui.strings.CommonPlurals +import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun PollAnswerView( answerItem: PollAnswerItem, modifier: Modifier = Modifier, ) { + val nbVotesText = pluralStringResource( + id = CommonPlurals.common_poll_votes_count, + count = answerItem.votesCount, + answerItem.votesCount, + ) + val a11yText = buildString { + val sentenceDelimiter = stringResource(CommonStrings.common_sentence_delimiter) + append(answerItem.answer.text.removeSuffix(".")) + if (answerItem.showVotes) { + append(sentenceDelimiter) + append(nbVotesText) + if (answerItem.votesCount != 0) { + append(sentenceDelimiter) + (answerItem.percentage * 100).toInt().let { percent -> + append(pluralStringResource(R.plurals.a11y_polls_percent_of_total, percent, percent)) + } + } + if (answerItem.isWinner) { + append(sentenceDelimiter) + append(stringResource(R.string.a11y_polls_winning_answer)) + } + } + } Row( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .clearAndSetSemantics { + contentDescription = a11yText + }, ) { Icon( imageVector = if (answerItem.isSelected) { - Icons.Default.CheckCircle + CompoundIcons.CheckCircleSolid() } else { - Icons.Default.RadioButtonUnchecked + CompoundIcons.Circle() }, contentDescription = null, modifier = Modifier @@ -72,11 +102,6 @@ internal fun PollAnswerView( style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular, ) if (answerItem.showVotes) { - val text = pluralStringResource( - id = CommonPlurals.common_poll_votes_count, - count = answerItem.votesCount, - answerItem.votesCount - ) Row( modifier = Modifier.align(Alignment.Bottom), verticalAlignment = Alignment.CenterVertically, @@ -89,13 +114,13 @@ internal fun PollAnswerView( ) Spacer(modifier = Modifier.width(2.dp)) Text( - text = text, + text = nbVotesText, style = ElementTheme.typography.fontBodySmMedium, color = ElementTheme.colors.textPrimary, ) } else { Text( - text = text, + text = nbVotesText, style = ElementTheme.typography.fontBodySmRegular, color = ElementTheme.colors.textSecondary, ) diff --git a/features/poll/api/src/main/res/values-cs/translations.xml b/features/poll/api/src/main/res/values-cs/translations.xml new file mode 100644 index 00000000000..888f9efbad6 --- /dev/null +++ b/features/poll/api/src/main/res/values-cs/translations.xml @@ -0,0 +1,9 @@ + + + + "%1$d procento z celkového počtu hlasů" + "%1$d procenta z celkového počtu hlasů" + "%1$d procent z celkového počtu hlasů" + + "Toto je vítězná odpověď" + diff --git a/features/poll/api/src/main/res/values-cy/translations.xml b/features/poll/api/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..9be4adc91e8 --- /dev/null +++ b/features/poll/api/src/main/res/values-cy/translations.xml @@ -0,0 +1,4 @@ + + + "Dyma\'r ateb buddugol" + diff --git a/features/poll/api/src/main/res/values-da/translations.xml b/features/poll/api/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..1024970e20e --- /dev/null +++ b/features/poll/api/src/main/res/values-da/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d% af de samlede stemmer" + "%1$d procent af det samlede antal stemmer" + + "Dette er det vindende svar" + diff --git a/features/poll/api/src/main/res/values-de/translations.xml b/features/poll/api/src/main/res/values-de/translations.xml new file mode 100644 index 00000000000..b80268d03ea --- /dev/null +++ b/features/poll/api/src/main/res/values-de/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d Prozent der Stimmen insgesamt" + "%1$d Prozent der Gesamtstimmen" + + "Das ist die Gewinnerantwort" + diff --git a/features/poll/api/src/main/res/values-el/translations.xml b/features/poll/api/src/main/res/values-el/translations.xml new file mode 100644 index 00000000000..fcd34977d30 --- /dev/null +++ b/features/poll/api/src/main/res/values-el/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d τοις εκατό των συνολικών ψήφων" + "%1$d τοις εκατό του συνόλου των ψήφων" + + "Αυτή είναι η νικητήρια απάντηση" + diff --git a/features/poll/api/src/main/res/values-et/translations.xml b/features/poll/api/src/main/res/values-et/translations.xml new file mode 100644 index 00000000000..a57d937d4f9 --- /dev/null +++ b/features/poll/api/src/main/res/values-et/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d protsent kõikidest antud häältest" + "%1$d protsenti kõikidest antud häältest" + + "See vastus võitis" + diff --git a/features/poll/api/src/main/res/values-fr/translations.xml b/features/poll/api/src/main/res/values-fr/translations.xml new file mode 100644 index 00000000000..a085a9d40e5 --- /dev/null +++ b/features/poll/api/src/main/res/values-fr/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d pour cent du total des votes" + "%1$d pour cent du total des votes" + + "C’est la réponse gagnante" + diff --git a/features/poll/api/src/main/res/values-hu/translations.xml b/features/poll/api/src/main/res/values-hu/translations.xml new file mode 100644 index 00000000000..3bb31812950 --- /dev/null +++ b/features/poll/api/src/main/res/values-hu/translations.xml @@ -0,0 +1,8 @@ + + + + "az összes szavazat %1$d százaléka" + "az összes szavazat %1$d százaléka" + + "Ez a győztes válasz" + diff --git a/features/poll/api/src/main/res/values-in/translations.xml b/features/poll/api/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..1810031c93d --- /dev/null +++ b/features/poll/api/src/main/res/values-in/translations.xml @@ -0,0 +1,7 @@ + + + + "%1$d persen dari total suara" + + "Ini adalah jawaban yang menang" + diff --git a/features/poll/api/src/main/res/values-it/translations.xml b/features/poll/api/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..1a634bd625d --- /dev/null +++ b/features/poll/api/src/main/res/values-it/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d percento dei voti totali" + "%1$d percento dei voti totali" + + "Questa è la risposta vincente" + diff --git a/features/poll/api/src/main/res/values-nb/translations.xml b/features/poll/api/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..332f319e7ca --- /dev/null +++ b/features/poll/api/src/main/res/values-nb/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d prosent av totalt antall stemmer" + "%1$d prosent av totalt antall stemmer" + + "Dette er vinnersvaret" + diff --git a/features/poll/api/src/main/res/values-pt-rBR/translations.xml b/features/poll/api/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..88681aca303 --- /dev/null +++ b/features/poll/api/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d por cento de todos os votos" + "%1$d por cento de todos os votos" + + "Esta é a resposta vencedora" + diff --git a/features/poll/api/src/main/res/values-pt/translations.xml b/features/poll/api/src/main/res/values-pt/translations.xml new file mode 100644 index 00000000000..f1292fee2fe --- /dev/null +++ b/features/poll/api/src/main/res/values-pt/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d porcento de todos os votos" + "%1$d porcento de todos os votos" + + "Esta é a reposta vencedora" + diff --git a/features/poll/api/src/main/res/values-sk/translations.xml b/features/poll/api/src/main/res/values-sk/translations.xml new file mode 100644 index 00000000000..42a6d229453 --- /dev/null +++ b/features/poll/api/src/main/res/values-sk/translations.xml @@ -0,0 +1,9 @@ + + + + "%1$d percento z celkového počtu hlasov" + "%1$d percentá z celkového počtu hlasov" + "%1$d percent z celkového počtu hlasov" + + "Toto je víťazná odpoveď" + diff --git a/features/poll/api/src/main/res/values-uk/translations.xml b/features/poll/api/src/main/res/values-uk/translations.xml new file mode 100644 index 00000000000..bbb1618deb9 --- /dev/null +++ b/features/poll/api/src/main/res/values-uk/translations.xml @@ -0,0 +1,9 @@ + + + + "%1$d відсоток від усіх голосів" + "%1$d відсотки від усіх голосів" + "%1$d відсотків від усіх голосів" + + "Ця відповідь перемогла" + diff --git a/features/poll/api/src/main/res/values/localazy.xml b/features/poll/api/src/main/res/values/localazy.xml new file mode 100644 index 00000000000..2d1142194c4 --- /dev/null +++ b/features/poll/api/src/main/res/values/localazy.xml @@ -0,0 +1,8 @@ + + + + "%1$d percent of total votes" + "%1$d percents of total votes" + + "This is the winning answer" + diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt index 18bb1647783..ea11ef918e6 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt @@ -12,17 +12,17 @@ import im.vector.app.features.analytics.plan.PollEnd import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.services.analytics.api.AnalyticsService import javax.inject.Inject @ContributesBinding(RoomScope::class) class DefaultEndPollAction @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val analyticsService: AnalyticsService, ) : EndPollAction { override suspend fun execute(pollStartId: EventId): Result { - return room.endPoll( + return room.liveTimeline.endPoll( pollStartId = pollStartId, text = "The poll with event id: $pollStartId has ended." ).onSuccess { diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt index d11be46cdc0..4f1f29df461 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt @@ -12,17 +12,17 @@ import im.vector.app.features.analytics.plan.PollVote import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.services.analytics.api.AnalyticsService import javax.inject.Inject @ContributesBinding(RoomScope::class) class DefaultSendPollResponseAction @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val analyticsService: AnalyticsService, ) : SendPollResponseAction { override suspend fun execute(pollStartId: EventId, answerId: String): Result { - return room.sendPollResponse( + return room.liveTimeline.sendPollResponse( pollStartId = pollStartId, answers = listOf(answerId), ).onSuccess { diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt index 04f927086bd..5a86e476a97 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope import io.element.android.services.analytics.api.AnalyticsService +import java.util.concurrent.atomic.AtomicBoolean @ContributesNode(RoomScope::class) class CreatePollNode @AssistedInject constructor( @@ -34,7 +35,16 @@ class CreatePollNode @AssistedInject constructor( private val inputs: Inputs = inputs() - private val presenter = presenterFactory.create(backNavigator = ::navigateUp, mode = inputs.mode) + private var isNavigatingUp = AtomicBoolean(false) + + private val presenter = presenterFactory.create( + backNavigator = { + if (isNavigatingUp.compareAndSet(false, true)) { + navigateUp() + } + }, + mode = inputs.mode, + ) init { lifecycle.subscribe( diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index 7df06d96409..ce6a8682b6b 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -19,8 +19,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -33,7 +31,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.poll.impl.R import io.element.android.libraries.designsystem.components.button.BackButton @@ -41,7 +38,6 @@ import io.element.android.libraries.designsystem.components.dialogs.Confirmation import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.aliasScreenTitle 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.IconSource @@ -145,7 +141,7 @@ fun CreatePollView( trailingContent = ListItemContent.Custom { Icon( imageVector = CompoundIcons.Delete(), - contentDescription = null, + contentDescription = stringResource(R.string.screen_create_poll_delete_option_a11y, answer.text), modifier = Modifier.clickable(answer.canDelete) { state.eventSink(CreatePollEvents.RemoveAnswer(index)) }, @@ -159,7 +155,7 @@ fun CreatePollView( ListItem( headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_add_option_btn)) }, leadingContent = ListItemContent.Icon( - iconSource = IconSource.Vector(Icons.Default.Add), + iconSource = IconSource.Vector(CompoundIcons.Plus()), ), style = ListItemStyle.Primary, onClick = { @@ -180,8 +176,14 @@ fun CreatePollView( supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) }, trailingContent = ListItemContent.Switch( checked = state.pollKind == PollKind.Undisclosed, - onChange = { state.eventSink(CreatePollEvents.SetPollKind(if (it) PollKind.Undisclosed else PollKind.Disclosed)) }, ), + onClick = { + state.eventSink( + CreatePollEvents.SetPollKind( + if (state.pollKind == PollKind.Disclosed) PollKind.Undisclosed else PollKind.Disclosed + ) + ) + }, ) if (state.canDelete) { ListItem( @@ -205,14 +207,9 @@ private fun CreatePollTopAppBar( onSaveClick: () -> Unit = {}, ) { TopAppBar( - title = { - Text( - text = when (mode) { - CreatePollState.Mode.New -> stringResource(id = R.string.screen_create_poll_title) - CreatePollState.Mode.Edit -> stringResource(id = R.string.screen_edit_poll_title) - }, - style = ElementTheme.typography.aliasScreenTitle, - ) + titleStr = when (mode) { + CreatePollState.Mode.New -> stringResource(id = R.string.screen_create_poll_title) + CreatePollState.Mode.Edit -> stringResource(id = R.string.screen_edit_poll_title) }, navigationIcon = { BackButton(onClick = onBackClick) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt index f258444fc30..0fbdcdee36e 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt @@ -7,9 +7,10 @@ package io.element.android.features.poll.impl.data +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.matrix.api.timeline.getActiveTimeline @@ -19,10 +20,10 @@ import kotlinx.coroutines.flow.first import javax.inject.Inject class PollRepository @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val timelineProvider: TimelineProvider, ) { - suspend fun getPoll(eventId: EventId): Result = runCatching { + suspend fun getPoll(eventId: EventId): Result = runCatchingExceptions { timelineProvider .getActiveTimeline() .timelineItems @@ -41,7 +42,7 @@ class PollRepository @Inject constructor( pollKind: PollKind, maxSelections: Int, ): Result = when (existingPollId) { - null -> room.createPoll( + null -> room.liveTimeline.createPoll( question = question, answers = answers, maxSelections = maxSelections, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt index 00f68e923bf..e3d76322657 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt @@ -23,7 +23,8 @@ import io.element.android.features.poll.impl.history.model.PollHistoryFilter import io.element.android.features.poll.impl.history.model.PollHistoryItems import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.Timeline import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map @@ -31,16 +32,17 @@ import kotlinx.coroutines.launch import javax.inject.Inject class PollHistoryPresenter @Inject constructor( - private val appCoroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val sendPollResponseAction: SendPollResponseAction, private val endPollAction: EndPollAction, private val pollHistoryItemFactory: PollHistoryItemsFactory, - private val room: MatrixRoom, + private val room: JoinedRoom, ) : Presenter { @Composable override fun present(): PollHistoryState { val timeline = room.liveTimeline - val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState() + val paginationState by timeline.backwardPaginationStatus.collectAsState() val pollHistoryItemsFlow = remember { timeline.timelineItems.map { items -> pollHistoryItemFactory.create(items) @@ -64,10 +66,10 @@ class PollHistoryPresenter @Inject constructor( is PollHistoryEvents.LoadMore -> { coroutineScope.loadMore(timeline) } - is PollHistoryEvents.SelectPollAnswer -> appCoroutineScope.launch { + is PollHistoryEvents.SelectPollAnswer -> sessionCoroutineScope.launch { sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId) } - is PollHistoryEvents.EndPoll -> appCoroutineScope.launch { + is PollHistoryEvents.EndPoll -> sessionCoroutineScope.launch { endPollAction.execute(pollStartId = event.pollStartId) } is PollHistoryEvents.SelectFilter -> { diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt index be517fa80d0..0ff1ef57a59 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt @@ -8,7 +8,6 @@ package io.element.android.features.poll.impl.history import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -24,13 +23,14 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -42,7 +42,6 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItem 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.aliasScreenTitle 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.SegmentedButton @@ -53,7 +52,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PollHistoryView( state: PollHistoryState, @@ -77,12 +76,7 @@ fun PollHistoryView( modifier = modifier, topBar = { TopAppBar( - title = { - Text( - text = stringResource(R.string.screen_polls_history_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(R.string.screen_polls_history_title), navigationIcon = { BackButton(onClick = goBack) }, @@ -180,7 +174,9 @@ private fun PollHistoryList( if (pollHistoryItems.isEmpty()) { item { Column( - modifier = Modifier.fillParentMaxSize().padding(bottom = 24.dp), + modifier = Modifier + .fillParentMaxSize() + .padding(bottom = 24.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -193,7 +189,9 @@ private fun PollHistoryList( text = emptyStringResource, style = ElementTheme.typography.fontBodyLgRegular, color = ElementTheme.colors.textSecondary, - modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp, horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp, horizontal = 16.dp), textAlign = TextAlign.Center, ) @@ -229,14 +227,17 @@ private fun PollHistoryItemRow( modifier: Modifier = Modifier, ) { Surface( - modifier = modifier, + modifier = modifier.semantics(mergeDescendants = true) { + // Allow the answers to be traversed by Talkback + isTraversalGroup = true + }, border = BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary), shape = RoundedCornerShape(size = 12.dp) ) { Column(modifier = Modifier.padding(16.dp)) { Text( text = pollHistoryItem.formattedDate, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodySmRegular, ) Spacer(modifier = Modifier.height(4.dp)) diff --git a/features/poll/impl/src/main/res/values-cs/translations.xml b/features/poll/impl/src/main/res/values-cs/translations.xml index ac1ebfb8dd5..b5c7f325ad6 100644 --- a/features/poll/impl/src/main/res/values-cs/translations.xml +++ b/features/poll/impl/src/main/res/values-cs/translations.xml @@ -5,6 +5,7 @@ "Anonymní hlasování" "Volba %1$d" "Vaše změny nebyly uloženy. Opravdu se chcete vrátit?" + "Smazat možnost %1$s" "Otázka nebo téma" "Čeho se hlasování týká?" "Vytvořit hlasování" diff --git a/features/poll/impl/src/main/res/values-cy/translations.xml b/features/poll/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..d325c1348e3 --- /dev/null +++ b/features/poll/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,20 @@ + + + "Ychwanegu dewis" + "Dangos canlyniadau dim ond ar ôl i\'r pleidleisio ddod i ben" + "Cuddio pleidleisiau" + "Dewis %1$d" + "Dyw eich newidiadau heb gael eu cadw. Ydych chi\'n siŵr eich bod am fynd nôl?" + "Dileu opsiwn %1$s" + "Cwestiwn neu bwnc" + "Am beth mae\'r bleidlais?" + "Creu Pleidlais" + "Ydych chi\'n siŵr eich bod am ddileu\'r bleidlais hon?" + "Dileu Pleidlais" + "Golygu pleidlais" + "Methu dod o hyd i unrhyw bleidleisiau cyfredol." + "Methu dod o hyd i unrhyw bleidleisiau blaenorol." + "Parhaus" + "Gorffennol" + "Pleidleisiau" + diff --git a/features/poll/impl/src/main/res/values-da/translations.xml b/features/poll/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..84ab5de145b --- /dev/null +++ b/features/poll/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,20 @@ + + + "Tilføj mulighed" + "Vis først resultater, når afstemningen er slut" + "Skjul stemmer" + "Mulighed %1$d" + "Dine ændringer er ikke blevet gemt. Er du sikker på, at du vil gå tilbage?" + "Slet mulighed %1$s" + "Spørgsmål eller emne" + "Hvad handler afstemningen om?" + "Opret afstemning" + "Er du sikker på, at du ønsker at slette denne afstemning?" + "Slet afstemning" + "Redigér afstemning" + "Kan ikke finde nogen igangværende afstemninger." + "Kan ikke finde nogen tidligere afstemninger." + "Igangværende" + "Tidligere" + "Afstemninger" + diff --git a/features/poll/impl/src/main/res/values-de/translations.xml b/features/poll/impl/src/main/res/values-de/translations.xml index 99520079e6e..a7c2939d423 100644 --- a/features/poll/impl/src/main/res/values-de/translations.xml +++ b/features/poll/impl/src/main/res/values-de/translations.xml @@ -4,16 +4,17 @@ "Ergebnisse erst nach Ende der Umfrage anzeigen" "Anonyme Umfrage" "Option %1$d" - "Deine Änderungen wurden nicht gespeichert. Bist du sicher, dass du zurückgehen willst?" + "Ihre Änderungen wurden nicht gespeichert. Sind Sie sicher, dass Sie zurückgehen wollen?" + "Lösche Option %1$s" "Frage oder Thema" "Worum geht es bei der Umfrage?" "Umfrage erstellen" - "Bist du dir sicher, dass du diese Umfrage löschen möchtest?" + "Möchten Sie diese Umfrage wirklich löschen?" "Umfrage löschen" "Umfrage bearbeiten" "Keine laufenden Umfragen vorhanden." - "Keine vergangenen Umfragen vorhanden." + "Keine beendeten Umfragen vorhanden." "Aktuell" - "Vergangene" + "Vergangenheit" "Umfragen" diff --git a/features/poll/impl/src/main/res/values-el/translations.xml b/features/poll/impl/src/main/res/values-el/translations.xml index e13ba7746af..108c9c6bcec 100644 --- a/features/poll/impl/src/main/res/values-el/translations.xml +++ b/features/poll/impl/src/main/res/values-el/translations.xml @@ -5,6 +5,7 @@ "Απόκρυψη ψήφων" "Επιλογή %1$d" "Οι αλλαγές σου δεν έχουν αποθηκευτεί. Σίγουρα θες να πας πίσω;" + "Διαγραφή επιλογής %1$s" "Ερώτηση ή θέμα" "Τί αφορά η δημοσκόπηση;" "Δημιουργία Δημοσκόπησης" diff --git a/features/poll/impl/src/main/res/values-et/translations.xml b/features/poll/impl/src/main/res/values-et/translations.xml index 4257685e756..8312856e87e 100644 --- a/features/poll/impl/src/main/res/values-et/translations.xml +++ b/features/poll/impl/src/main/res/values-et/translations.xml @@ -5,6 +5,7 @@ "Peida hääled" "Valik %1$d" "Sinu tehtud muudatused pole veel salvestatud. Kas sa oled kindel, et soovid minna tagasi?" + "Kustuta valik: %1$s" "Küsimus või teema" "Mis on küsitluse teema?" "Loo küsitlus" diff --git a/features/poll/impl/src/main/res/values-eu/translations.xml b/features/poll/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..768854d44c0 --- /dev/null +++ b/features/poll/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,19 @@ + + + "Gehitu aukera" + "Erakutsi emaitzak inkesta amaitutakoan soilik" + "Ezkutatu botoak" + "%1$d. aukera" + "Zure aldaketak ez dira gorde. Ziur itzuli nahi duzula?" + "Galdera edo gaia" + "Zeri buruzko inkesta da?" + "Sortu inkesta" + "Ziur inkesta hau ezabatu nahi duzula?" + "Ezabatu inkesta" + "Editatu inkesta" + "Ezin da abian dagoen inkestarik aurkitu." + "Ezin da iraungitako inkestarik aurkitu." + "Abian direnak" + "Iraungitakoak" + "Inkestak" + diff --git a/features/poll/impl/src/main/res/values-fr/translations.xml b/features/poll/impl/src/main/res/values-fr/translations.xml index 8a0c8ea5d56..badf3888e2c 100644 --- a/features/poll/impl/src/main/res/values-fr/translations.xml +++ b/features/poll/impl/src/main/res/values-fr/translations.xml @@ -5,6 +5,7 @@ "Masquer les votes" "Option %1$d" "Vos modifications n’ont pas été enregistrées. Êtes-vous certain de vouloir quitter ?" + "Supprimer l’option %1$s" "Question ou sujet" "Quel est le sujet du sondage ?" "Créer un sondage" diff --git a/features/poll/impl/src/main/res/values-hu/translations.xml b/features/poll/impl/src/main/res/values-hu/translations.xml index 310c1a77dbc..ed6a79363f0 100644 --- a/features/poll/impl/src/main/res/values-hu/translations.xml +++ b/features/poll/impl/src/main/res/values-hu/translations.xml @@ -5,6 +5,7 @@ "Szavazatok elrejtése" "%1$d. lehetőség" "A módosítások nem lettek mentve. Biztos, hogy visszalép?" + "Lehetőség törlése: %1$s" "Kérdés vagy téma" "Miről szól ez a szavazás?" "Szavazás létrehozása" diff --git a/features/poll/impl/src/main/res/values-in/translations.xml b/features/poll/impl/src/main/res/values-in/translations.xml index f55dccd75d6..99250c4bef2 100644 --- a/features/poll/impl/src/main/res/values-in/translations.xml +++ b/features/poll/impl/src/main/res/values-in/translations.xml @@ -5,6 +5,7 @@ "Pemungutan suara anonim" "Opsi %1$d" "Perubahan Anda belum disimpan. Apakah Anda yakin ingin kembali?" + "Hapus opsi %1$s" "Pertanyaan atau topik" "Tentang apa pemungutan suara ini?" "Buat pemungutan suara" diff --git a/features/poll/impl/src/main/res/values-it/translations.xml b/features/poll/impl/src/main/res/values-it/translations.xml index bf7190db233..3ffbf03afa5 100644 --- a/features/poll/impl/src/main/res/values-it/translations.xml +++ b/features/poll/impl/src/main/res/values-it/translations.xml @@ -5,6 +5,7 @@ "Nascondi voti" "Opzione %1$d" "Le modifiche non sono state salvate. Vuoi davvero tornare indietro?" + "Elimina l\'opzione %1$s" "Domanda o argomento" "Di cosa parla il sondaggio?" "Crea sondaggio" diff --git a/features/poll/impl/src/main/res/values-ka/translations.xml b/features/poll/impl/src/main/res/values-ka/translations.xml index 05d046a246f..fe70a883a84 100644 --- a/features/poll/impl/src/main/res/values-ka/translations.xml +++ b/features/poll/impl/src/main/res/values-ka/translations.xml @@ -4,6 +4,7 @@ "შედეგების ჩვენება მხოლოდ გამოკითხვის დასრულების შემდეგ" "ხმების დამალვა" "ვარიანტი %1$d" + "თქვენი ცვლილებები არაა შენახული. დარწმუნებული ხართ დაბრუნებაში?" "კითხვა ან თემა" "რას ეხება გამოკითხვა?" "გამოკითხვის შექმნა" diff --git a/features/poll/impl/src/main/res/values-nb/translations.xml b/features/poll/impl/src/main/res/values-nb/translations.xml index e9078d7c944..7396cf9cf90 100644 --- a/features/poll/impl/src/main/res/values-nb/translations.xml +++ b/features/poll/impl/src/main/res/values-nb/translations.xml @@ -1,9 +1,20 @@ + "Legg til alternativ" + "Vis resultater bare etter at avstemningen er avsluttet" + "Skjul stemmer" + "Alternativ %1$d" "Endringene dine er ikke lagret. Er du sikker på at du vil gå tilbake?" + "Slett alternativet %1$s" + "Spørsmål eller emne" "Hva handler avstemningen om?" "Opprett avstemning" "Er du sikker på at du vil slette denne avstemningen?" "Slett avstemning" "Rediger avstemning" + "Finner ingen pågående avstemninger." + "Kan ikke finne noen tidligere avstemninger." + "Pågående" + "Fortid" + "Avstemninger" diff --git a/features/poll/impl/src/main/res/values-pt-rBR/translations.xml b/features/poll/impl/src/main/res/values-pt-rBR/translations.xml index 3be7be89b9c..d1eb60cc231 100644 --- a/features/poll/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/poll/impl/src/main/res/values-pt-rBR/translations.xml @@ -5,6 +5,7 @@ "Ocultar votos" "Opção %1$d" "Suas alterações não foram salvas. Tem certeza de que você quer voltar?" + "Apagar opção %1$s" "Pergunta ou tópico" "Sobre o que é a enquete?" "Criar enquete" diff --git a/features/poll/impl/src/main/res/values-pt/translations.xml b/features/poll/impl/src/main/res/values-pt/translations.xml index 456b03b3174..26bacb9f131 100644 --- a/features/poll/impl/src/main/res/values-pt/translations.xml +++ b/features/poll/impl/src/main/res/values-pt/translations.xml @@ -5,6 +5,7 @@ "Ocultar votos" "Opção %1$d" "As tuas alterações não foram guardadas. Tens a certeza que queres voltar atrás?" + "Eliminar opção %1$s" "Pergunta ou tópico" "De que trata a sondagem?" "Criar sondagem" diff --git a/features/poll/impl/src/main/res/values-sk/translations.xml b/features/poll/impl/src/main/res/values-sk/translations.xml index 57f2ff1fb7c..a3c51291056 100644 --- a/features/poll/impl/src/main/res/values-sk/translations.xml +++ b/features/poll/impl/src/main/res/values-sk/translations.xml @@ -5,6 +5,7 @@ "Anonymná anketa" "Možnosť %1$d" "Vaše zmeny neboli uložené. Naozaj sa chcete vrátiť?" + "Odstrániť možnosť %1$s" "Otázka alebo téma" "O čom je anketa?" "Vytvoriť anketu" diff --git a/features/poll/impl/src/main/res/values-tr/translations.xml b/features/poll/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..eecf3ad0533 --- /dev/null +++ b/features/poll/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,19 @@ + + + "Seçenek ekle" + "Sonuçları yalnızca anket bittikten sonra göster" + "Oyları gizle" + "Seçenek %1$d" + "Değişiklikleriniz kaydedilmedi. Geri dönmek istediğinden emin misin?" + "Soru veya konu" + "Anket ne hakkında?" + "Anket Oluştur" + "Bu anketi silmek istediğinize emin misiniz?" + "Anketi Sil" + "Anketi düzenle" + "Devam eden bir anket bulamadım." + "Geçmiş herhangi bir anket bulamıyorum." + "Devam ediyor" + "Geçmiş" + "Anketler" + diff --git a/features/poll/impl/src/main/res/values-uk/translations.xml b/features/poll/impl/src/main/res/values-uk/translations.xml index ec1a7bda202..1e62e17a613 100644 --- a/features/poll/impl/src/main/res/values-uk/translations.xml +++ b/features/poll/impl/src/main/res/values-uk/translations.xml @@ -5,6 +5,7 @@ "Приховати голоси" "Варіант %1$d" "Внесені зміни не збережено. Ви впевнені, що хочете повернутися?" + "Видалити варіант %1$s" "Питання або тема" "Про що йдеться в опитуванні?" "Створити опитування" diff --git a/features/poll/impl/src/main/res/values-ur/translations.xml b/features/poll/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..8bb04683a1c --- /dev/null +++ b/features/poll/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,19 @@ + + + "اختیار شامل کریں" + "رائے شماری کے بعد ہی نتائج ظاہر کریں" + "آراء چھپائیں" + "اختیار %1$d" + "آپ کی تبدیلیاں محفوظ نہیں کی گئیں۔ کیا آپ کو یقین ہے کہ آپ واپس جانا چاہتے ہیں؟" + "سوال یا موضوع" + "رائے شماری کس بارے میں ہے؟" + "رائے شماری بنائیں" + "کیا آپ کو یقین ہے کہ آپ اس رائے شماری کو حذف کرنا چاہتے ہیں؟" + "رائے شماری حذف کریں" + "رائے شماری میں ترمیم کریں" + "کوئی جاری رائے شماری ہا نہیں مل سکے۔" + "ماضی کے کوئی رائے شماری ہا نہیں مل سکے۔" + "جاری" + "ماضی" + "رائے شماری ہا" + diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml index 7a7a15ea3cf..6019d908b4f 100644 --- a/features/poll/impl/src/main/res/values/localazy.xml +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -5,6 +5,7 @@ "Hide votes" "Option %1$d" "Your changes have not been saved. Are you sure you want to go back?" + "Delete option %1$s" "Question or topic" "What is the poll about?" "Create Poll" diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt index 76ccc1ffacb..67556886bc1 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -21,12 +21,11 @@ import io.element.android.features.poll.impl.anOngoingPollContent import io.element.android.features.poll.impl.data.PollRepository import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind -import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.PollContent 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.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider import io.element.android.services.analytics.test.FakeAnalyticsService @@ -52,7 +51,7 @@ class CreatePollPresenterTest { private val timeline = FakeTimeline( timelineItems = aPollTimelineItems(mapOf(pollEventId to existingPoll)) ) - private val fakeMatrixRoom = FakeMatrixRoom( + private val fakeJoinedRoom = FakeJoinedRoom( liveTimeline = timeline ) private val fakeAnalyticsService = FakeAnalyticsService() @@ -81,7 +80,7 @@ class CreatePollPresenterTest { @Test fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( liveTimeline = FakeTimeline() ) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room) @@ -121,8 +120,10 @@ class CreatePollPresenterTest { fun `create poll sends a poll start event`() = runTest { val createPollResult = lambdaRecorder, Int, PollKind, Result> { _, _, _, _ -> Result.success(Unit) } val presenter = createCreatePollPresenter( - room = FakeMatrixRoom( - createPollResult = createPollResult + room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + createPollLambda = createPollResult + }, ), mode = CreatePollMode.NewPoll, ) @@ -169,8 +170,10 @@ class CreatePollPresenterTest { Result.failure(error) } val presenter = createCreatePollPresenter( - room = FakeMatrixRoom( - createPollResult = createPollResult + room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + createPollLambda = createPollResult + }, ), mode = CreatePollMode.NewPoll, ) @@ -254,12 +257,8 @@ class CreatePollPresenterTest { @Test fun `when edit poll fails, error is tracked`() = runTest { val error = Exception("cause") - val editPollResult = lambdaRecorder { _: EventId, _: String, _: List, _: Int, _: PollKind -> - Result.failure(error) - } val presenter = createCreatePollPresenter( - room = FakeMatrixRoom( - editPollResult = editPollResult, + room = FakeJoinedRoom( liveTimeline = timeline, ), mode = CreatePollMode.EditPoll(pollEventId), @@ -277,7 +276,7 @@ class CreatePollPresenterTest { awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A")) awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save) advanceUntilIdle() // Wait for the coroutine to finish - assert(editPollLambda).isCalledOnce() + editPollLambda.assertions().isCalledOnce() assertThat(fakeAnalyticsService.capturedEvents).isEmpty() assertThat(fakeAnalyticsService.trackedErrors).hasSize(1) assertThat(fakeAnalyticsService.trackedErrors).containsExactly( @@ -551,7 +550,7 @@ class CreatePollPresenterTest { private fun createCreatePollPresenter( mode: CreatePollMode = CreatePollMode.NewPoll, - room: MatrixRoom = fakeMatrixRoom, + room: FakeJoinedRoom = fakeJoinedRoom, ): CreatePollPresenter = CreatePollPresenter( repository = PollRepository(room, LiveTimelineProvider(room)), analyticsService = fakeAnalyticsService, diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt index f4f5147f82b..eeb4d0d2237 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt @@ -22,18 +22,16 @@ import io.element.android.features.poll.impl.model.DefaultPollContentStateFactor import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.dateformatter.test.FakeDateFormatter -import io.element.android.libraries.matrix.api.room.MatrixRoom 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.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline 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.testCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.getAndUpdate @@ -57,7 +55,7 @@ class PollHistoryPresenterTest { ), backwardPaginationStatus = backwardPaginationStatus ) - private val room = FakeMatrixRoom( + private val room = FakeJoinedRoom( liveTimeline = timeline ) @@ -155,8 +153,7 @@ class PollHistoryPresenterTest { } private fun TestScope.createPollHistoryPresenter( - room: MatrixRoom = FakeMatrixRoom(), - appCoroutineScope: CoroutineScope = this, + room: FakeJoinedRoom = FakeJoinedRoom(), endPollAction: EndPollAction = FakeEndPollAction(), sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory( @@ -166,7 +163,7 @@ class PollHistoryPresenterTest { ), ): PollHistoryPresenter { return PollHistoryPresenter( - appCoroutineScope = appCoroutineScope, + sessionCoroutineScope = this, sendPollResponseAction = sendPollResponseAction, endPollAction = endPollAction, pollHistoryItemFactory = pollHistoryItemFactory, 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 3da8d50d492..54df066bbed 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 @@ -131,7 +131,10 @@ class PollHistoryViewTest { rule.setPollHistoryViewView( state = state, ) - rule.onNodeWithText(answer.text).performClick() + rule.onNodeWithText( + text = answer.text, + useUnmergedTree = true, + ).performClick() eventsRecorder.assertSingle( PollHistoryEvents.SelectPollAnswer(eventId, answer.id) ) diff --git a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt index 293124d9fe0..f41d497b18c 100644 --- a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt +++ b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt @@ -13,7 +13,9 @@ 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.architecture.NodeInputs +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 kotlinx.parcelize.Parcelize interface PreferencesEntryPoint : FeatureEntryPoint { @@ -29,6 +31,7 @@ interface PreferencesEntryPoint : FeatureEntryPoint { } data class Params(val initialElement: InitialTarget) : NodeInputs + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder interface NodeBuilder { @@ -41,5 +44,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint { fun onOpenBugReport() fun onSecureBackupClick() fun onOpenRoomNotificationSettings(roomId: RoomId) + fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId) } } diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 59cac808dd4..8eb3c5d860a 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -1,3 +1,5 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.setupAnvil /* @@ -19,6 +21,25 @@ android { isIncludeAndroidResources = true } } + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigFieldStr( + name = "URL_COPYRIGHT", + value = BuildTimeConfig.URL_COPYRIGHT ?: "https://element.io/copyright", + ) + buildConfigFieldStr( + name = "URL_ACCEPTABLE_USE", + value = BuildTimeConfig.URL_ACCEPTABLE_USE ?: "https://element.io/acceptable-use-policy-terms", + ) + buildConfigFieldStr( + name = "URL_PRIVACY", + value = BuildTimeConfig.URL_PRIVACY ?: "https://element.io/privacy", + ) + } } setupAnvil() @@ -45,6 +66,7 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) + implementation(projects.libraries.uiUtils) implementation(projects.libraries.fullscreenintent.api) implementation(projects.features.rageshake.api) implementation(projects.features.lockscreen.api) @@ -53,9 +75,11 @@ dependencies { implementation(projects.features.licenses.api) implementation(projects.features.logout.api) implementation(projects.features.deactivation.api) - implementation(projects.features.roomlist.api) + implementation(projects.features.home.api) + implementation(projects.features.invite.api) implementation(projects.services.analytics.api) implementation(projects.services.analytics.compose) + implementation(projects.services.appnavstate.api) implementation(projects.services.toolbox.api) implementation(libs.datetime) implementation(libs.coil.compose) @@ -63,6 +87,9 @@ dependencies { implementation(libs.androidx.datastore.preferences) api(projects.features.preferences.api) + implementation(platform(libs.network.okhttp.bom)) + implementation(libs.network.okhttp) + testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) @@ -79,15 +106,13 @@ dependencies { testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushstore.test) testImplementation(projects.features.ftue.test) + testImplementation(projects.features.invite.test) testImplementation(projects.features.rageshake.test) - testImplementation(projects.features.rageshake.impl) testImplementation(projects.features.logout.test) - testImplementation(projects.libraries.indicator.impl) + testImplementation(projects.libraries.indicator.test) testImplementation(projects.libraries.pushproviders.test) - testImplementation(projects.features.logout.impl) testImplementation(projects.services.analytics.test) testImplementation(projects.services.toolbox.test) - testImplementation(projects.features.analytics.impl) testImplementation(projects.tests.testutils) testImplementation(libs.androidx.compose.ui.test.junit) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index 27987e7960d..234b4c683b0 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -39,9 +39,12 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.appyx.canPop import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope +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.user.MatrixUser import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint +import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -50,6 +53,7 @@ class PreferencesFlowNode @AssistedInject constructor( @Assisted plugins: List, private val lockScreenEntryPoint: LockScreenEntryPoint, private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint, + private val pushHistoryEntryPoint: PushHistoryEntryPoint, private val logoutEntryPoint: LogoutEntryPoint, private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint, private val accountDeactivationEntryPoint: AccountDeactivationEntryPoint, @@ -83,6 +87,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object TroubleshootNotifications : NavTarget + @Parcelize + data object PushHistory : NavTarget + @Parcelize data object LockScreenSettings : NavTarget @@ -182,6 +189,10 @@ class PreferencesFlowNode @AssistedInject constructor( override fun onTroubleshootNotificationsClick() { backstack.push(NavTarget.TroubleshootNotifications) } + + override fun onPushHistoryClick() { + backstack.push(NavTarget.PushHistory) + } } createNode(buildContext, listOf(notificationSettingsCallback)) } @@ -198,6 +209,23 @@ class PreferencesFlowNode @AssistedInject constructor( }) .build() } + NavTarget.PushHistory -> { + pushHistoryEntryPoint.nodeBuilder(this, buildContext) + .callback(object : PushHistoryEntryPoint.Callback { + override fun onDone() { + if (backstack.canPop()) { + backstack.pop() + } else { + navigateUp() + } + } + + override fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) { + plugins().forEach { it.navigateTo(sessionId, roomId, eventId) } + } + }) + .build() + } is NavTarget.EditDefaultNotificationSetting -> { val callback = object : EditDefaultNotificationSettingNode.Callback { override fun openRoomNotificationSettings(roomId: RoomId) { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutNode.kt index ea3a411d0a3..08be5069906 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutNode.kt @@ -8,9 +8,9 @@ package io.element.android.features.preferences.impl.about import android.app.Activity +import androidx.activity.compose.LocalActivity 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 @@ -41,7 +41,7 @@ class AboutNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val activity = LocalContext.current as Activity + val activity = requireNotNull(LocalActivity.current) val isDark = ElementTheme.isLightTheme.not() val state = presenter.present() AboutView( diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt index ce8b53a977a..9d2018f4a74 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt @@ -12,9 +12,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.components.preferences.PreferencePage -import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -31,13 +32,17 @@ fun AboutView( title = stringResource(id = CommonStrings.common_about) ) { state.elementLegals.forEach { elementLegal -> - PreferenceText( - title = stringResource(id = elementLegal.titleRes), + ListItem( + headlineContent = { + Text(stringResource(id = elementLegal.titleRes)) + }, onClick = { onElementLegalClick(elementLegal) } ) } - PreferenceText( - title = stringResource(id = CommonStrings.common_open_source_licenses), + ListItem( + headlineContent = { + Text(stringResource(id = CommonStrings.common_open_source_licenses)) + }, onClick = onOpenSourceLicensesClick, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt index a94e1637ba5..a5de31f05d8 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt @@ -8,11 +8,12 @@ package io.element.android.features.preferences.impl.about import androidx.annotation.StringRes +import io.element.android.features.preferences.impl.BuildConfig import io.element.android.libraries.ui.strings.CommonStrings -private const val COPYRIGHT_URL = "https://element.io/copyright" -private const val USE_POLICY_URL = "https://element.io/acceptable-use-policy-terms" -private const val PRIVACY_URL = "https://element.io/privacy" +private const val COPYRIGHT_URL = BuildConfig.URL_COPYRIGHT +private const val USE_POLICY_URL = BuildConfig.URL_ACCEPTABLE_USE +private const val PRIVACY_URL = BuildConfig.URL_PRIVACY sealed class ElementLegal( @StringRes val titleRes: Int, 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 45d7ae615b1..fc78ae561a5 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 @@ -7,13 +7,13 @@ package io.element.android.features.preferences.impl.advanced -import io.element.android.compound.theme.Theme +import io.element.android.libraries.matrix.api.media.MediaPreviewValue sealed interface AdvancedSettingsEvents { data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvents - data object ChangeTheme : AdvancedSettingsEvents - data object CancelChangeTheme : AdvancedSettingsEvents - data class SetTheme(val theme: Theme) : AdvancedSettingsEvents + data class SetTheme(val theme: ThemeOption) : AdvancedSettingsEvents + data class SetTimelineMediaPreviewValue(val value: MediaPreviewValue) : AdvancedSettingsEvents + data class SetHideInviteAvatars(val value: Boolean) : AdvancedSettingsEvents } 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 b256997d1c3..be5f11116c4 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 @@ -9,58 +9,74 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import io.element.android.compound.theme.Theme import io.element.android.compound.theme.mapToTheme import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject class AdvancedSettingsPresenter @Inject constructor( private val appPreferencesStore: AppPreferencesStore, private val sessionPreferencesStore: SessionPreferencesStore, + private val mediaPreviewConfigStateStore: MediaPreviewConfigStateStore, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, ) : Presenter { @Composable override fun present(): AdvancedSettingsState { - val localCoroutineScope = rememberCoroutineScope() - val isDeveloperModeEnabled by appPreferencesStore - .isDeveloperModeEnabledFlow() - .collectAsState(initial = false) - val isSharePresenceEnabled by sessionPreferencesStore - .isSharePresenceEnabled() - .collectAsState(initial = true) - val doesCompressMedia by sessionPreferencesStore - .doesCompressMedia() - .collectAsState(initial = true) - val theme by remember { + val isDeveloperModeEnabled by remember { + appPreferencesStore.isDeveloperModeEnabledFlow() + }.collectAsState(initial = false) + val isSharePresenceEnabled by remember { + sessionPreferencesStore.isSharePresenceEnabled() + }.collectAsState(initial = true) + val doesCompressMedia by remember { + sessionPreferencesStore.doesCompressMedia() + }.collectAsState(initial = true) + val theme = remember { appPreferencesStore.getThemeFlow().mapToTheme() + }.collectAsState(initial = Theme.System) + + val mediaPreviewConfigState = mediaPreviewConfigStateStore.state() + + val themeOption by remember { + derivedStateOf { + when (theme.value) { + Theme.System -> ThemeOption.System + Theme.Dark -> ThemeOption.Dark + Theme.Light -> ThemeOption.Light + } + } } - .collectAsState(initial = Theme.System) - var showChangeThemeDialog by remember { mutableStateOf(false) } fun handleEvents(event: AdvancedSettingsEvents) { when (event) { - is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch { + is AdvancedSettingsEvents.SetDeveloperModeEnabled -> sessionCoroutineScope.launch { appPreferencesStore.setDeveloperModeEnabled(event.enabled) } - is AdvancedSettingsEvents.SetSharePresenceEnabled -> localCoroutineScope.launch { + is AdvancedSettingsEvents.SetSharePresenceEnabled -> sessionCoroutineScope.launch { sessionPreferencesStore.setSharePresence(event.enabled) } - is AdvancedSettingsEvents.SetCompressMedia -> localCoroutineScope.launch { + is AdvancedSettingsEvents.SetCompressMedia -> sessionCoroutineScope.launch { sessionPreferencesStore.setCompressMedia(event.compress) } - AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false - AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true - is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch { - appPreferencesStore.setTheme(event.theme.name) - showChangeThemeDialog = false + is AdvancedSettingsEvents.SetTheme -> sessionCoroutineScope.launch { + when (event.theme) { + ThemeOption.System -> appPreferencesStore.setTheme(Theme.System.name) + ThemeOption.Dark -> appPreferencesStore.setTheme(Theme.Dark.name) + ThemeOption.Light -> appPreferencesStore.setTheme(Theme.Light.name) + } } + is AdvancedSettingsEvents.SetHideInviteAvatars -> mediaPreviewConfigStateStore.setHideInviteAvatars(event.value) + is AdvancedSettingsEvents.SetTimelineMediaPreviewValue -> mediaPreviewConfigStateStore.setTimelineMediaPreviewValue(event.value) } } @@ -68,9 +84,9 @@ class AdvancedSettingsPresenter @Inject constructor( isDeveloperModeEnabled = isDeveloperModeEnabled, isSharePresenceEnabled = isSharePresenceEnabled, doesCompressMedia = doesCompressMedia, - theme = theme, - showChangeThemeDialog = showChangeThemeDialog, - eventSink = { handleEvents(it) } + theme = themeOption, + mediaPreviewConfigState = mediaPreviewConfigState, + eventSink = ::handleEvents, ) } } 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 a202ccc95fd..ef41ab26b3b 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 @@ -7,13 +7,35 @@ package io.element.android.features.preferences.impl.advanced -import io.element.android.compound.theme.Theme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.preferences.DropdownOption +import io.element.android.libraries.ui.strings.CommonStrings data class AdvancedSettingsState( val isDeveloperModeEnabled: Boolean, val isSharePresenceEnabled: Boolean, val doesCompressMedia: Boolean, - val theme: Theme, - val showChangeThemeDialog: Boolean, + val theme: ThemeOption, + val mediaPreviewConfigState: MediaPreviewConfigState, val eventSink: (AdvancedSettingsEvents) -> Unit ) + +enum class ThemeOption : DropdownOption { + System { + @Composable + @ReadOnlyComposable + override fun getText(): String = stringResource(CommonStrings.common_system) + }, + Dark { + @Composable + @ReadOnlyComposable + override fun getText(): String = stringResource(CommonStrings.common_dark) + }, + Light { + @Composable + @ReadOnlyComposable + 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 d8e0730bf02..42e63134d1c 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 @@ -8,16 +8,20 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.compound.theme.Theme +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.media.MediaPreviewValue open class AdvancedSettingsStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aAdvancedSettingsState(), aAdvancedSettingsState(isDeveloperModeEnabled = true), - aAdvancedSettingsState(showChangeThemeDialog = true), aAdvancedSettingsState(isSharePresenceEnabled = true), aAdvancedSettingsState(doesCompressMedia = true), + aAdvancedSettingsState(hideInviteAvatars = true), + aAdvancedSettingsState(timelineMediaPreviewValue = MediaPreviewValue.Off), + aAdvancedSettingsState(setHideInviteAvatarsAction = AsyncAction.Loading), + aAdvancedSettingsState(setTimelineMediaPreviewAction = AsyncAction.Loading), ) } @@ -25,13 +29,22 @@ fun aAdvancedSettingsState( isDeveloperModeEnabled: Boolean = false, isSharePresenceEnabled: Boolean = false, doesCompressMedia: Boolean = false, - showChangeThemeDialog: Boolean = false, + theme: ThemeOption = ThemeOption.System, + hideInviteAvatars: Boolean = false, + timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On, + setTimelineMediaPreviewAction: AsyncAction = AsyncAction.Uninitialized, + setHideInviteAvatarsAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (AdvancedSettingsEvents) -> Unit = {}, ) = AdvancedSettingsState( isDeveloperModeEnabled = isDeveloperModeEnabled, isSharePresenceEnabled = isSharePresenceEnabled, doesCompressMedia = doesCompressMedia, - theme = Theme.System, - showChangeThemeDialog = showChangeThemeDialog, + theme = theme, + mediaPreviewConfigState = MediaPreviewConfigState( + hideInviteAvatars = hideInviteAvatars, + timelineMediaPreviewValue = timelineMediaPreviewValue, + setTimelineMediaPreviewAction = setTimelineMediaPreviewAction, + setHideInviteAvatarsAction = setHideInviteAvatarsAction + ), 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 0332e98a954..f5c13866584 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 @@ -7,27 +7,37 @@ package io.element.android.features.preferences.impl.advanced +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import im.vector.app.features.analytics.plan.Interaction -import io.element.android.compound.theme.Theme -import io.element.android.compound.theme.themes import io.element.android.features.preferences.impl.R -import io.element.android.libraries.designsystem.components.dialogs.ListOption -import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown import io.element.android.libraries.designsystem.components.preferences.PreferencePage -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch +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.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.Text +import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost +import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState +import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState +import io.element.android.libraries.matrix.api.media.MediaPreviewValue 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.toImmutableList +import kotlinx.collections.immutable.toPersistentList @Composable fun AdvancedSettingsView( @@ -36,20 +46,28 @@ fun AdvancedSettingsView( modifier: Modifier = Modifier, ) { val analyticsService = LocalAnalyticsService.current + + val snackbarDispatcher = LocalSnackbarDispatcher.current + val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() + val snackbarHostState = rememberSnackbarHostState(snackbarMessage = snackbarMessage) + PreferencePage( modifier = modifier, onBackClick = onBackClick, - title = stringResource(id = CommonStrings.common_advanced_settings) + title = stringResource(id = CommonStrings.common_advanced_settings), + snackbarHost = { + SnackbarHost( + snackbarHostState, + modifier = Modifier.navigationBarsPadding() + ) + } ) { - ListItem( - headlineContent = { - Text(text = stringResource(id = CommonStrings.common_appearance)) - }, - trailingContent = ListItemContent.Text( - state.theme.toHumanReadable() - ), - onClick = { - state.eventSink(AdvancedSettingsEvents.ChangeTheme) + PreferenceDropdown( + title = stringResource(id = CommonStrings.common_appearance), + selectedOption = state.theme, + options = ThemeOption.entries.toPersistentList(), + onSelectOption = { themeOption -> + state.eventSink(AdvancedSettingsEvents.SetTheme(themeOption)) } ) ListItem( @@ -98,45 +116,89 @@ fun AdvancedSettingsView( state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue)) } ) + ModerationAndSafety(state) } +} - if (state.showChangeThemeDialog) { - SingleSelectionDialog( - options = getOptions(), - initialSelection = themes.indexOf(state.theme), - onSelectOption = { - state.eventSink( - AdvancedSettingsEvents.SetTheme( - themes[it] - ) +@Composable +private fun ModerationAndSafety( + state: AdvancedSettingsState, + modifier: Modifier = Modifier, +) { + PreferenceCategory( + modifier = modifier, + title = stringResource(R.string.screen_advanced_settings_moderation_and_safety_section_title), + showTopDivider = true + ) { + PreferenceSwitch( + title = stringResource(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title), + isChecked = state.mediaPreviewConfigState.hideInviteAvatars, + onCheckedChange = { + state.eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(it)) + }, + enabled = !state.mediaPreviewConfigState.setHideInviteAvatarsAction.isLoading() + ) + ListSectionHeader( + title = stringResource(R.string.screen_advanced_settings_show_media_timeline_title), + hasDivider = false, + description = { + ListSupportingText( + text = stringResource(R.string.screen_advanced_settings_show_media_timeline_subtitle), + contentPadding = ListSupportingTextDefaults.Padding.None, ) + } + ) + ListItem( + headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_always_hide)) }, + leadingContent = ListItemContent.RadioButton( + selected = state.mediaPreviewConfigState.timelineMediaPreviewValue == MediaPreviewValue.Off, + compact = true + ), + onClick = { + state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) + }, + enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading() + ) + ListItem( + headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_private_rooms)) }, + leadingContent = ListItemContent.RadioButton( + selected = state.mediaPreviewConfigState.timelineMediaPreviewValue == MediaPreviewValue.Private, + compact = true + ), + onClick = { + state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) }, - onDismissRequest = { state.eventSink(AdvancedSettingsEvents.CancelChangeTheme) }, + enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading() + ) + ListItem( + headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_always_show)) }, + leadingContent = ListItemContent.RadioButton( + selected = state.mediaPreviewConfigState.timelineMediaPreviewValue == MediaPreviewValue.On, + compact = true + ), + onClick = { + state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) + }, + enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading() ) } } +@PreviewWithLargeHeight @Composable -private fun getOptions(): ImmutableList { - return themes.map { - ListOption(title = it.toHumanReadable()) - }.toImmutableList() -} +internal fun AdvancedSettingsViewLightPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = + ElementPreviewLight { ContentToPreview(state) } +@PreviewWithLargeHeight @Composable -private fun Theme.toHumanReadable(): String { - return stringResource( - id = when (this) { - Theme.System -> CommonStrings.common_system - Theme.Dark -> CommonStrings.common_dark - Theme.Light -> CommonStrings.common_light - } - ) -} +internal fun AdvancedSettingsViewDarkPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = + ElementPreviewDark { ContentToPreview(state) } -@PreviewsDayNight +@ExcludeFromCoverage @Composable -internal fun AdvancedSettingsViewPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = - ElementPreview { - AdvancedSettingsView(state = state, onBackClick = { }) - } +private fun ContentToPreview(state: AdvancedSettingsState) { + AdvancedSettingsView( + state = state, + onBackClick = { } + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStore.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStore.kt new file mode 100644 index 00000000000..49f199ec3f8 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStore.kt @@ -0,0 +1,122 @@ +/* + * 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.preferences.impl.advanced + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.media.MediaPreviewService +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +data class MediaPreviewConfigState( + val hideInviteAvatars: Boolean, + val timelineMediaPreviewValue: MediaPreviewValue, + val setHideInviteAvatarsAction: AsyncAction, + val setTimelineMediaPreviewAction: AsyncAction, +) + +interface MediaPreviewConfigStateStore { + @Composable + fun state(): MediaPreviewConfigState + fun setHideInviteAvatars(hide: Boolean) + fun setTimelineMediaPreviewValue(value: MediaPreviewValue) +} + +@ContributesBinding(SessionScope::class) +@SingleIn(SessionScope::class) +class DefaultMediaPreviewConfigStateStore @Inject constructor( + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, + private val mediaPreviewService: MediaPreviewService, + private val snackbarDispatcher: SnackbarDispatcher, +) : MediaPreviewConfigStateStore { + private val hideInviteAvatars = mutableStateOf(false) + private val timelineMediaPreviewValue = mutableStateOf(MediaPreviewValue.On) + private val setHideInviteAvatarsAction = mutableStateOf>(AsyncAction.Uninitialized) + private val setTimelineMediaPreviewAction = mutableStateOf>(AsyncAction.Uninitialized) + + init { + val configFlow = mediaPreviewService.mediaPreviewConfigFlow + val hideInviteAvatarsFlow = configFlow.map { it.hideInviteAvatar }.distinctUntilChanged() + val timelineMediaPreviewFlow = configFlow.map { it.mediaPreviewValue }.distinctUntilChanged() + + hideInviteAvatarsFlow + .onEach { + Timber.d("Hide invite avatars changed to $it") + hideInviteAvatars.value = it + } + .launchIn(sessionCoroutineScope) + + timelineMediaPreviewFlow + .onEach { + Timber.d("Timeline media preview value changed to $it") + timelineMediaPreviewValue.value = it + } + .launchIn(sessionCoroutineScope) + } + + @Composable + override fun state(): MediaPreviewConfigState { + return MediaPreviewConfigState( + hideInviteAvatars = hideInviteAvatars.value, + timelineMediaPreviewValue = timelineMediaPreviewValue.value, + setHideInviteAvatarsAction = setHideInviteAvatarsAction.value, + setTimelineMediaPreviewAction = setTimelineMediaPreviewAction.value, + ) + } + + override fun setHideInviteAvatars(hide: Boolean) { + sessionCoroutineScope.launch { + val prevHideInviteAvatars = hideInviteAvatars.value + if (prevHideInviteAvatars == hide) return@launch + Timber.d("Setting hide invite avatars to $hide") + hideInviteAvatars.value = hide + runUpdatingState(setHideInviteAvatarsAction) { + mediaPreviewService + .setHideInviteAvatars(hide) + .onFailure { + hideInviteAvatars.value = prevHideInviteAvatars + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_something_went_wrong_message)) + } + } + } + } + + override fun setTimelineMediaPreviewValue(value: MediaPreviewValue) { + sessionCoroutineScope.launch { + val prevTimelineMediaPreviewValue = timelineMediaPreviewValue.value + if (prevTimelineMediaPreviewValue == value) return@launch + Timber.d("Setting timeline media preview value to $value") + timelineMediaPreviewValue.value = value + runUpdatingState(setTimelineMediaPreviewAction) { + mediaPreviewService + .setMediaPreviewValue(value) + .onFailure { + timelineMediaPreviewValue.value = prevTimelineMediaPreviewValue + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_something_went_wrong_message)) + } + } + } + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt index 19365e307c2..c673276a176 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt @@ -44,17 +44,17 @@ class BlockedUsersPresenter @Inject constructor( mutableStateOf(AsyncAction.Uninitialized) } - val renderBlockedUsersDetail = featureFlagService - .isFeatureEnabledFlow(FeatureFlags.ShowBlockedUsersDetails) - .collectAsState(initial = false) + val renderBlockedUsersDetail by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.ShowBlockedUsersDetails) + }.collectAsState(initial = false) val ignoredUserIds by matrixClient.ignoredUsersFlow.collectAsState() val ignoredMatrixUser by produceState( initialValue = ignoredUserIds.map { MatrixUser(userId = it) }, - key1 = renderBlockedUsersDetail.value, + key1 = renderBlockedUsersDetail, key2 = ignoredUserIds ) { value = ignoredUserIds.map { - if (renderBlockedUsersDetail.value) { + if (renderBlockedUsersDetail) { matrixClient.getProfile(it).getOrNull() } else { null diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt index 57335c5d97b..ba90f93ba57 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt @@ -21,7 +21,7 @@ class BlockedUsersStateProvider : PreviewParameterProvider { aBlockedUsersState(blockedUsers = emptyList()), aBlockedUsersState(unblockUserAction = AsyncAction.ConfirmingNoParams), aBlockedUsersState(unblockUserAction = AsyncAction.Loading), - aBlockedUsersState(unblockUserAction = AsyncAction.Failure(Throwable("Failed to unblock user"))), + aBlockedUsersState(unblockUserAction = AsyncAction.Failure(RuntimeException("Failed to unblock user"))), aBlockedUsersState(unblockUserAction = AsyncAction.Success(Unit)), ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt index a892ebba44e..56b5dd709ef 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.compound.theme.ElementTheme import io.element.android.features.preferences.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.async.AsyncIndicator @@ -29,9 +28,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -49,12 +46,7 @@ fun BlockedUsersView( Scaffold( topBar = { TopAppBar( - title = { - Text( - text = stringResource(CommonStrings.common_blocked_users), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(CommonStrings.common_blocked_users), navigationIcon = { BackButton(onClick = onBackClick) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index 11a11ada722..ced7b8d2b42 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -9,12 +9,12 @@ package io.element.android.features.preferences.impl.developer import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack sealed interface DeveloperSettingsEvents { data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents - data class SetSimplifiedSlidingSyncEnabled(val isEnabled: Boolean) : DeveloperSettingsEvents - data class SetHideImagesAndVideos(val value: Boolean) : DeveloperSettingsEvents data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents + data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : DeveloperSettingsEvents data object ClearCache : DeveloperSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt index 91f8afcb8ef..fafe5fc9205 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt @@ -7,10 +7,9 @@ package io.element.android.features.preferences.impl.developer -import android.app.Activity +import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import com.airbnb.android.showkase.models.Showkase import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -29,7 +28,7 @@ class DeveloperSettingsNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { @Composable override fun View(modifier: Modifier) { - val activity = LocalContext.current as Activity + val activity = requireNotNull(LocalActivity.current) fun openShowkase() { val intent = Showkase.getBrowserIntent(activity) activity.startActivity(intent) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index 66c8d8d7b3e..9bd3d93dacf 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -15,11 +15,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshots.SnapshotStateMap -import io.element.android.appconfig.ElementCallConfig -import io.element.android.features.logout.api.LogoutUseCase import io.element.android.features.preferences.impl.developer.tracing.toLogLevel import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase @@ -30,15 +29,20 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.bool.orFalse +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.Feature import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.net.URL @@ -51,7 +55,6 @@ class DeveloperSettingsPresenter @Inject constructor( private val rageshakePresenter: Presenter, private val appPreferencesStore: AppPreferencesStore, private val buildMeta: BuildMeta, - private val logoutUseCase: LogoutUseCase, ) : Presenter { @Composable override fun present(): DeveloperSettingsState { @@ -69,20 +72,21 @@ class DeveloperSettingsPresenter @Inject constructor( val clearCacheAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } - val customElementCallBaseUrl by appPreferencesStore - .getCustomElementCallBaseUrlFlow() - .collectAsState(initial = null) - val isSimplifiedSlidingSyncEnabled by appPreferencesStore - .isSimplifiedSlidingSyncEnabledFlow() - .collectAsState(initial = false) - val hideImagesAndVideos by appPreferencesStore - .doesHideImagesAndVideosFlow() - .collectAsState(initial = false) + val customElementCallBaseUrl by remember { + appPreferencesStore + .getCustomElementCallBaseUrlFlow() + }.collectAsState(initial = null) - val tracingLogLevel by appPreferencesStore - .getTracingLogLevelFlow() - .map { AsyncData.Success(it.toLogLevelItem()) } - .collectAsState(initial = AsyncData.Uninitialized) + val tracingLogLevelFlow = remember { + appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) } + } + val tracingLogLevel by tracingLogLevelFlow.collectAsState(initial = AsyncData.Uninitialized) + val tracingLogPacks by produceState(persistentListOf()) { + appPreferencesStore.getTracingLogPacksFlow() + // Sort the entries alphabetically by its title + .map { it.sortedBy { it.title }.toPersistentList() } + .collectLatest { value = it } + } LaunchedEffect(Unit) { FeatureFlags.entries @@ -117,23 +121,22 @@ class DeveloperSettingsPresenter @Inject constructor( triggerClearCache = { handleEvents(DeveloperSettingsEvents.ClearCache) } ) is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch { - // If the URL is either empty or the default one, we want to save 'null' to remove the custom URL - val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL } + val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() } appPreferencesStore.setCustomElementCallBaseUrl(urlToSave) } DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) - is DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled -> coroutineScope.launch { - appPreferencesStore.setSimplifiedSlidingSyncEnabled(event.isEnabled) - runCatching { - logoutUseCase.logout(ignoreSdkError = true) - } - } - is DeveloperSettingsEvents.SetHideImagesAndVideos -> coroutineScope.launch { - appPreferencesStore.setHideImagesAndVideos(event.value) - } is DeveloperSettingsEvents.SetTracingLogLevel -> coroutineScope.launch { appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel()) } + is DeveloperSettingsEvents.ToggleTracingLogPack -> coroutineScope.launch { + val currentPacks = tracingLogPacks.toMutableSet() + if (currentPacks.contains(event.logPack)) { + currentPacks.remove(event.logPack) + } else { + currentPacks.add(event.logPack) + } + appPreferencesStore.setTracingLogPacks(currentPacks) + } } } @@ -144,12 +147,10 @@ class DeveloperSettingsPresenter @Inject constructor( rageshakeState = rageshakeState, customElementCallBaseUrlState = CustomElementCallBaseUrlState( baseUrl = customElementCallBaseUrl, - defaultUrl = ElementCallConfig.DEFAULT_BASE_URL, validator = ::customElementCallUrlValidator, ), - isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled, - hideImagesAndVideos = hideImagesAndVideos, tracingLogLevel = tracingLogLevel, + tracingLogPacks = tracingLogPacks, eventSink = ::handleEvents ) } @@ -201,8 +202,8 @@ class DeveloperSettingsPresenter @Inject constructor( } private fun customElementCallUrlValidator(url: String?): Boolean { - return runCatching { - if (url.isNullOrEmpty()) return@runCatching + return runCatchingExceptions { + if (url.isNullOrEmpty()) return@runCatchingExceptions val parsedUrl = URL(url) if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol") if (parsedUrl.host.isNullOrBlank()) error("Missing host") diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index c9641ab686a..93e7b9ae7b5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -12,6 +12,7 @@ import io.element.android.features.rageshake.api.preferences.RageshakePreference import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack import kotlinx.collections.immutable.ImmutableList data class DeveloperSettingsState( @@ -20,14 +21,12 @@ data class DeveloperSettingsState( val rageshakeState: RageshakePreferencesState, val clearCacheAction: AsyncAction, val customElementCallBaseUrlState: CustomElementCallBaseUrlState, - val isSimpleSlidingSyncEnabled: Boolean, - val hideImagesAndVideos: Boolean, val tracingLogLevel: AsyncData, + val tracingLogPacks: ImmutableList, val eventSink: (DeveloperSettingsEvents) -> Unit ) data class CustomElementCallBaseUrlState( val baseUrl: String?, - val defaultUrl: String, val validator: (String?) -> Boolean, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt index 869c0105292..1585a3b8ddb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -13,6 +13,8 @@ import io.element.android.features.rageshake.api.preferences.aRageshakePreferenc import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList +import io.element.android.libraries.matrix.api.tracing.TraceLogPack +import kotlinx.collections.immutable.toPersistentList open class DeveloperSettingsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -32,8 +34,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(), - isSimplifiedSlidingSyncEnabled: Boolean = false, - hideImagesAndVideos: Boolean = false, + traceLogPacks: List = emptyList(), eventSink: (DeveloperSettingsEvents) -> Unit = {}, ) = DeveloperSettingsState( features = aFeatureUiModelList(), @@ -41,18 +42,15 @@ fun aDeveloperSettingsState( cacheSize = AsyncData.Success("1.2 MB"), clearCacheAction = clearCacheAction, customElementCallBaseUrlState = customElementCallBaseUrlState, - isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled, - hideImagesAndVideos = hideImagesAndVideos, tracingLogLevel = AsyncData.Success(LogLevelItem.INFO), + tracingLogPacks = traceLogPacks.toPersistentList(), eventSink = eventSink, ) fun aCustomElementCallBaseUrlState( baseUrl: String? = null, - defaultUrl: String = "https://call.element.io", validator: (String?) -> Boolean = { true }, ) = CustomElementCallBaseUrlState( baseUrl = baseUrl, - defaultUrl = defaultUrl, validator = validator, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 7b0b39db90e..9a9cc61d680 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -7,25 +7,34 @@ package io.element.android.features.preferences.impl.developer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView +import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.featureflag.ui.FeatureListView import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.toPersistentList @@ -42,7 +51,6 @@ fun DeveloperSettingsView( title = stringResource(id = CommonStrings.common_developer_options) ) { // Note: this is OK to hardcode strings in this debug screen. - SettingsCategory(state) PreferenceCategory( title = "Feature flags", showTopDivider = true, @@ -50,6 +58,7 @@ fun DeveloperSettingsView( FeatureListContent(state) } ElementCallCategory(state = state) + PreferenceCategory(title = "Rust SDK") { PreferenceDropdown( title = "Tracing log level", @@ -57,21 +66,31 @@ fun DeveloperSettingsView( selectedOption = state.tracingLogLevel.dataOrNull(), options = LogLevelItem.entries.toPersistentList(), onSelectOption = { logLevel -> - state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(logLevel)) + state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(logLevel)) } ) - PreferenceSwitch( - title = "Enable Simplified Sliding Sync", - subtitle = "When toggled you'll be logged out of the app and will need to log in again.", - isChecked = state.isSimpleSlidingSyncEnabled, - onCheckedChange = { - state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(it)) - } + } + PreferenceCategory(title = "Enable trace logs per SDK feature") { + Text( + text = "Requires app reboot", + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) ) + for (logPack in TraceLogPack.entries) { + PreferenceSwitch( + title = logPack.title, + isChecked = state.tracingLogPacks.contains(logPack), + onCheckedChange = { isChecked -> state.eventSink(DeveloperSettingsEvents.ToggleTracingLogPack(logPack, isChecked)) } + ) + } } + PreferenceCategory(title = "Showkase") { - PreferenceText( - title = "Open Showkase browser", + ListItem( + headlineContent = { + Text("Open Showkase browser") + }, onClick = onOpenShowkase ) } @@ -79,17 +98,31 @@ fun DeveloperSettingsView( state = state.rageshakeState, ) PreferenceCategory(title = "Crash", showTopDivider = false) { - PreferenceText( - title = "Crash the app 💥", + ListItem( + headlineContent = { + Text("Crash the app 💥") + }, onClick = { error("This crash is a test.") } ) } val cache = state.cacheSize PreferenceCategory(title = "Cache", showTopDivider = false) { - PreferenceText( - title = "Clear cache", - currentValue = cache.dataOrNull(), - loadingCurrentValue = state.cacheSize.isLoading() || state.clearCacheAction.isLoading(), + ListItem( + headlineContent = { + Text("Clear cache") + }, + trailingContent = if (state.cacheSize.isLoading() || state.clearCacheAction.isLoading()) { + ListItemContent.Custom { + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .size(20.dp), + strokeWidth = 2.dp + ) + } + } else { + ListItemContent.Text(cache.dataOrNull().orEmpty()) + }, onClick = { if (state.clearCacheAction.isLoading().not()) { state.eventSink(DeveloperSettingsEvents.ClearCache) @@ -100,44 +133,26 @@ fun DeveloperSettingsView( } } -@Composable -private fun SettingsCategory( - state: DeveloperSettingsState, -) { - PreferenceCategory(title = "Preferences", showTopDivider = false) { - PreferenceSwitch( - title = "Hide image & video previews", - subtitle = "When toggled image & video will not render in the timeline by default.", - isChecked = state.hideImagesAndVideos, - onCheckedChange = { - state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(it)) - } - ) - } -} - @Composable private fun ElementCallCategory( state: DeveloperSettingsState, ) { PreferenceCategory(title = "Element Call", showTopDivider = true) { val callUrlState = state.customElementCallBaseUrlState - fun isUsingDefaultUrl(value: String?): Boolean { - return value.isNullOrEmpty() || value == callUrlState.defaultUrl - } - val supportingText = if (isUsingDefaultUrl(callUrlState.baseUrl)) { + val supportingText = if (callUrlState.baseUrl.isNullOrEmpty()) { stringResource(R.string.screen_advanced_settings_element_call_base_url_description) } else { callUrlState.baseUrl } PreferenceTextField( headline = stringResource(R.string.screen_advanced_settings_element_call_base_url), - value = callUrlState.baseUrl ?: callUrlState.defaultUrl, + value = callUrlState.baseUrl, + placeholder = "https://.../room", supportingText = supportingText, validation = callUrlState.validator, onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error), - displayValue = { value -> !isUsingDefaultUrl(value) }, + displayValue = { value -> !value.isNullOrEmpty() }, keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri), onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) } ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/LogLevelItem.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/LogLevelItem.kt index 9707fc30e7b..7148cff57fb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/LogLevelItem.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/LogLevelItem.kt @@ -7,22 +7,28 @@ package io.element.android.features.preferences.impl.developer.tracing +import androidx.compose.runtime.Composable import io.element.android.libraries.designsystem.components.preferences.DropdownOption enum class LogLevelItem : DropdownOption { ERROR { - override val text: String = "Error" + @Composable + override fun getText(): String = "Error" }, WARN { - override val text: String = "Warn" + @Composable + override fun getText(): String = "Warn" }, INFO { - override val text: String = "Info" + @Composable + override fun getText(): String = "Info" }, DEBUG { - override val text: String = "Debug" + @Composable + override fun getText(): String = "Debug" }, TRACE { - override val text: String = "Trace" + @Composable + override fun getText(): String = "Trace" } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt index 15a6afcd89a..75f38f60eeb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt @@ -27,6 +27,7 @@ class NotificationSettingsNode @AssistedInject constructor( interface Callback : Plugin { fun editDefaultNotificationMode(isOneToOne: Boolean) fun onTroubleshootNotificationsClick() + fun onPushHistoryClick() } private val callbacks = plugins() @@ -39,6 +40,10 @@ class NotificationSettingsNode @AssistedInject constructor( callbacks.forEach { it.onTroubleshootNotificationsClick() } } + private fun onPushHistoryClick() { + callbacks.forEach { it.onPushHistoryClick() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -47,6 +52,7 @@ class NotificationSettingsNode @AssistedInject constructor( onOpenEditDefault = { openEditDefault(isOneToOne = it) }, onBackClick = ::navigateUp, onTroubleshootNotificationsClick = ::onTroubleshootNotificationsClick, + onPushHistoryClick = ::onPushHistoryClick, modifier = modifier, ) } 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 a78b94c8661..93b7f27b065 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 @@ -22,6 +22,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingStateNoSuccess +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -58,9 +59,9 @@ class NotificationSettingsPresenter @Inject constructor( val changeNotificationSettingAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val localCoroutineScope = rememberCoroutineScope() - val appNotificationsEnabled = userPushStore - .getNotificationEnabledForDevice() - .collectAsState(initial = false) + val appNotificationsEnabled by remember { + userPushStore.getNotificationEnabledForDevice() + }.collectAsState(initial = false) val matrixSettings: MutableState = remember { mutableStateOf(NotificationSettingsState.MatrixSettings.Uninitialized) @@ -83,19 +84,19 @@ class NotificationSettingsPresenter @Inject constructor( } } } - // List of Distributor names - val distributorNames = remember { - distributors.map { it.second.name }.toImmutableList() + // List of Distributors + val availableDistributors = remember { + distributors.map { it.second }.toImmutableList() } - var currentDistributorName by remember { mutableStateOf>(AsyncData.Uninitialized) } + var currentDistributor by remember { mutableStateOf>(AsyncData.Uninitialized) } var refreshPushProvider by remember { mutableIntStateOf(0) } LaunchedEffect(refreshPushProvider) { val p = pushService.getCurrentPushProvider() - val name = p?.getCurrentDistributor(matrixClient.sessionId)?.name - currentDistributorName = if (name != null) { - AsyncData.Success(name) + val distributor = p?.getCurrentDistributor(matrixClient.sessionId) + currentDistributor = if (distributor != null) { + AsyncData.Success(distributor) } else { AsyncData.Failure(Exception("Failed to get current push provider")) } @@ -108,24 +109,23 @@ class NotificationSettingsPresenter @Inject constructor( ) = launch { showChangePushProviderDialog = false data ?: return@launch - // No op if the value is the same. - if (data.second.name == currentDistributorName.dataOrNull()) return@launch - currentDistributorName = AsyncData.Loading(currentDistributorName.dataOrNull()) - data.let { (pushProvider, distributor) -> - pushService.registerWith( - matrixClient = matrixClient, - pushProvider = pushProvider, - distributor = distributor + val (pushProvider, distributor) = data + // No op if the distributor is the same. + if (distributor == currentDistributor.dataOrNull()) return@launch + currentDistributor = AsyncData.Loading(currentDistributor.dataOrNull()) + pushService.registerWith( + matrixClient = matrixClient, + pushProvider = pushProvider, + distributor = distributor + ) + .fold( + { + refreshPushProvider++ + }, + { + currentDistributor = AsyncData.Failure(it) + } ) - .fold( - { - refreshPushProvider++ - }, - { - currentDistributorName = AsyncData.Failure(it) - } - ) - } } fun handleEvents(event: NotificationSettingsEvents) { @@ -159,11 +159,11 @@ class NotificationSettingsPresenter @Inject constructor( matrixSettings = matrixSettings.value, appSettings = NotificationSettingsState.AppSettings( systemNotificationsEnabled = systemNotificationsEnabled.value, - appNotificationsEnabled = appNotificationsEnabled.value + appNotificationsEnabled = appNotificationsEnabled, ), changeNotificationSettingAction = changeNotificationSettingAction.value, - currentPushDistributor = currentDistributorName, - availablePushDistributors = distributorNames, + currentPushDistributor = currentDistributor, + availablePushDistributors = availableDistributors, showChangePushProviderDialog = showChangePushProviderDialog, fullScreenIntentPermissionsState = key(refreshFullScreenIntentSettings) { fullScreenIntentPermissionsPresenter.present() }, eventSink = ::handleEvents @@ -210,7 +210,7 @@ class NotificationSettingsPresenter @Inject constructor( } private fun CoroutineScope.fixConfigurationMismatch(target: MutableState) = launch { - runCatching { + runCatchingExceptions { val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow() val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow() diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt index 5f3b00a1740..a7a1bf91923 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt @@ -12,6 +12,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.pushproviders.api.Distributor import kotlinx.collections.immutable.ImmutableList @Immutable @@ -19,8 +20,8 @@ data class NotificationSettingsState( val matrixSettings: MatrixSettings, val appSettings: AppSettings, val changeNotificationSettingAction: AsyncAction, - val currentPushDistributor: AsyncData, - val availablePushDistributors: ImmutableList, + val currentPushDistributor: AsyncData, + val availablePushDistributors: ImmutableList, val showChangePushProviderDialog: Boolean, val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState, val eventSink: (NotificationSettingsEvents) -> Unit, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt index 01765d2399e..0b46328b9bf 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.pushproviders.api.Distributor import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -22,13 +23,21 @@ open class NotificationSettingsStateProvider : PreviewParameterProvider = AsyncData.Success("Firebase"), - availablePushDistributors: List = listOf("Firebase", "ntfy"), + currentPushDistributor: AsyncData = AsyncData.Success(aDistributor("Firebase")), + availablePushDistributors: List = listOf( + aDistributor("Firebase"), + aDistributor("ntfy"), + ), showChangePushProviderDialog: Boolean = false, fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), eventSink: (NotificationSettingsEvents) -> Unit = {}, @@ -88,3 +100,11 @@ fun aInvalidNotificationSettingsState( fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), eventSink = eventSink, ) + +fun aDistributor( + name: String = "Name", + value: String = "$name Value", +) = Distributor( + value = value, + name = name, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt index d74ed8581c4..f73fecaf3c4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -31,13 +31,14 @@ import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceText 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.IconSource import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.toImmutableList @@ -50,6 +51,7 @@ fun NotificationSettingsView( state: NotificationSettingsState, onOpenEditDefault: (isOneToOne: Boolean) -> Unit, onTroubleshootNotificationsClick: () -> Unit, + onPushHistoryClick: () -> Unit, onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -82,6 +84,7 @@ fun NotificationSettingsView( // onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) }, onInviteForMeNotificationsChange = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) }, onTroubleshootNotificationsClick = onTroubleshootNotificationsClick, + onPushHistoryClick = onPushHistoryClick, ) } AsyncActionView( @@ -105,17 +108,24 @@ private fun NotificationSettingsContentView( // onCallsNotificationsChanged: (Boolean) -> Unit, onInviteForMeNotificationsChange: (Boolean) -> Unit, onTroubleshootNotificationsClick: () -> Unit, + onPushHistoryClick: () -> Unit, ) { val context = LocalContext.current val systemSettings: NotificationSettingsState.AppSettings = state.appSettings if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) { - PreferenceText( - icon = CompoundIcons.NotificationsOffSolid(), - title = stringResource(id = R.string.screen_notification_settings_system_notifications_turned_off), - subtitle = stringResource( - id = R.string.screen_notification_settings_system_notifications_action_required, - stringResource(id = R.string.screen_notification_settings_system_notifications_action_required_content_link) - ), + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.NotificationsOffSolid())), + headlineContent = { + Text(stringResource(id = R.string.screen_notification_settings_system_notifications_turned_off)) + }, + supportingContent = { + Text( + stringResource( + id = R.string.screen_notification_settings_system_notifications_action_required, + stringResource(id = R.string.screen_notification_settings_system_notifications_action_required_content_link) + ) + ) + }, onClick = { context.startNotificationSettingsIntent() } @@ -131,26 +141,37 @@ private fun NotificationSettingsContentView( if (systemSettings.appNotificationsEnabled) { if (!state.fullScreenIntentPermissionsState.permissionGranted) { PreferenceCategory { - PreferenceText( - icon = CompoundIcons.VoiceCall(), - title = stringResource(id = R.string.full_screen_intent_banner_title), - subtitle = stringResource(R.string.full_screen_intent_banner_message), + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VoiceCallSolid())), + headlineContent = { + Text(stringResource(id = R.string.full_screen_intent_banner_title)) + }, + supportingContent = { + Text(stringResource(R.string.full_screen_intent_banner_message)) + }, onClick = { - state.fullScreenIntentPermissionsState.openFullScreenIntentSettings() + state.fullScreenIntentPermissionsState.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) } ) } } PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_notification_section_title)) { - PreferenceText( - title = stringResource(id = R.string.screen_notification_settings_group_chats), - subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultGroupNotificationMode), + ListItem( + headlineContent = { + Text(stringResource(id = R.string.screen_notification_settings_group_chats)) + }, + supportingContent = { + Text(getTitleForRoomNotificationMode(mode = matrixSettings.defaultGroupNotificationMode)) + }, onClick = onGroupChatsClick ) - - PreferenceText( - title = stringResource(id = R.string.screen_notification_settings_direct_chats), - subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultOneToOneNotificationMode), + ListItem( + headlineContent = { + Text(stringResource(id = R.string.screen_notification_settings_direct_chats)) + }, + supportingContent = { + Text(getTitleForRoomNotificationMode(mode = matrixSettings.defaultOneToOneNotificationMode)) + }, onClick = onDirectChatsClick ) } @@ -180,11 +201,18 @@ private fun NotificationSettingsContentView( ) } PreferenceCategory(title = stringResource(id = R.string.troubleshoot_notifications_entry_point_section)) { - PreferenceText( - modifier = Modifier, - title = stringResource(id = R.string.troubleshoot_notifications_entry_point_title), + ListItem( + headlineContent = { + Text(stringResource(id = R.string.troubleshoot_notifications_entry_point_title)) + }, onClick = onTroubleshootNotificationsClick ) + ListItem( + headlineContent = { + Text(stringResource(R.string.troubleshoot_notifications_entry_point_push_history_title)) + }, + onClick = onPushHistoryClick + ) } if (state.showAdvancedSettings) { PreferenceCategory(title = stringResource(id = CommonStrings.common_advanced_settings)) { @@ -206,7 +234,7 @@ private fun NotificationSettingsContentView( stringResource(id = CommonStrings.common_error) ) is AsyncData.Success -> ListItemContent.Text( - state.currentPushDistributor.dataOrNull() ?: "" + state.currentPushDistributor.dataOrNull()?.name ?: "" ) }, onClick = { @@ -219,8 +247,14 @@ private fun NotificationSettingsContentView( if (state.showChangePushProviderDialog) { SingleSelectionDialog( title = stringResource(id = R.string.screen_advanced_settings_choose_distributor_dialog_title_android), - options = state.availablePushDistributors.map { - ListOption(title = it) + options = state.availablePushDistributors.map { distributor -> + // If there are several distributors with the same name, use the full name + val title = if (state.availablePushDistributors.count { it.name == distributor.name } > 1) { + distributor.fullName + } else { + distributor.name + } + ListOption(title = title) }.toImmutableList(), initialSelection = state.availablePushDistributors.indexOf(state.currentPushDistributor.dataOrNull()), onSelectOption = { index -> @@ -279,5 +313,6 @@ internal fun NotificationSettingsViewPreview(@PreviewParameter(NotificationSetti onBackClick = {}, onOpenEditDefault = {}, onTroubleshootNotificationsClick = {}, + onPushHistoryClick = {}, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt index 74533d2bc62..7a8fcea6284 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt @@ -21,7 +21,7 @@ open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvide anEditDefaultNotificationSettingsState(), anEditDefaultNotificationSettingsState(isOneToOne = true), anEditDefaultNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading), - anEditDefaultNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))), + anEditDefaultNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(RuntimeException("error"))), anEditDefaultNotificationSettingsState(displayMentionsOnlyDisclaimer = true), ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt index 6a94670c7d0..f38e59355c5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt @@ -16,7 +16,8 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.preferences.impl.R import io.element.android.libraries.designsystem.components.async.AsyncActionView -import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferencePage @@ -97,9 +98,11 @@ fun EditDefaultNotificationSettingView( Text(text = subtitle) }, leadingContent = ListItemContent.Custom { - CompositeAvatar( + Avatar( avatarData = summary.avatarData, - heroes = summary.heroesAvatar, + avatarType = AvatarType.Room( + heroes = summary.heroesAvatar, + ), ) }, onClick = { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index 57196621b92..c1c98be3539 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -8,9 +8,9 @@ package io.element.android.features.preferences.impl.root import android.app.Activity +import androidx.activity.compose.LocalActivity 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 @@ -21,7 +21,6 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutView -import io.element.android.features.logout.api.util.onSuccessLogout import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.user.MatrixUser @@ -113,7 +112,7 @@ class PreferencesRootNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = presenter.present() - val activity = LocalContext.current as Activity + val activity = requireNotNull(LocalActivity.current) val isDark = ElementTheme.isLightTheme.not() PreferencesRootView( state = state, @@ -140,11 +139,6 @@ class PreferencesRootNode @AssistedInject constructor( onDeactivateClick = this::onOpenAccountDeactivation ) - directLogoutView.Render( - state = state.directLogoutState, - onSuccessLogout = { - onSuccessLogout(activity, isDark, it) - } - ) + directLogoutView.Render(state = state.directLogoutState) } } 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 ae5267de0f8..6fd2d635e11 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 @@ -15,9 +15,11 @@ 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 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 import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState @@ -44,9 +46,11 @@ class PreferencesRootPresenter @Inject constructor( private val indicatorService: IndicatorService, private val directLogoutPresenter: Presenter, private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider, + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, ) : Presenter { @Composable override fun present(): PreferencesRootState { + val coroutineScope = rememberCoroutineScope() val matrixUser = matrixClient.userProfile.collectAsState() LaunchedEffect(Unit) { // Force a refresh of the profile @@ -79,6 +83,7 @@ class PreferencesRootPresenter @Inject constructor( var canDeactivateAccount by remember { mutableStateOf(false) } + val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } LaunchedEffect(Unit) { canDeactivateAccount = matrixClient.canDeactivateAccount() } @@ -100,7 +105,7 @@ class PreferencesRootPresenter @Inject constructor( fun handleEvent(event: PreferencesRootEvents) { when (event) { is PreferencesRootEvents.OnVersionInfoClick -> { - showDeveloperSettingsProvider.unlockDeveloperSettings() + showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope) } } } @@ -114,6 +119,7 @@ class PreferencesRootPresenter @Inject constructor( accountManagementUrl = accountManagementUrl.value, devicesManagementUrl = devicesManagementUrl.value, showAnalyticsSettings = hasAnalyticsProviders, + canReportBug = canReportBug, showDeveloperSettings = showDeveloperSettings, canDeactivateAccount = canDeactivateAccount, showNotificationSettings = showNotificationSettings.value, 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 9a16e6f65b8..2e5cb4fa148 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 @@ -20,6 +20,7 @@ data class PreferencesRootState( val showSecureBackupBadge: Boolean, val accountManagementUrl: String?, val devicesManagementUrl: String?, + val canReportBug: Boolean, val showAnalyticsSettings: Boolean, val showDeveloperSettings: Boolean, val canDeactivateAccount: Boolean, 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 e4ca5507779..43307e9988f 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 @@ -25,6 +25,7 @@ fun aPreferencesRootState( accountManagementUrl = "aUrl", devicesManagementUrl = "anOtherUrl", showAnalyticsSettings = true, + canReportBug = true, showDeveloperSettings = true, showNotificationSettings = true, showLockScreenSettings = true, 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 75d260d6a34..337544ed7f0 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 @@ -25,7 +25,6 @@ import io.element.android.features.preferences.impl.user.UserPreferences import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferencePage -import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight @@ -202,11 +201,13 @@ private fun ColumnScope.GeneralSection( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())), onClick = onOpenAbout, ) - ListItem( - headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())), - onClick = onOpenRageShake - ) + if (state.canReportBug) { + ListItem( + headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())), + onClick = onOpenRageShake + ) + } if (state.showAnalyticsSettings) { ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_analytics)) }, @@ -262,7 +263,7 @@ private fun ColumnScope.Footer( textAlign = TextAlign.Center, text = text, style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.materialColors.secondary, + color = ElementTheme.colors.textSecondary, ) } @@ -270,7 +271,7 @@ private fun ColumnScope.Footer( private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) { ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_developer_options)) }, - leadingContent = ListItemContent.Icon(IconSource.Resource(CompoundDrawables.ic_compound_code)), + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Code())), onClick = onOpenDeveloperSettings ) } 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 70037612d09..9b90c8ba535 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 @@ -5,21 +5,20 @@ * Please see LICENSE files in the repository root for full details. */ -@file:OptIn(ExperimentalCoilApi::class) - package io.element.android.features.preferences.impl.tasks import android.content.Context -import coil.Coil -import coil.annotation.ExperimentalCoilApi +import coil3.SingletonImageLoader import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.ftue.api.state.FtueService +import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.api.PushService +import io.element.android.services.appnavstate.api.ActiveRoomsHolder import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import javax.inject.Inject @@ -38,12 +37,16 @@ class DefaultClearCacheUseCase @Inject constructor( private val okHttpClient: Provider, private val ftueService: FtueService, private val pushService: PushService, + private val seenInvitesStore: SeenInvitesStore, + private val activeRoomsHolder: ActiveRoomsHolder, ) : ClearCacheUseCase { override suspend fun invoke() = withContext(coroutineDispatchers.io) { + // Active rooms should be disposed of before clearing the cache + activeRoomsHolder.clear(matrixClient.sessionId) // Clear Matrix cache matrixClient.clearCache() // Clear Coil cache - Coil.imageLoader(context).let { + SingletonImageLoader.get(context).let { it.diskCache?.clear() it.memoryCache?.clear() } @@ -53,8 +56,10 @@ class DefaultClearCacheUseCase @Inject constructor( context.cacheDir.deleteRecursively() // Clear some settings ftueService.reset() + seenInvitesStore.clear() // Ensure any error will be displayed again pushService.setIgnoreRegistrationError(matrixClient.sessionId, false) + pushService.resetBatteryOptimizationState() // Ensure the app is restarted defaultCacheService.onClearedCache(matrixClient.sessionId) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt index 9d578293608..fd3f96efac5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.user.MatrixUser @@ -58,7 +59,7 @@ class EditUserProfilePresenter @AssistedInject constructor( @Composable override fun present(): EditUserProfileState { val cameraPermissionState = cameraPermissionPresenter.present() - var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.let { Uri.parse(it) }) } + var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.toUri()) } var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( onResult = { uri -> @@ -168,7 +169,7 @@ class EditUserProfilePresenter @AssistedInject constructor( } private suspend fun updateAvatar(avatarUri: Uri?): Result { - return runCatching { + return runCatchingExceptions { if (avatarUri != null) { val preprocessed = mediaPreProcessor.process( uri = avatarUri, 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 298fd8cb17f..0e4c1b4f2d4 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 @@ -7,7 +7,9 @@ package io.element.android.features.preferences.impl.user.editprofile +import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.core.net.toUri import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.permissions.api.aPermissionsState @@ -17,14 +19,17 @@ open class EditUserProfileStateProvider : PreviewParameterProvider get() = sequenceOf( aEditUserProfileState(), + aEditUserProfileState(userAvatarUrl = "example://uri".toUri()), // Add other states here ) } -fun aEditUserProfileState() = EditUserProfileState( +fun aEditUserProfileState( + userAvatarUrl: Uri? = null, +) = EditUserProfileState( userId = UserId("@john.doe:matrix.org"), displayName = "John Doe", - userAvatarUrl = null, + userAvatarUrl = userAvatarUrl, avatarActions = persistentListOf(), saveAction = AsyncAction.Uninitialized, saveButtonEnabled = true, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt index 5b52f83b669..00581745ceb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -32,11 +32,11 @@ import io.element.android.features.preferences.impl.R import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.aliasScreenTitle 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 @@ -67,12 +67,7 @@ fun EditUserProfileView( modifier = modifier.clearFocusOnTap(focusManager), topBar = { TopAppBar( - title = { - Text( - text = stringResource(R.string.screen_edit_profile_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(R.string.screen_edit_profile_title), navigationIcon = { BackButton(onClick = onBackClick) }, actions = { TextButton( @@ -101,6 +96,7 @@ fun EditUserProfileView( displayName = state.displayName, avatarUrl = state.userAvatarUrl, avatarSize = AvatarSize.EditProfileDetails, + avatarType = AvatarType.User, onAvatarClick = { onAvatarClick() }, modifier = Modifier.align(Alignment.CenterHorizontally), ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/utils/ShowDeveloperSettingsProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/utils/ShowDeveloperSettingsProvider.kt index 6589fd1c666..2ed16d6582e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/utils/ShowDeveloperSettingsProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/utils/ShowDeveloperSettingsProvider.kt @@ -9,6 +9,8 @@ package io.element.android.features.preferences.impl.utils import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType +import io.element.android.libraries.ui.utils.MultipleTapToUnlock +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject @@ -19,18 +21,15 @@ class ShowDeveloperSettingsProvider @Inject constructor( companion object { const val DEVELOPER_SETTINGS_COUNTER = 7 } - private var counter = DEVELOPER_SETTINGS_COUNTER + + private val multipleTapToUnlock = MultipleTapToUnlock(DEVELOPER_SETTINGS_COUNTER) private val isDeveloperBuild = buildMeta.buildType != BuildType.RELEASE private val _showDeveloperSettings = MutableStateFlow(isDeveloperBuild) val showDeveloperSettings: StateFlow = _showDeveloperSettings - fun unlockDeveloperSettings() { - if (counter == 0) { - return - } - counter-- - if (counter == 0) { + fun unlockDeveloperSettings(scope: CoroutineScope) { + if (multipleTapToUnlock.unlock(scope)) { _showDeveloperSettings.value = true } } 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 9b017f6807f..289c59d360f 100644 --- a/features/preferences/impl/src/main/res/values-bg/translations.xml +++ b/features/preferences/impl/src/main/res/values-bg/translations.xml @@ -1,7 +1,14 @@ + "Изберете как да получавате известия" + "Скриване на профилните снимки в заявките за покана за стая" + "Оптимизиране на качеството на медията" + "Модерация и безопасност" "Потвърждения за прочитане" "Споделяне на присъствието" + "Скриване винаги" + "Показване винаги" + "В частни стаи" "Отблокиране" "Отблокиране на потребителя" "Име" @@ -20,6 +27,7 @@ "Включване на известията на това устройство" "Групови чатове" "Покани" + "Вашият сървър не поддържа тази опция в шифровани стаи, може да не получавате известия в някои стаи." "Споменавания" "Споменавания" "Да бъда известяван за" 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 6faec4a9883..db7f28a99d1 100644 --- a/features/preferences/impl/src/main/res/values-cs/translations.xml +++ b/features/preferences/impl/src/main/res/values-cs/translations.xml @@ -8,14 +8,22 @@ "Vlastní URL pro Element Call" "Nastavte vlastní URL pro Element Call." "Neplatné URL, ujistěte se, že jste uvedli protokol (http/https) a správnou adresu." + "Skrýt avatary v žádostech o pozvání do místnosti" + "Skrýt náhledy médií na časové ose" "Rychlejší nahrávání fotografií a videí a snížení spotřeby dat" "Optimalizace kvality médií" + "Moderování a bezpečnost" "Poskytovatel push oznámení" "Vypněte editor formátovaného textu pro ruční zadání Markdown." "Potvrzení o přečtení" "Pokud je vypnuto, potvrzení o přečtení se nikomu neodesílají. Stále budete dostávat potvrzení o přečtení od ostatních uživatelů." "Sdílejte přítomnost" "Pokud je tato funkce vypnutá, nebudete moci odesílat ani přijímat potvrzení o přečtení ani upozornění o psaní." + "Vždy skrýt" + "Vždy zobrazit" + "V soukromých místnostech" + "Skryté médium lze vždy zobrazit klepnutím na něj" + "Zobrazit média na časové ose" "Povolit možnost zobrazení zdroje zprávy na časové ose." "Nemáte žádné blokované uživatele" "Odblokovat" @@ -57,6 +65,7 @@ Pokud budete pokračovat, některá nastavení se mohou změnit." "systémová nastavení" "Systémová oznámení byla vypnuta" "Oznámení" + "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 new file mode 100644 index 00000000000..da0308b003d --- /dev/null +++ b/features/preferences/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,69 @@ + + + "Er mwyn sicrhau fyddwch chi ddim yn colli galwad bwysig, newidiwch eich gosodiadau i ganiatáu hysbysiadau sgrin lawn pan fydd eich ffôn wedi\'i gloi." + "Gwella profiad eich galwadau" + "Dewiswch sut i dderbyn hysbysiadau" + "Modd datblygwr" + "Galluogi i gael mynediad at nodweddion a swyddogaethau datblygwyr." + "URL sylfaen Galwad Element Cyfaddas" + "Gosod URL sylfaen cyfaddas ar gyfer Galwad Element." + "URL annilys, gwnewch yn siŵr eich bod yn cynnwys y protocol (http/https) a\'r cyfeiriad cywir." + "Cuddio afatarau yn y ceisiadau gwahoddiad i ystafell" + "Cuddio rhagolygon cyfryngau yn y llinell amser" + "Llwythwch i fyny lluniau a fideos yn gynt a lleihau\'r defnydd o ddata" + "Optimeiddio ansawdd y cyfryngau" + "Cymedroli a Diogelwch" + "Darparwr hysbysiad gwthio" + "Analluogi\'r golygydd testun cyfoethog i deipio Markdown â llaw." + "Derbynebau darllen" + "Os wedi\'i ddiffodd, fydd eich derbynebau darllen ddim yn cael eu hanfon at unrhyw un. Byddwch yn dal i dderbyn derbynebau darllen gan ddefnyddwyr eraill." + "Rhannu presenoldeb" + "Os wedi\'i ddiffodd, fyddwch chi ddim yn gallu anfon na derbyn derbynebau wedi\'u darllen na hysbysiadau teipio." + "Cuddio bob tro" + "Dangos bob tro" + "Mewn ystafelloedd preifat" + "Mae modd dangos cyfrwng cudd trwy dapio arno" + "Dangos cyfryngau mewn llinell amser" + "Galluogi\'r dewis i weld ffynhonnell y neges yn y llinell amser." + "Does gennych chi ddim defnyddwyr sydd wedi\'u rhwystro" + "Dad-rwystro" + "Byddwch yn gallu gweld pob neges oddi wrthyn nhw eto." + "Dadrwystro defnyddiwr" + "Wrthi\'n dadrwystro…" + "Enw dangos" + "Eich enw dangos" + "Cafwyd gwall anhysbys a doedd dim modd newid y manylion." + "Methu diweddaru\'r proffil" + "Golygu proffil" + "Yn diweddaru proffil…" + "Gosodiadau ychwanegol" + "Galwadau sain a fideo" + "Anghydweddiad y ffurfweddiad" + "Rydym wedi symleiddio\'r Gosodiadau Hysbysiadau i\'w gwneud yn haws dod o hyd i ddewisiadau. Nid yw rhai gosodiadau cyfaddas rydych chi wedi\'u dewis yn y gorffennol yn cael eu dangos yma, ond maen nhw\'n dal yn weithredol. + +Os ewch ymlaen, efallai y bydd rhai o\'ch gosodiadau\'n newid." + "Sgyrsiau uniongyrchol" + "Gosodiad cyfaddas fesul sgwrs" + "Digwyddodd gwall wrth ddiweddaru\'r gosodiad hysbysu." + "Pob neges" + "Crybwylliadau ac Allweddeiriau\'n unig" + "Ar sgyrsiau uniongyrchol, rhoi gwybod i mi am" + "Ar sgyrsiau grŵp, rhoi gwybod i mi am" + "Galluogi hysbysiadau ar y ddyfais hon" + "Nid yw\'r ffurfweddiad wedi\'i gywiro, ceisiwch eto." + "Sgyrsiau grŵp" + "Gwahoddiadau" + "Nid yw eich gweinydd cartref yn cefnogi\'r dewis hwn mewn ystafelloedd sydd wedi\'u hamgryptio, efallai fyddwch chi ddim yn cael gwybod mewn rhai ystafelloedd." + "Crybwyll" + "Y Cyfan" + "Crybwyll" + "Rhoi gwybod i mi am" + "Rhoi gwybod i mi ar @ystafell" + "I dderbyn hysbysiadau, newidiwch eich %1$s." + "gosodiadau system" + "Hysbysiadau system wedi\'u diffodd" + "Hysbysiadau" + "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 new file mode 100644 index 00000000000..c2edfc3d447 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,69 @@ + + + "For at sikre, at du aldrig går glip af et vigtigt opkald, skal du ændre dine indstillinger til at tillade underretninger i fuld skærm, når din telefon er låst." + "Gør din opkaldsoplevelse bedre" + "Vælg, hvordan du vil modtage notifikationer" + "Udviklertilstand" + "Aktivér for at få adgang til funktioner og funktionalitet for udviklere." + "Brugerdefineret URL til opkaldsbase for Element" + "Angiv en brugerdefineret basis-URL til Element Call." + "Ugyldig URL, sørg for at inkludere protokollen (http/https) og den korrekte adresse." + "Skjul avatarer i ruminvitationsanmodninger" + "Skjul forhåndsvisning af medier i tidslinjen" + "Upload fotos og videoer hurtigere, og reducér dataforbrug" + "Optimér mediekvaliteten" + "Moderation og sikkerhed" + "Udbyder af push-notifikationer" + "Deaktiver rich text-editoren for at skrive Markdown manuelt." + "Kvitteringer​•​for​•​læsning" + "Hvis deaktiveret, sendes dine læsekvitteringer ikke til nogen. Du vil stadig modtage læsekvitteringer fra andre brugere." + "Del tilstedeværelse" + "Hvis deaktiveret, kan du ikke sende eller modtage læsekvitteringer eller indtastningsmeddelelser." + "Skjul altid" + "Vis altid" + "I private rum" + "Et skjult medie kan altid vises ved at trykke på det" + "Vis medier i tidslinjen" + "Aktivér mulighed for at se meddelelseskilden i tidslinjen." + "Du har ingen blokerede brugere" + "Fjern blokering" + "Du vil være i stand til at se alle beskeder fra dem igen." + "Fjern blokering af bruger" + "Fjerner blokering…" + "Vist navn" + "Dit viste navn" + "Der opstod en ukendt fejl, og oplysningerne kunne ikke ændres." + "Kan ikke opdatere profilen" + "Redigér profil" + "Opdaterer profil…" + "Yderligere indstillinger" + "Lyd- og videoopkald" + "Uoverensstemmelse i konfigurationen" + "Vi har forenklet indstillingerne for notifikationer for at gøre det nemmere at finde dem. Nogle af de brugerdefinerede indstillinger, du tidligere har valgt, vises ikke her, men de er stadig aktive. + +Hvis du fortsætter, kan nogle af dine indstillinger blive ændret." + "Direkte samtaler" + "Brugerdefineret indstilling pr. samtale" + "Der opstod en fejl under opdatering af notifikationsindstillingen." + "Alle beskeder" + "Kun omtaler og nøgleord" + "I direkte samtaler, giv mig besked om" + "I gruppesamtaler, giv mig besked ved" + "Aktivér notifikationer på denne enhed" + "Konfigurationen er ikke blevet rettet, prøv igen." + "Gruppesamtaler" + "Invitationer" + "Din hjemmeserver understøtter ikke denne mulighed i krypterede rum, og derfor er det muligt at du ikke får besked i alle rum." + "Omtaler" + "Alle" + "Omtaler" + "Giv mig besked om" + "Giv mig besked på @room" + "For at modtage notifikationer, skal du ændre din %1$s ." + "systemindstillinger" + "Systemmeddelelser slået fra" + "Notifikationer" + "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 7d83685eda0..eb25ba482d0 100644 --- a/features/preferences/impl/src/main/res/values-de/translations.xml +++ b/features/preferences/impl/src/main/res/values-de/translations.xml @@ -1,23 +1,31 @@ - "Damit du keinen wichtigen Anruf verpasst, ändere bitte deine Einstellungen so, dass du bei gesperrtem Telefon Benachrichtigungen im Vollbildmodus erhältst." + "Damit Sie keine wichtigen Anrufe verpassen, ändern Sie bitte Ihre Einstellungen, so dass das gesperrte Telefon auch Benachrichtigungen im Vollbildmodus erhalten darf." "Verbessere dein Anruferlebnis" - "Wähle aus, wie du Benachrichtigungen erhalten möchtest" - "Entwickler-Modus" + "Wählen Sie, wie Sie Benachrichtigungen erhalten möchten" + "Entwicklermodus" "Aktivieren, um Zugriff auf Features und Funktionen für Entwickler zu aktivieren." - "Benutzerdefinierte Element-Aufruf-Basis-URL" + "Benutzerdefinierte Element Call Basis-URL" "Lege eine eigene Basis-URL für Element Call fest." - "Ungültige URL, bitte stelle sicher, dass du das Protokoll (http/https) und die richtige Adresse angibst." - "Laden Sie Fotos und Videos schneller hoch und reduzieren Sie die Datennutzung" + "Ungültige URL, bitte geben Sie das Protokoll (http/https) und die richtige Adresse an." + "Avatare in Chateinladungen ausblenden" + "Medienvorschau im Nachrichtenverlauf ausblenden" + "Laden Sie Fotos und Videos schneller hoch und reduzieren den Datenverbrauch" "Optimieren Sie die Medienqualität" + "Moderation und Sicherheit" "Anbieter für Push-Benachrichtigungen" "Deaktiviere den Rich-Text-Editor, um Markdown manuell einzugeben." "Lesebestätigungen" - "Wenn diese Option deaktiviert ist, werden Ihre Lesebestätigungen an niemanden gesendet. Du erhältst weiterhin Lesebestätigungen von anderen Benutzern." + "Wenn diese Option deaktiviert ist, werden Ihre Lesebestätigungen an niemanden gesendet. Sie erhalten weiterhin Lesebestätigungen von anderen Benutzern." "Präsenz teilen" - "Wenn diese Option deaktiviert ist, kannst du keine Lesebestätigungen oder Tipp-Benachrichtigungen senden oder empfangen." - "Option aktiveren, um Nachrichtenquelle in der Zeitleiste anzuzeigen." - "Du hast keine blockierten Nutzer" + "Wenn diese Option deaktiviert ist, können Sie keine Lesebestätigungen oder Tippbenachrichtigungen senden oder empfangen." + "Immer verstecken" + "Immer anzeigen" + "In privaten Chatrooms" + "Ausgeblendete Medien können jederzeit durch Antippen angezeigt werden" + "Medien im Nachrichtenverlauf anzeigen" + "Aktivieren Sie die Option, um den Nachrichtenquellcode in der Zeitleiste anzuzeigen." + "Sie haben keine geblockten Nutzer" "Blockierung aufheben" "Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt." "Blockierung aufheben" @@ -45,7 +53,7 @@ Wenn du fortfährst, können sich einige deiner Einstellungen ändern." "Die Konfiguration wurde nicht korrigiert, bitte versuche es erneut." "Gruppenchats" "Einladungen" - "Dein Homeserver unterstützt diese Option in verschlüsselten Chat nicht. In einigen Chats wirst du möglicherweise nicht benachrichtigt." + "Ihr Homeserver unterstützt diese Option in verschlüsselten Räumen nicht. In einigen Räumen werden Sie möglicherweise nicht benachrichtigt." "Erwähnungen" "Alle" "Erwähnungen" @@ -55,6 +63,7 @@ Wenn du fortfährst, können sich einige deiner Einstellungen ändern." "Systemeinstellungen" "Systembenachrichtigungen deaktiviert" "Benachrichtigungen" + "Verlauf pushen" "Fehlerbehebung" - "Fehlerbehebung für Benachrichtigungen" + "Beheben Sie die Fehler bei 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 7b388644a17..49d2f6df477 100644 --- a/features/preferences/impl/src/main/res/values-el/translations.xml +++ b/features/preferences/impl/src/main/res/values-el/translations.xml @@ -1,6 +1,6 @@ - "Για να διασφαλίσεις ότι δεν θα χάσεις ποτέ μια σημαντική κλήση, άλλαξε τις ρυθμίσεις σου για να επιτρέψεις τις ειδοποιήσεις πλήρους οθόνης όταν το τηλέφωνό σου είναι κλειδωμένο." + "Για να διασφαλίσετε ότι δεν θα χάσετε ποτέ μια σημαντική κλήση, αλλάξτε τις ρυθμίσεις σας ώστε να επιτρέπονται οι ειδοποιήσεις πλήρους οθόνης όταν το τηλέφωνό σας είναι κλειδωμένο." "Βελτίωσε την εμπειρία κλήσεων" "Επέλεξε τον τρόπο λήψης ειδοποιήσεων" "Λειτουργία προγραμματιστή" @@ -8,14 +8,22 @@ "Προσαρμοσμένο URL βάσης κλήσεων Element" "Όρισε μια προσαρμοσμένη διεύθυνση βάσης URL για κλήση Element." "Μη έγκυρη διεύθυνση URL, βεβαιώσου ότι έχεις συμπεριλάβει το πρωτόκολλο (http/https) και τη σωστή διεύθυνση." + "Απόκρυψη εικόνων προφίλ σε αιτήματα πρόσκλησης αίθουσας" + "Απόκρυψη προεπισκοπήσεων πολυμέσων στο timeline" "Ανέβασε φωτογραφίες και βίντεο γρηγορότερα και μείωσε τη χρήση δεδομένων" "Βελτιστοποίηση ποιότητας των μέσων" + "Συντονισμός και Ασφάλεια" "Πάροχος ειδοποιήσεων push" "Απενεργοποίησε τον επεξεργαστή εμπλουτισμένου κειμένου για να πληκτρολογήσεις Markdown χειροκίνητα." "Αποδεικτικά ανάγνωσης" "Εάν απενεργοποιηθεί, τα αποδεικτικά ανάγνωσης δεν θα στέλνονται σε κανέναν. Θα εξακολουθείς να λαμβάνεις αποδεικτικά ανάγνωσης από άλλους χρήστες." "Κοινή χρήση παρουσίας" "Εάν απενεργοποιηθεί, δεν θα μπορείς να στέλνεις ή να λαμβάνεις αποδεικτικά ανάγνωσης ή ειδοποιήσεις πληκτρολόγησης." + "Πάντα απόκρυψη" + "Πάντα εμφάνιση" + "Σε ιδιωτικές αίθουσες" + "Ένα κρυφό πολυμέσο μπορεί πάντα να εμφανιστεί πατώντας το" + "Εμφάνιση πολυμέσων στο timeline" "Ενεργοποίησε την επιλογή για προβολή πηγής μηνυμάτων στη ροή." "Δεν έχεις αποκλεισμένους χρήστες" "Άρση αποκλεισμού" @@ -45,16 +53,17 @@ "Η διαμόρφωση δεν έχει διορθωθεί, δοκίμασε ξανά." "Ομαδικές συνομιλίες" "Προσκλήσεις" - "Ο οικιακός διακομιστής σου δεν υποστηρίζει αυτήν την επιλογή σε κρυπτογραφημένα δωμάτια, ενδέχεται να μην λάβεις ειδοποίηση σε ορισμένα δωμάτια." + "Ο αρχικός διακομιστής σας δεν υποστηρίζει αυτή την επιλογή σε κρυπτογραφημένες αίθουσες, ενδέχεται να μην λαμβάνετε ειδοποιήσεις σε ορισμένες αίθουσες." "Αναφορές" "Όλα" "Αναφορές" "Ειδοποίησέ με για" "Ειδοποίηση για @room" - "Για να λαμβάνεις ειδοποιήσεις, άλλαξε το %1$s ." + "Για να λαμβάνεις ειδοποιήσεις, άλλαξε το %1$s." "ρυθμίσεις συστήματος" "Ειδοποιήσεις συστήματος ανενεργές" "Ειδοποιήσεις" + "Ιστορικό 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 db20929504e..afdc23d90b7 100644 --- a/features/preferences/impl/src/main/res/values-es/translations.xml +++ b/features/preferences/impl/src/main/res/values-es/translations.xml @@ -1,16 +1,29 @@ + "Para asegurarte de que nunca te pierdas una llamada importante, modifica tus ajustes para permitir notificaciones a pantalla completa cuando el teléfono esté bloqueado." + "Mejora tu experiencia de llamada" "Elige cómo recibir las notificaciones" "Modo desarrollador" "Habilita para tener acceso a características y funcionalidades para desarrolladores." "URL base personalizada de Element Call" "Define una URL base personalizada para Element Call." "URL no válida, asegúrate de incluir el protocolo (http/https) y la dirección correcta." + "Ocultar avatares en las invitaciones a salas" + "Ocultar vistas previas de multimedia en la cronología" + "Sube fotos y vídeos más rápido y reduce el uso de datos" + "Optimizar la calidad de los medios" + "Moderación y seguridad" + "Proveedor de notificaciones push" "Desactiva el editor de texto enriquecido para escribir Markdown manualmente." "Confirmaciones de lectura" "Si se desactiva, las confirmaciones de lectura no se enviarán a nadie. Seguirás recibiendo confirmaciones de lectura de otros usuarios." "Compartir presencia" - "Si se desactiva, no podrás enviar ni recibir confirmaciones de lectura ni notificaciones de escritura" + "Si se desactiva, no podrás enviar ni recibir confirmaciones de lectura ni notificaciones de escritura." + "Ocultar siempre" + "Mostrar siempre" + "En las salas privadas" + "Siempre se puede mostrar un ítem multimedia oculto pulsando sobre él" + "Mostrar multimedia en la cronología" "Habilita la opción para ver el contenido en bruto del mensaje en la cronología." "No tienes usuarios bloqueados" "Desbloquear" @@ -40,7 +53,7 @@ Si continúas, es posible que algunos de tus ajustes cambien." "La configuración no se ha corregido, por favor inténtalo de nuevo." "Chats grupales" "Invitaciones" - "Tu servidor principal no admite esta opción en salas cifradas, puede que no recibas notificaciones en algunas salas." + "Tu servidor base no admite esta opción en salas cifradas, puede que no recibas notificaciones de algunas salas." "Menciones" "Todos" "Menciones" @@ -50,4 +63,7 @@ Si continúas, es posible que algunos de tus ajustes cambien." "ajustes del sistema" "Notificaciones del sistema desactivadas" "Notificaciones" + "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 bf7c028dd3c..e9ee78f4299 100644 --- a/features/preferences/impl/src/main/res/values-et/translations.xml +++ b/features/preferences/impl/src/main/res/values-et/translations.xml @@ -8,14 +8,22 @@ "Element Calli kohandatud teenuseaadress" "Seadista kohandatud teenuseaadress Element Calli jaoks." "Vigane url. Palun vaata, et url algaks protokolliga (http/https) ning aadress ise oleks ka õige." + "Peida jututubade kutsetest tunnuspildid" + "Peida meedia eelvaated ajajoonel" "Sellega laadid fotosid ja videoid kiiremini üles ning vähendad andmemahtu" "Optimeeri meedia kvaliteeti" + "Modereerimine ja ohutus" "Tõuketeavituste pakkuja" "Kui soovid Markdown-vormingut käsitsi lisada, siis lülita vormindatud teksti toimeti välja." "Lugemisteatised" "Kui lülitad selle valiku välja, siis mitte keegi enam ei saa sinult lugemisteatisi. Küll aga saad sina teiste kasutajate lugemisteatisi." "Jaga oma olekut" "Kui see eelistus on välja lülitatud, siis sa ei saa ega saada ei lugemisteatisi ega kirjutamise teavitusi." + "Peida alati" + "Näita alati" + "Privaatsetes jututubades" + "Peidetud meediumi saad alati näha temal klõpsides" + "Näita ajajoonel meediat" "Selle eelistuse sisselülitamisel on võimalik ajajoonel vaadata sõnumite lähtekoodi." "Sa pole ühtegi kasutajat blokeerinud" "Eemalda blokeering" @@ -55,6 +63,7 @@ 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" + "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 new file mode 100644 index 00000000000..b624cd87f01 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,50 @@ + + + "Dei garrantzitsurik galduko ez duzula ziurtatzeko, aldatu ezarpenak telefonoa blokeatuta dagoenean pantaila osoko jakinarazpenak baimentzeko." + "Hobetu deien esperientzia" + "Aukeratu jakinarazpenak nola jaso" + "Garatzaile modua" + "Gaitu garatzaileentzako ezaugarrietarako eta funtzionalitateetarako sarbidea izateko." + "Igo argazkiak eta bideoak azkarrago eta murriztu datuen erabilera" + "Moderazioa eta Segurtasuna" + "Push jakinarazpen hornitzailea" + "Desgaitu testu aberatseko editorea Markdown eskuz idazteko." + "Irakurketa-agiriak" + "Desaktibatutaz gero, ez zaizkio inori bidaliko mezuak irakurri izanaren agiriak. Beste erabiltzaile batzuen irakurketa-agiriak jasoko dituzu oraindik ere." + "Partekatu presentzia" + "Gaitu aukera mezuaren iturria denbora-lerroan ikusteko." + "Ez duzu erabiltzailerik blokeatu" + "Desblokeatu" + "Beraien mezu guztiak berriro ikusteko aukera izango duzu." + "Desblokeatu erabiltzailea" + "Desblokeatzen…" + "Pantaila-izena" + "Zure pantaila-izena" + "Errore ezezagun bat aurkitu da eta ezin izan da informazioa aldatu." + "Ezin da profila eguneratu" + "Editatu profila" + "Profila eguneratzen…" + "Ezarpen gehiago" + "Audio eta bideo deiak" + "Konfigurazioa ez dator bat" + "Txat zuzenak" + "Errorea gertatu da jakinarazpen-ezarpena eguneratzean." + "Mezu guztiak" + "Aipamenak eta hitz gakoak soilik" + "Zuzeneko txatetan, jakinarazi" + "Taldeko txatetan, jakinarazi" + "Gaitu jakinarazpenak gailu honetan" + "Konfigurazioa ez da zuzendu; saiatu berriro." + "Taldeko txatak" + "Gonbidapenak" + "Zure zerbitzaria ez da bateragarria enkriptatutako gelen aukerarekin; litekeena da gela batzuetako jakinarazpenak ez jasotzea." + "Aipamenak" + "Guztia" + "Aipamenak" + "Jakinarazi" + "Jakinarazi @gelan" + "Jakinarazpenak jasotzeko, aldatu %1$s." + "sistemaren ezarpenak" + "Sistemaren jakinarazpenak desaktibatuta daude" + "Jakinarazpenak" + 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 f0cf54cea04..bf02b8a0f17 100644 --- a/features/preferences/impl/src/main/res/values-fa/translations.xml +++ b/features/preferences/impl/src/main/res/values-fa/translations.xml @@ -3,12 +3,21 @@ "بهبود تجریهٔ تماستان" "گزینش چگونگی دریافت آگاهی" "حالت توسعه‌دهنده" + "دسترسی به ویژگی ها و عملکردها را برای توسعه دهندگان فعال کنید." "نشانی پایهٔ تماس المنتی سفارشی" "تنظمی نشانی پایه‌‌ای سفارشی برای تماس المنتی." + "URL نامعتبر، لطفا مطمئن شوید که پروتکل (http/https) و آدرس صحیح را درج کرده اید." + "نهفتن رسانه در خط زمانی" + "بهینه سازی کیفیت رسانه" + "نظارت و امنیت" "فراهم کنندهٔ آگاهی‌های ارسالی" "از کار انداختن ویرایشگر متن غنی یا نوشتن دستی مارک‌دون." "رسید‌های خواندن" "هم‌رسانی حضور" + "نهفتن همیشگی" + "نمایش همیشگی" + "در اتاق‌های خصوصی" + "نمایش رسانه در خط زمانی" "هیچ کاربر مسدودی ندارید" "رفع انسداد" "قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید." 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 710447578f9..f938235e66c 100644 --- a/features/preferences/impl/src/main/res/values-fi/translations.xml +++ b/features/preferences/impl/src/main/res/values-fi/translations.xml @@ -8,14 +8,22 @@ "Mukautettu Element Call URL-osoite" "Aseta mukautettu URL-osoite Element Callille." "Virheellinen URL-osoite. Varmista, että sisällytät protokollan (http/https) ja oikean osoitteen." + "Piilota huoneiden avatarit kutsuista" + "Piilota median esikatselu aikajanalla" "Lähetä valokuvia ja videoita nopeammin ja vähennä datan käyttöä." "Optimoi median laatu" + "Moderointi ja Turvallisuus" "Push-ilmoitusten tarjoaja" "Ota rikastettu tekstieditori pois käytöstä, jotta voit kirjoittaa Markdownia manuaalisesti." "Lukukuittaukset" "Jos tämä on poissa päältä, sinun lukukuittauksia ei lähetetä kenellekään. Vastaanotat silti lukukuittauksia muilta käyttäjiltä." "Jaa läsnäolo" "Jos tämä on poissa päältä, et lähetä tai vastaanota lukukuittauksia tai kirjoitusilmotuksia." + "Piilota aina" + "Näytä aina" + "Yksityisissä huoneissa" + "Piilotetun median voi aina näyttää napauttamalla sitä" + "Näytä media aikajanalla" "Ota käyttöön mahdollisuus tarkastella viestin lähdettä aikajanalla." "Et ole estänyt ketään" "Poista esto" @@ -55,6 +63,7 @@ Jos jatkat, jotkin asetukset saattavat muuttua." "järjestelmäsi asetuksia" "Järjestelmän ilmoitukset on poissa päältä" "Ilmoitukset" + "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 d48b102b9b5..1180454d036 100644 --- a/features/preferences/impl/src/main/res/values-fr/translations.xml +++ b/features/preferences/impl/src/main/res/values-fr/translations.xml @@ -8,14 +8,22 @@ "URL de base pour Element Call personnalisée" "Configurer une URL de base pour Element Call." "URL invalide, assurez-vous d’inclure le protocol (http/https) et l’adresse correcte." + "Masquer les avatars des salons dans les invitations" + "Masquer les aperçus des médias dans les discussions" "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é" "Fournisseur de Push" "Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown." "Accusés de lecture" "En cas de désactivation, vos accusés de lecture ne seront pas envoyés aux autres membres. Vous verrez toujours les accusés des autres membres." "Partager la présence" "Si cette option est désactivée, vous ne pourrez ni envoyer ni recevoir de confirmations de lecture ni de notifications de saisie." + "Toujours cacher" + "Toujours montrer" + "Dans les salons privés" + "Un média caché peut toujours être affiché en cliquant dessus" + "Afficher les médias dans les discussions." "Activer cette option pour pouvoir voir la source des messages dans la discussion." "Vous n’avez bloqué personne" "Débloquer" @@ -55,6 +63,7 @@ Si vous continuez, il est possible que certains de vos paramètres soient modifi "paramètres du système" "Les notifications du système sont désactivées" "Notifications" + "Historique des Push" "Dépannage" "Dépanner les notifications" 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 cb01dd66f3f..da63c18fb7a 100644 --- a/features/preferences/impl/src/main/res/values-hu/translations.xml +++ b/features/preferences/impl/src/main/res/values-hu/translations.xml @@ -8,14 +8,22 @@ "Egyéni Element Call alapwebcím" "Egyéni alapwebcím beállítása az Element Callhoz." "Érvénytelen webcím, győződjön meg arról, hogy szerepel-e benne a protokoll (http/https), és hogy helyes-e a cím." + "Profilképek elrejtése a szobameghívókban" + "Médiaelőnézetek elrejtése az idővonalon" "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" "Leküldéses értesítések szolgáltatója" "A formázott szöveges szerkesztő letiltása, hogy kézzel írhasson Markdownt." "Olvasási visszaigazolások" "Ha ki van kapcsolva, az olvasási visszaigazolások nem lesznek elküldve senkinek. A többi felhasználó olvasási visszaigazolását továbbra is meg fogja kapni." "Jelenlét megosztása" "Ha ki van kapcsolva, nem tud olvasási visszaigazolást vagy írási értesítést küldeni és fogadni" + "Elrejtés mindig" + "Megjelenítés mindig" + "Privát szobákban" + "A rejtett médiatartalmak koppintással jeleníthetők meg" + "Média megjelenítése az idővonalon" "Engedélyezze a beállítást az üzenet forrásának megjelenítéséhez az idővonalon." "Nincsenek letiltott felhasználók" "Letiltás feloldása" @@ -55,6 +63,7 @@ Ha folytatja, egyes beállítások megváltozhatnak." "rendszerbeállításokat" "A rendszerértesítések ki vannak kapcsolva" "Értesítések" + "Leküldéses értesítés 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 7b9bc51a82b..ffc00d38ed8 100644 --- a/features/preferences/impl/src/main/res/values-in/translations.xml +++ b/features/preferences/impl/src/main/res/values-in/translations.xml @@ -8,14 +8,22 @@ "URL dasar Element Call khusus" "Tetapkan URL dasar khusus untuk Element Call." "URL tidak valid, pastikan Anda menyertakan protokol (http/https) dan alamat yang benar." + "Sembunyikan avatar dalam permintaan undangan ruangan" + "Sembunyikan pratinjau media pada lini masa" "Unggah foto dan video lebih cepat dan kurangi penggunaan data" "Optimalkan kualitas media" + "Moderasi dan Keamanan" "Penyedia notifikasi dorongan" "Nonaktifkan penyunting teks kaya untuk mengetik Markdown secara manual." "Laporan dibaca" "Jika dimatikan, laporan dibaca Anda tidak akan dikirim kepada siapa pun. Anda masih akan menerima laporan dibaca dari pengguna lain." "Bagikan presensi" "Jika dimatikan, Anda tidak akan dapat mengirim atau menerima laporan dibaca atau notifikasi pengetikan" + "Selalu sembunyikan" + "Selalu tampilkan" + "Dalam ruangan privat" + "Media tersembunyi selalu dapat ditampilkan dengan mengetuknya" + "Tampilkan media pada lini masa" "Aktifkan opsi untuk melihat sumber pesan dalam lini masa." "Anda tidak memiliki pengguna yang diblokir" "Buka blokir" @@ -57,6 +65,7 @@ Jika Anda melanjutkan, beberapa pengaturan Anda dapat berubah." "pengaturan sistem" "Pemberitahuan sistem dimatikan" "Notifikasi" + "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 e47167e32b6..677a4f01c3f 100644 --- a/features/preferences/impl/src/main/res/values-it/translations.xml +++ b/features/preferences/impl/src/main/res/values-it/translations.xml @@ -8,14 +8,22 @@ "URL base di Element Call personalizzato" "Imposta un URL di base personalizzato per Element Call." "URL non valido, assicurati di includere il protocollo (http/https) e l\'indirizzo corretto." + "Nascondi gli avatar nelle richieste di invito alle stanze" + "Nascondi le anteprime dei media nelle conversazioni" "Carica foto e video più velocemente e riduci l\'utilizzo dei dati" "Ottimizza la qualità dei contenuti multimediali" + "Moderazione e Sicurezza" "Fornitore di notifiche push" "Disattiva l\'editor di testo avanzato per scrivere manualmente in Markdown" "Ricevute di visualizzazione" "Se disattivato, le tue ricevute di visualizzazione non verranno inviate a nessuno. Riceverai comunque ricevute di visualizzazione da altri utenti." "Condividi presenza online" "Se disattivato, non potrai inviare o ricevere ricevute di lettura o notifiche di scrittura." + "Nascondi sempre" + "Mostra sempre" + "Nelle stanze private" + "Un file multimediale nascosto può sempre essere visualizzato toccandolo" + "Mostra i media nella conversazione" "Attiva l\'opzione per visualizzare il codice sorgente del messaggio nella conversazione." "Non hai utenti bloccati" "Sblocca" @@ -55,6 +63,7 @@ Se procedi, alcune delle tue impostazioni potrebbero cambiare." "impostazioni di sistema" "Notifiche di sistema disattivate" "Notifiche" + "Cronologia push" "Risoluzione dei problemi" "Risoluzione di problemi delle notifiche" 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 89c39c1c446..1efe1972673 100644 --- a/features/preferences/impl/src/main/res/values-ka/translations.xml +++ b/features/preferences/impl/src/main/res/values-ka/translations.xml @@ -7,10 +7,16 @@ "დააყენეთ საბაზისო URL Element-ის ზარებისათვის." "არასწორი URL, გთხოვთ, დარწმუნდეთ, რომ შეიტანეთ პროტოკოლი (http/https) და სწორი მისამართი." "გამორთეთ მდიდარი ტექსტის რედაქტორი, რათა ხელით აკრიფოთ Markdown." + "წაკითხვის შეტყობინებები" + "გამორთვის შემთხვევაში სხვები ვერ მიიღებენ თქვენი წაკითხვის შეტყობინებებს. თქვენ მაინც მიიღებთ სხვების წაკითხვის შეტყობინებებს." + "მყოფობის გაზიარება" + "გამორთვის შემთხვევაში თქვენ ვერ მიიღებთ და ვერ გაგზავნით წაკითხვის ან წერის შეტყობინებებს." "ჩართეთ ოპცია რათა შეტყობინების წყაროს დროის ისტორია ნახოთ." + "თქვენ არ დაგიბლოკავთ მომხმარებლები" "განბლოკვა" "თქვენ კვლავ შეძლებთ მათგან ყველა შეტყობინების ნახვას." "Მომხმარებლის განბლოკვა" + "განბლოკვა…" "ნაჩვენები სახელი" "თქვენი ნაჩვენები სახელი" "დაფიქსირდა უცნობი შეცდომა და ინფორმაციის შეცვლა ვერ მოხერხდა." @@ -44,4 +50,6 @@ "სისტემის პარამეტრები" "სისტემის შეტყობინებები გამორთულია" "შეტყობინებები" + "პრობლემების გადაჭრა" + "პრობლემების გადაჭრის შეტყობინებები" diff --git a/features/preferences/impl/src/main/res/values-lt/translations.xml b/features/preferences/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..b0a2f7bd814 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,7 @@ + + + "Pasirinkite, kaip gauti pranešimus" + "Atblokuoti" + "Vėl galėsite matyti visas iš jų gautas žinutes." + "Atblokuoti vartotoją" + 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 c24e6df23b5..84283b898b2 100644 --- a/features/preferences/impl/src/main/res/values-nb/translations.xml +++ b/features/preferences/impl/src/main/res/values-nb/translations.xml @@ -1,13 +1,69 @@ + "For å sikre at du aldri går glipp av en viktig samtale, må du endre innstillingene dine for å tillate fullskjermvarsler når telefonen er låst." + "Forbedre samtaleopplevelsen din" "Velg hvordan du vil motta varsler" "Utviklermodus" + "Aktiver for å få tilgang til funksjoner og funksjonalitet for utviklere." + "Egendefinert Element Call base URL" + "Angi en egendefinert base URL for Element Call." + "Ugyldig URL. Pass på at du inkluderer protokollen (http/https) og riktig adresse." + "Skjul avatarer i invitasjonsforespørsler til rom" + "Skjul forhåndsvisninger av medier på tidslinjen" + "Last opp bilder og videoer raskere og reduser databruken" + "Optimaliser mediekvaliteten" + "Moderasjon og sikkerhet" + "Leverandør av pushvarsling" + "Deaktiver rik tekstredigering for å skrive Markdown manuelt." + "Lesebekreftelser" + "Hvis slått av, sendes ikke lesebekreftelsene dine til noen. Du vil fortsatt motta lesebekreftelser fra andre brukere." + "Del tilstedeværelse" + "Hvis slått av, kan du ikke sende eller motta lesebekreftelser eller skrivevarsler." + "Skjul alltid" + "Vis alltid" + "I private rom" + "Et skjult medium kan alltid vises ved å trykke på det" + "Vis medier i tidslinjen" + "Aktiver alternativet for å vise meldingskilden på tidslinjen." + "Du har ingen blokkerte brukere" "Fjern blokkering" "Du vil kunne se alle meldingene fra dem igjen." "Fjern blokkering av bruker" + "Fjerner blokkering …" "Visningsnavn" "Ditt visningsnavn" + "Det oppstod en ukjent feil, og informasjonen kunne ikke endres." "Kan ikke oppdatere profilen" "Rediger profil" "Oppdaterer profilen…" + "Ytterligere innstillinger" + "Lyd- og videosamtaler" + "Uoverensstemmelse i konfigurasjonen" + "Vi har forenklet varslingsinnstillingene for å gjøre det lettere å finne alternativene. Noen av de egendefinerte innstillingene du har valgt tidligere, vises ikke her, men de er fortsatt aktive. + +Hvis du fortsetter, kan noen av innstillingene dine endres." + "Direkte chatter" + "Egendefinert innstilling per chat" + "Det oppstod en feil under oppdatering av varslingsinnstillingen." + "Alle meldinger" + "Bare omtaler og nøkkelord" + "På direkte chatter, varsle meg for" + "I gruppechatter, varsle meg om" + "Aktiver varsler på denne enheten" + "Konfigurasjonen er ikke korrigert, prøv igjen." + "Gruppechatter" + "Invitasjoner" + "Hjemmeserveren din støtter ikke dette alternativet i krypterte rom, og det kan hende at du ikke blir varslet i enkelte rom." + "Omtaler" + "Alle" + "Omtaler" + "Varsle meg om" + "Gi meg varsel på @room" + "For å motta varsler, vennligst endre %1$s." + "systeminnstillinger" + "Systemvarsler er slått av" + "Varslinger" + "Push-historikk" + "Feilsøk" + "Feilsøk varsler" 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 5bb41476257..8ae089b1704 100644 --- a/features/preferences/impl/src/main/res/values-pl/translations.xml +++ b/features/preferences/impl/src/main/res/values-pl/translations.xml @@ -8,14 +8,22 @@ "Własny bazowy URL dla połączeń Element" "Ustaw własny bazowy URL dla połączeń Element" "Nieprawidłowy adres URL, upewnij się, że zawiera protokół (http/https) i poprawny adres." - "Przesyłaj zdjęcia i filmy szybciej i zmniejsz zużycie danych" + "Ukryj awatary w prośbach o dołączenie do pokoju" + "Ukryj podglądy multimediów na osi czasu" + "Przesyłaj zdjęcia i filmy szybciej, zmniejszając zużycie danych" "Optymalizuj jakość multimediów" + "Moderacja i bezpieczeństwo" "Dostawca powiadomień push" "Wyłącz edytor tekstu bogatego, aby pisać tekst Markdown ręcznie." "Potwierdzenia odczytania" "Gdy wyłączona, Twoje potwierdzenia odczytania nie zostaną wysłane. Potwierdzenia od innych wciąż będą odbierane." "Udostępnij obecność" "Gdy wyłączona, nie będziesz mógł wysyłać lub odbierać potwierdzeń odczytu ani powiadomień pisania." + "Zawsze ukrywaj" + "Zawsze pokazuj" + "W pokojach prywatnych" + "Ukryte media można zawsze wyświetlić, dotykając ich" + "Pokaż media na osi czasu" "Włącz opcję, aby wyświetlić źródło wiadomości na osi czasu." "Nie blokujesz żadnych użytkowników" "Odblokuj" @@ -51,10 +59,11 @@ Niektóre ustawienia mogą ulec zmianie, jeśli kontynuujesz." "Wzmianki" "Powiadamiaj mnie przez" "Powiadom mnie na @pokój" - "Aby otrzymywać powiadomienia, zmień swoje%1$s ." + "Aby otrzymywać powiadomienia, zmień swoje%1$s." "ustawienia systemowe" "Powiadomienia systemowe wyłączone" "Powiadomienia" + "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 42ac543aee1..a6a1396dece 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 @@ -1,16 +1,29 @@ + "Para garantir que você nunca perca uma chamada importante, por favor altere as suas configurações para permitir notificações em tela cheia enquanto o seu celular estiver bloqueado." + "Melhore a sua experiência de chamadas" "Escolha como receber notificações" "Modo de desenvolvedor" "Habilite para ter acesso a recursos e funcionalidades para desenvolvedores." "URL base do Element Call personalizado" "Defina um URL base personalizado para Element Call." "URL inválida, por favor verifique se o protocolo (http/https) e o endereço correto estão presentes." + "Oculte avatares em solicitações de convite para salas" + "Ocultar visualizações de mídia na linha do tempo" + "Faça upload de fotos e vídeos com mais rapidez e reduza o uso de dados" + "Otimize a qualidade da mídia" + "Moderação e segurança" + "Provedor de notificações por push" "Desative o editor de rich text para digitar Markdown manualmente." "Confirmações de leitura" "Se desligado, suas confirmações de leitura não serão enviadas para ninguém. Você ainda receberá confirmações de leitura de outros usuários." "Compartilhar presença" "Se desligado, você não poderá enviar ou receber confirmações de leitura ou notificações de digitação." + "Ocultar sempre" + "Mostrar sempre" + "Em salas privadas" + "Uma mídia oculta sempre pode ser exibida se você tocar nela" + "Mostrar mídia na linha do tempo" "Ativar a opção de visualizar o fonte da mensagem na linha do tempo." "Você não tem usuários bloqueados" "Desbloquear" @@ -40,6 +53,7 @@ Se você continuar, algumas de suas configurações poderão mudar." "A configuração não foi corrigida, tente novamente." "Bate-papos em grupo" "Convites" + "Seu servidor doméstico não suporta esta opção em salas criptografadas. Você pode não ser notificado em algumas salas." "Menções" "Todos" "Menções" @@ -49,4 +63,7 @@ Se você continuar, algumas de suas configurações poderão mudar." "configurações do sistema" "Notificações do sistema desativadas" "Notificações" + "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 766e27f4698..af380cf9ac5 100644 --- a/features/preferences/impl/src/main/res/values-pt/translations.xml +++ b/features/preferences/impl/src/main/res/values-pt/translations.xml @@ -8,14 +8,22 @@ "URL base para Element Call personalizado" "Define um URL base para a Element Call." "URL inválido, certifica-te de que incluis o protocolo (http/https) e o endereço correto." + "Ocultar avatares nos pedidos de acesso a salas" + "Ocultar pré-visualizações de multimédia na cronologia" "Carrega fotos e vídeos mais rapidamente e reduz a utilização de dados" "Otimiza a qualidade da mídia" + "Moderação e Segurança" "Fornecedor de envio" "Desativa o editor de texto rico para poderes escrever Markdown manualmente." "Recibos de leitura" "Se desativada, os teus recibos de leitura não serão enviados a ninguém. Continuas a receber recibos de leitura de outros utilizadores." "Partilhar presença" "Se desativado, não poderás enviar ou receber recibos de leitura ou notificações de escrita." + "Ocultar sempre" + "Mostrar sempre" + "Em salas privadas" + "Multimédia oculta pode sempre ser exibida tocando-lhe" + "Mostrar multimédia na cronologia" "Ativa a opção para ver a origem da mensagem na cronologia." "Não tens nenhum utilizador bloqueado" "Desbloquear" @@ -55,6 +63,7 @@ Se prosseguires, algumas delas podem ser alteradas." "configurações do sistema" "Notificações do sistema desativadas" "Notificações" + "Histórico de notificações" "Resolução de problemas" "Corrigir notificações" 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 c015011c89c..70b15002a60 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -8,14 +8,22 @@ "Базовый URL сервера звонков Element" "Задайте свой сервер Element Call." "Адрес указан неверно, удостоверьтесь, что вы указали протокол (http/https) и правильный адрес." + "Скрыть аватары в запросах на приглашение в комнату" + "Скрыть предварительный просмотр медиафайлов на временной шкале" "Загружайте фотографии и видео быстрее и сокращайте потребление трафика" "Оптимизировать качество мультимедиа" + "Модерация и безопасность" "Поставщик push-уведомлений" "Отключить редактор форматированного текста и включить Markdown." "Уведомления о прочтении" "Если этот параметр выключен, ваш статус о прочтении не будет отображаться. Вы по-прежнему будете видеть статус о прочтении от других пользователей." "Поделиться присутствием" "Если выключено, вы не сможете отправлять, получать уведомления о прочтении и наборе текста" + "Всегда скрывать" + "Всегда показывать" + "В личных комнатах" + "Скрытый медиафайл всегда можно отобразить, нажав на него." + "Показать медиафайлы в хронологии" "Включить опцию просмотра источника сообщения в ленте." "У вас нет заблокированных пользователей" "Разблокировать" @@ -55,6 +63,7 @@ "настройки системы" "Системные уведомления выключены" "Уведомления" + "История уведомлений" "Устранение неполадок" "Уведомления об устранении неполадок" 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 118cd7d6a5f..33dddb66e79 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -8,14 +8,22 @@ "Vlastná Element Call základná URL adresa" "Nastaviť vlastnú základnú URL adresu pre Element Call." "Neplatná adresa URL, uistite sa, že ste uviedli protokol (http/https) a správnu adresu." + "Skrytie profilové obrázky v žiadostiach o pozvánku do miestnosti" + "Skryť ukážky médií na časovej osi" "Nahrávajte fotografie a videá rýchlejšie a znížte spotrebu dát" "Optimalizovať kvalitu médií" + "Moderovanie a bezpečnosť" "Poskytovateľ oznámení Push" "Vypnite rozšírený textový editor na ručné písanie Markdown." "Potvrdenia o prečítaní" "Ak je táto funkcia vypnutá, vaše potvrdenia o prečítaní sa nebudú nikomu odosielať. Stále budete dostávať potvrdenia o prečítaní od ostatných používateľov." "Zdieľať prítomnosť" "Ak je vypnuté, nebudete môcť odosielať ani prijímať potvrdenia o prečítaní alebo upozornenia o písaní" + "Vždy skryť" + "Vždy zobraziť" + "V súkromných miestnostiach" + "Skryté médium sa dá vždy zobraziť ťuknutím naň" + "Zobraziť médiá na časovej osi" "Povoliť možnosť zobrazenia zdroja správy na časovej osi." "Nemáte žiadnych blokovaných používateľov" "Odblokovať" @@ -57,6 +65,7 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""nastavenia systému" "Systémové oznámenia sú vypnuté" "Oznámenia" + "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 aa765bc0806..831280255c7 100644 --- a/features/preferences/impl/src/main/res/values-sv/translations.xml +++ b/features/preferences/impl/src/main/res/values-sv/translations.xml @@ -8,12 +8,22 @@ "Anpassad bas-URL för Element Call" "Ange en anpassad bas-URL för Element Call." "Ogiltig URL, se till att du inkluderar protokollet (http/https) och rätt adress." + "Dölj avatarer i förfrågningar om rumsinbjudningar" + "Dölj förhandsgranskningar av media i tidslinjen" + "Ladda upp foton och videor snabbare och minska dataanvändningen" + "Optimera mediekvaliteten" + "Moderering och säkerhet" "Pushnotisleverantör" "Inaktivera rik-text-redigeraren för att skriva Markdown manuellt." "Läskvitton" "Om det är avstängt kommer dina läskvitton inte att skickas till någon. Du kommer fortfarande att få läskvitton från andra användare." "Dela närvaro" "Om det är avstängt kan du inte skicka eller ta emot läskvitton eller skrivnotiser" + "Göm alltid" + "Visa alltid" + "I privata rum" + "En dold media kan alltid visas genom att trycka på den" + "Visa media i tidslinjen" "Aktivera alternativet för att visa meddelandekälla i tidslinjen." "Du har inga blockerade användare" "Avblockera" @@ -53,6 +63,7 @@ Om du fortsätter kan vissa av dina inställningar ändras." "systeminställningar" "Systemaviseringar avstängda" "Aviseringar" + "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 new file mode 100644 index 00000000000..a13a006e22c --- /dev/null +++ b/features/preferences/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,60 @@ + + + "Önemli bir aramayı asla kaçırmamak için, telefonunuz kilitliyken tam ekran bildirimlere izin vermek üzere ayarlarınızı değiştirin." + "Arama deneyiminizi geliştirin" + "Bildirimleri nasıl alacağınızı seçin" + "Geliştirici modu" + "Geliştiriciler için özelliklere ve işlevlere erişim sağlayın." + "Özel Element Call temel URL\'si" + "Element Call için özel bir temel URL ayarlayın." + "Geçersiz URL, lütfen protokolü (http/https) ve doğru adresi eklediğinizden emin olun." + "Fotoğraf ve videoları daha hızlı yükleyin ve veri kullanımını azaltın" + "Medya kalitesini optimize edin" + "Anlık bildirim sağlayıcısı" + "Markdown\'ı manuel olarak yazmak için zengin metin düzenleyicisini devre dışı bırakın." + "Okundu bilgisi" + "Kapatılırsa, okundu bilgileriniz kimseye gönderilmez. Diğer kullanıcılardan okundu bilgisi almaya devam edersiniz." + "Varlığı paylaşın" + "Kapatılırsa, okundu bilgisi veya yazma bildirimleri gönderemez veya alamazsınız." + "Zaman çizelgesinde mesaj kaynağını görüntüleme seçeneğini etkinleştirin." + "Engellenen kullanıcı yok." + "Engellemeyi kaldır" + "Onlardan gelen tüm mesajları tekrar görebileceksiniz." + "Kullanıcının engelini kaldır" + "Engel kaldırılıyor…" + "Görünen ad" + "Görünen adınız" + "Bilinmeyen bir hatayla karşılaşıldı ve bilgiler değiştirilemedi." + "Profil güncellenemiyor" + "Profili düzenle" + "Profil güncelleniyor…" + "Ek ayarlar" + "Sesli ve Görüntülü aramalar" + "Yapılandırma uyuşmazlığı" + "Seçeneklerin bulunmasını kolaylaştırmak için Bildirim Ayarlarını basitleştirdik. Geçmişte seçtiğiniz bazı özel ayarlar burada gösterilmez, ancak hala aktiftir. + +Devam ederseniz, bazı ayarlarınız değişebilir." + "Doğrudan sohbetler" + "Sohbet başına özel ayar" + "Bildirim ayarı güncellenirken bir hata oluştu." + "Tüm mesajlar" + "Yalnızca Bahsetmeler ve Anahtar Kelimeler" + "Doğrudan sohbetlerde, beni bilgilendir" + "Grup sohbetlerinde, beni bilgilendir" + "Bu cihazda bildirimleri etkinleştir" + "Yapılandırma düzeltilmedi, lütfen tekrar deneyin." + "Grup sohbetleri" + "Davetler" + "Ana sunucunuz şifreli odalarda bu seçeneği desteklemiyor, bazı odalarda bildirim almayabilirsiniz." + "Bahsetmeler" + "Tümü" + "Bahsetmeler" + "Bana bildir" + "Bana @room\'da bildir" + "Bildirimleri almak için lütfen %1$s değiştirin." + "si̇stem ayarları" + "Sistem bildirimleri kapalı" + "Bildirimler" + "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 afbbe97e391..22ca6bbc22b 100644 --- a/features/preferences/impl/src/main/res/values-uk/translations.xml +++ b/features/preferences/impl/src/main/res/values-uk/translations.xml @@ -8,14 +8,22 @@ "Користувацька URL-адреса Element Call" "Встановіть URL-адресу для Element Call." "Неправильна URL-адреса. Переконайтеся, що ви вказали протокол (http/https) та правильну адресу." + "Сховати аватари у запитах на запрошення до кімнат" + "Сховати попередній перегляд медіа у стрічці" "Швидше завантажуйте фотографії та відео та зменшуйте використання даних" "Оптимізуйте медіаякість" + "Модерування й безпека" "Постачальник push-сповіщень" "Вимкніть редактор розширеного тексту, щоб вводити Markdown вручну." "Читати журнали" "Якщо вимкнено, ваші сповіщення про прочитання нікому не надсилатимуться. Ви все одно отримуватимете сповіщення про прочитання від інших користувачів." "Поділіться присутністю" "Якщо цей параметр вимкнено, ви не зможете надсилати й отримувати звіти про прочитання чи сповіщення про введення тексту." + "Завжди ховати" + "Завжди показувати" + "У приватних кімнатах" + "Сховані медіа завжди можна переглянути, натиснувши на нього" + "Показувати медіа у стрічці" "Увімкнути опцію для перегляду коду повідомлення в стрічці" "У вас немає заблокованих користувачів." "Розблокувати" @@ -55,6 +63,7 @@ "системні налаштування" "Системні сповіщення вимкнені" "Сповіщення" + "Історія 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 new file mode 100644 index 00000000000..470763e8778 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,58 @@ + + + "اس بات کو یقینی بنانے کے لیے کہ آپ کبھی بھی اہم مکالمہ سے محروم نہ ہوں، براہ کرم اپنی ترتیبات تبدیل کریں تاکہ آپ کا ہاتف مقفل ہونے پر مکمل پردۂ نمائش اطلاعات کی اجازت دی جا سکے۔" + "اپنے مکالمتی تجربے کو احسن کریں" + "اطلاعات موصول کرنے کا طریقہ چنیں۔" + "مطور وضع" + "مطورین کیلئے خصوصیات اور فعالیت تک رسائی حاصل کرنے کے لیے فعال کریں۔" + "حسب ضرورت ایلیمنٹ مکالمہ بنیادی عنوان" + "ایلیمنٹ مکالمہ کیلئے حسب ضرورت بنیادی عنوان ترتیب دیں" + "باطل عنوان، برائے مہربانی یقینی بنائیں کہ آپ دستور (http/https) اور صحیح پتہ شامل کریں۔" + "دھکا اطلاع فراہم کنندہ" + "مارک ڈاون کو دستی طور پر تحریر کرنے کے لیے امیر متن مدون کو غیر فعال کریں۔" + "پڑھنے کی رسیدیں" + "اگر بند کر دیا جائے تو، آپ کی پڑھنے کی رسیدیں کسی کو نہیں بھیجی جائیں گی۔ آپ اب بھی دوسرے صارفین سے پڑھنے کی رسیدیں وصول کریں گے۔" + "موجودگی کا اشتراک کریں" + "اگربند کیا، تو آپ پڑھنے کی رسیدیں یا تحریر کی اطلاعات ارسال یا وصول نہیں کر سکیں گے۔" + "جدول زمانی میں پیغام کا ماخذ دیکھنے کے لئے اختیار فعال کریں۔" + "آپ کے کوئی مسدود صارفین نہیں ہے۔" + "غیر مسدود کریں" + "آپ انکی جانب سے تمام پیغامات دوبارہ دیکھ سکیں گے۔" + "صارف کو غیر مسدود کریں" + "غیر مسدود کر رہا ہے…" + "نمائشی نام" + "آپکا نمائشی نام" + "ایک نامعلوم نقص کا سامنا کرنا پڑا اور معلومات کو تبدیل نہیں کیا جا سکا۔" + "نمایہ کی تجدید کرنے سے قاصر" + "نمایہ میں ترمیم کریں" + "نمایہ کی تجدید ہو رہی ہے…" + "اضافی ترتیبات" + "صوتی اور بصری مکالمات" + "تکوین کی عدم مطابقت" + "ہم نے اختیارات کو تلاش کرنا آسان بنانے کے لیے اطلاعات کی ترتیبات کو آسان بنایا ہے۔ آپ کی ماضی میں منتخب کردہ کچھ حسب ضرورت ترتیبات یہاں نہیں دکھائی گئی ہیں، لیکن وہ اب بھی فعال ہیں۔ + + اگر آپ آگے بڑھتے ہیں تو آپ کی کچھ ترتیبات تبدیل ہو سکتی ہیں۔" + "براہ راست گفتگوہا" + "فی گفتگو حسب ضرورت ترتیب" + "اطلاع کی ترتیب کی تجدید کرتے ہوئے ایک نقص واقع ہوا۔" + "تمام پیغامات" + "صرف تذکرے اور کلیدی الفاظ" + "براہ راست گفتگوہا پر، مجھے مطلع کریں برائے" + "گروہی گفتگوہا پر، مجھے مطلع کریں" + "اس آلے پر اطلاعات فعال کریں" + "تکوین کو درست نہیں کیا گیا ہے، برائے مہربانی دوبارہ کوشش کریں۔" + "گروہی گفتگوہا" + "دعوت نامے" + "آپ کا منزلی خادم مرموز کردہ کمروں میں اس اختیار کی حمایت نہیں کرت، ہوسکتا ہے کچھ کمروں میں آپ کو مطلع نہ کیا جائے۔" + "تذکرے" + "تمام" + "تذکرے" + "مجھے مطلع کریں برائے" + "مجھے @کمرہ پر مطلع کریں" + "اطلاعات موصول کرنے کے لئے، براہ کرم اپنا %1$s تبدیل کریں۔" + "نظام کی ترتیبات" + "نظام کی اطلاعات بند کر دی گئیں" + "اطلاعات" + "ازالہ کریں" + "اطلاعات کا ازالہ کریں" + 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 b590cd95f88..52630e1c7b6 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 @@ -8,14 +8,22 @@ "自訂 Element 通話基礎 URL" "設定 Element 通話的自訂基礎 URL。" "無效的 URL,請確定包含了協定 (http/https) 與正確的地址。" + "在聊天室邀請請求中隱藏大頭貼" + "在時間軸中隱藏媒體預覽" "上傳照片與影片更快且減少資料使用量" "最佳化媒體品質" + "管理與安全" "推播通知提供者" "手動輸入 Markdown,停用格式化文字編輯器。" "已讀回條" "若關閉,您的讀取回條將不會傳送給任何人。您仍然會收到來自其他使用者的讀取回條。" "分享動態" "若關閉,您將無法傳送或接收讀取回條或輸入通知。" + "永遠隱藏" + "永遠顯示" + "在私人聊天室中" + "隨時可以透過點選來顯示隱藏媒體" + "在時間軸中顯示媒體" "啟用選項以在時間軸中檢視訊息來源。" "您並未封鎖使用者" "解除封鎖" @@ -55,6 +63,7 @@ "系統設定" "已關閉系統通知" "通知" + "推播通知歷史紀錄" "疑難排解" "疑難排解通知" diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index d2a03f922f7..520a2a9e187 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -8,14 +8,22 @@ "Custom Element Call base URL" "Set a custom base URL for Element Call." "Invalid URL, please make sure you include the protocol (http/https) and the correct address." + "Hide avatars in room invite requests" + "Hide media previews in timeline" "Upload photos and videos faster and reduce data usage" "Optimise media quality" + "Moderation and Safety" "Push notification provider" "Disable the rich text editor to type Markdown manually." "Read receipts" "If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users." "Share presence" "If turned off, you won’t be able to send or receive read receipts or typing notifications." + "Always hide" + "Always show" + "In private rooms" + "A hidden media can always be shown by tapping on it" + "Show media in timeline" "Enable option to view message source in the timeline." "You have no blocked users" "Unblock" @@ -55,6 +63,7 @@ If you proceed, some of your settings may change." "system settings" "System notifications turned off" "Notifications" + "Push history" "Troubleshoot" "Troubleshoot notifications" 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 a5014ac719f..de84e887d6a 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 @@ -11,11 +11,12 @@ 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.matrix.api.media.MediaPreviewValue import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -30,12 +31,16 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitLastSequentialItem() - assertThat(initialState.isDeveloperModeEnabled).isFalse() - assertThat(initialState.showChangeThemeDialog).isFalse() - assertThat(initialState.isSharePresenceEnabled).isTrue() - assertThat(initialState.doesCompressMedia).isTrue() - assertThat(initialState.theme).isEqualTo(Theme.System) + with(awaitItem()) { + assertThat(isDeveloperModeEnabled).isFalse() + assertThat(isSharePresenceEnabled).isTrue() + assertThat(doesCompressMedia).isTrue() + assertThat(theme).isEqualTo(ThemeOption.System) + assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse() + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(mediaPreviewConfigState.setTimelineMediaPreviewAction).isEqualTo(AsyncAction.Uninitialized) + } } } @@ -45,12 +50,17 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitLastSequentialItem() - assertThat(initialState.isDeveloperModeEnabled).isFalse() - initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) - assertThat(awaitItem().isDeveloperModeEnabled).isTrue() - initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(false)) - assertThat(awaitItem().isDeveloperModeEnabled).isFalse() + with(awaitItem()) { + assertThat(isDeveloperModeEnabled).isFalse() + eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) + } + with(awaitItem()) { + assertThat(isDeveloperModeEnabled).isTrue() + eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(false)) + } + with(awaitItem()) { + assertThat(isDeveloperModeEnabled).isFalse() + } } } @@ -60,12 +70,17 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitLastSequentialItem() - assertThat(initialState.isSharePresenceEnabled).isTrue() - initialState.eventSink.invoke(AdvancedSettingsEvents.SetSharePresenceEnabled(false)) - assertThat(awaitItem().isSharePresenceEnabled).isFalse() - initialState.eventSink.invoke(AdvancedSettingsEvents.SetSharePresenceEnabled(true)) - assertThat(awaitItem().isSharePresenceEnabled).isTrue() + with(awaitItem()) { + assertThat(isSharePresenceEnabled).isTrue() + eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(false)) + } + with(awaitItem()) { + assertThat(isSharePresenceEnabled).isFalse() + eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(true)) + } + with(awaitItem()) { + assertThat(isSharePresenceEnabled).isTrue() + } } } @@ -75,12 +90,17 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitLastSequentialItem() - assertThat(initialState.doesCompressMedia).isTrue() - initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(false)) - assertThat(awaitItem().doesCompressMedia).isFalse() - initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true)) - assertThat(awaitItem().doesCompressMedia).isTrue() + with(awaitItem()) { + assertThat(doesCompressMedia).isTrue() + eventSink(AdvancedSettingsEvents.SetCompressMedia(false)) + } + with(awaitItem()) { + assertThat(doesCompressMedia).isFalse() + eventSink(AdvancedSettingsEvents.SetCompressMedia(true)) + } + with(awaitItem()) { + assertThat(doesCompressMedia).isTrue() + } } } @@ -90,28 +110,112 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitLastSequentialItem() - initialState.eventSink.invoke(AdvancedSettingsEvents.ChangeTheme) - val withDialog = awaitItem() - assertThat(withDialog.showChangeThemeDialog).isTrue() - // Cancel - withDialog.eventSink(AdvancedSettingsEvents.CancelChangeTheme) - val withoutDialog = awaitItem() - assertThat(withoutDialog.showChangeThemeDialog).isFalse() - withDialog.eventSink.invoke(AdvancedSettingsEvents.ChangeTheme) - assertThat(awaitItem().showChangeThemeDialog).isTrue() - withDialog.eventSink(AdvancedSettingsEvents.SetTheme(Theme.Light)) - val withNewTheme = awaitItem() - assertThat(withNewTheme.showChangeThemeDialog).isFalse() - assertThat(withNewTheme.theme).isEqualTo(Theme.Light) + with(awaitItem()) { + assertThat(theme).isEqualTo(ThemeOption.System) + eventSink(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark)) + } + with(awaitItem()) { + assertThat(theme).isEqualTo(ThemeOption.Dark) + eventSink(AdvancedSettingsEvents.SetTheme(ThemeOption.Light)) + } + with(awaitItem()) { + assertThat(theme).isEqualTo(ThemeOption.Light) + eventSink(AdvancedSettingsEvents.SetTheme(ThemeOption.System)) + } + with(awaitItem()) { + assertThat(theme).isEqualTo(ThemeOption.System) + } } } - private fun createAdvancedSettingsPresenter( + @Test + fun `present - hide invite avatars`() = runTest { + val mediaPreviewStore = FakeMediaPreviewConfigStateStore() + val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse() + eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(true)) + } + with(awaitItem()) { + assertThat(mediaPreviewConfigState.hideInviteAvatars).isTrue() + eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(false)) + } + with(awaitItem()) { + assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse() + } + } + assertThat(mediaPreviewStore.getSetHideInviteAvatarsEvents()).isEqualTo(listOf(true, false)) + } + + @Test + fun `present - timeline media preview value`() = runTest { + val mediaPreviewStore = FakeMediaPreviewConfigStateStore() + val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) + } + with(awaitItem()) { + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) + } + with(awaitItem()) { + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) + } + } + assertThat(mediaPreviewStore.getSetTimelineMediaPreviewValueEvents()).isEqualTo( + listOf(MediaPreviewValue.Off, MediaPreviewValue.Private) + ) + } + + @Test + fun `present - media preview state with custom initial values`() = runTest { + val mediaPreviewStore = FakeMediaPreviewConfigStateStore( + hideInviteAvatarsValue = true, + timelineMediaPreviewValue = MediaPreviewValue.Private + ) + val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(mediaPreviewConfigState.hideInviteAvatars).isTrue() + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) + } + } + } + + @Test + fun `present - async actions state`() = runTest { + val mediaPreviewStore = FakeMediaPreviewConfigStateStore( + setHideInviteAvatarsActionValue = AsyncAction.Loading, + setTimelineMediaPreviewActionValue = AsyncAction.Success(Unit) + ) + val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Loading) + assertThat(mediaPreviewConfigState.setTimelineMediaPreviewAction).isEqualTo(AsyncAction.Success(Unit)) + } + } + } + + private fun CoroutineScope.createAdvancedSettingsPresenter( appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), + mediaPreviewConfigStateStore: MediaPreviewConfigStateStore = FakeMediaPreviewConfigStateStore(), ) = AdvancedSettingsPresenter( appPreferencesStore = appPreferencesStore, sessionPreferencesStore = sessionPreferencesStore, + mediaPreviewConfigStateStore = mediaPreviewConfigStateStore, + sessionCoroutineScope = this, ) } 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 a8e105e22a3..d5e90047c7d 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 @@ -14,8 +14,9 @@ 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 -import io.element.android.compound.theme.Theme import io.element.android.features.preferences.impl.R +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.compose.LocalAnalyticsService @@ -29,6 +30,7 @@ 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 { @@ -49,29 +51,17 @@ class AdvancedSettingsViewTest { } } - @Test - fun `clicking on Appearance emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( - state = aAdvancedSettingsState( - eventSink = eventsRecorder - ), - ) - rule.clickOn(CommonStrings.common_appearance) - eventsRecorder.assertSingle(AdvancedSettingsEvents.ChangeTheme) - } - @Test fun `clicking on other theme emits the expected event`() { val eventsRecorder = EventsRecorder() rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, - showChangeThemeDialog = true ), ) + rule.clickOn(CommonStrings.common_appearance) rule.clickOn(CommonStrings.common_dark) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(Theme.Dark)) + eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark)) } @Test @@ -140,6 +130,93 @@ class AdvancedSettingsViewTest { ) ) } + + @Test + @Config(qualifiers = "h1080dp") + fun `clicking on hide invite avatars emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + hideInviteAvatars = false + ), + ) + 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`() { + val eventsRecorder = EventsRecorder() + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + timelineMediaPreviewValue = MediaPreviewValue.On + ), + ) + 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`() { + val eventsRecorder = EventsRecorder() + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + timelineMediaPreviewValue = MediaPreviewValue.On + ), + ) + 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`() { + val eventsRecorder = EventsRecorder() + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + timelineMediaPreviewValue = MediaPreviewValue.Off + ), + ) + 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`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + hideInviteAvatars = false, + setHideInviteAvatarsAction = AsyncAction.Loading + ), + ) + // The toggle should be disabled, so clicking should not emit any events + 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`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + timelineMediaPreviewValue = MediaPreviewValue.On, + setTimelineMediaPreviewAction = AsyncAction.Loading + ), + ) + // The options should be disabled, so clicking should not emit any events + rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) + rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) + } } private fun AndroidComposeTestRule.setAdvancedSettingsView( diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/FakeMediaPreviewConfigStateStore.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/FakeMediaPreviewConfigStateStore.kt new file mode 100644 index 00000000000..18c6283965b --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/FakeMediaPreviewConfigStateStore.kt @@ -0,0 +1,51 @@ +/* + * 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.preferences.impl.advanced + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.media.MediaPreviewValue + +class FakeMediaPreviewConfigStateStore( + hideInviteAvatarsValue: Boolean = false, + timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On, + setHideInviteAvatarsActionValue: AsyncAction = AsyncAction.Uninitialized, + setTimelineMediaPreviewActionValue: AsyncAction = AsyncAction.Uninitialized, +) : MediaPreviewConfigStateStore { + private val hideInviteAvatars = mutableStateOf(hideInviteAvatarsValue) + private val timelineMediaPreviewValue = mutableStateOf(timelineMediaPreviewValue) + private val setHideInviteAvatarsAction = mutableStateOf(setHideInviteAvatarsActionValue) + private val setTimelineMediaPreviewAction = mutableStateOf(setTimelineMediaPreviewActionValue) + + private val setHideInviteAvatarsEvents = mutableListOf() + private val setTimelineMediaPreviewValueEvents = mutableListOf() + + @Composable + override fun state(): MediaPreviewConfigState { + return MediaPreviewConfigState( + hideInviteAvatars = hideInviteAvatars.value, + timelineMediaPreviewValue = timelineMediaPreviewValue.value, + setHideInviteAvatarsAction = setHideInviteAvatarsAction.value, + setTimelineMediaPreviewAction = setTimelineMediaPreviewAction.value, + ) + } + + override fun setHideInviteAvatars(hide: Boolean) { + setHideInviteAvatarsEvents.add(hide) + hideInviteAvatars.value = hide + } + + override fun setTimelineMediaPreviewValue(value: MediaPreviewValue) { + setTimelineMediaPreviewValueEvents.add(value) + timelineMediaPreviewValue.value = value + } + + fun getSetHideInviteAvatarsEvents(): List = setHideInviteAvatarsEvents.toList() + fun getSetTimelineMediaPreviewValueEvents(): List = setTimelineMediaPreviewValueEvents.toList() +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStoreTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStoreTest.kt new file mode 100644 index 00000000000..01ede60e92e --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStoreTest.kt @@ -0,0 +1,206 @@ +/* + * 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.preferences.impl.advanced + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService +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.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class MediaPreviewConfigStateStoreTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `initial state is correct with default values`() = runTest { + val store = createMediaPreviewConfigStateStore() + + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + val initialState = awaitItem() + assertThat(initialState.hideInviteAvatars).isFalse() + assertThat(initialState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + assertThat(initialState.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(initialState.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + } + + @Test + fun `state updates when config flow emits new values`() = runTest { + val configFlow = MutableStateFlow(MediaPreviewConfig.DEFAULT) + val mediaPreviewService = FakeMediaPreviewService(configFlow) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + // Initial state + val initialState = awaitItem() + assertThat(initialState.hideInviteAvatars).isFalse() + assertThat(initialState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + + // Update config + configFlow.value = MediaPreviewConfig(hideInviteAvatar = true, mediaPreviewValue = MediaPreviewValue.Private) + + skipItems(1) + // Updated state + val updatedState = awaitItem() + assertThat(updatedState.hideInviteAvatars).isTrue() + assertThat(updatedState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) + } + } + + @Test + fun `setHideInviteAvatars updates state and calls service on success`() = runTest { + val setHideInviteAvatarsValueLambda = lambdaRecorder> { Result.success(Unit) } + val mediaPreviewService = FakeMediaPreviewService( + setHideInviteAvatarsResult = setHideInviteAvatarsValueLambda + ) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isFalse() + } + store.setHideInviteAvatars(true) + + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + } + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + assertThat(state.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + assertThat(state.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Success::class.java) + } + assert(setHideInviteAvatarsValueLambda).isCalledOnce() + } + } + + @Test + fun `setHideInviteAvatars reverts state on failure`() = runTest { + val setHideInviteAvatarsValueLambda = lambdaRecorder> { + Result.failure(Exception()) + } + val mediaPreviewService = FakeMediaPreviewService( + setHideInviteAvatarsResult = setHideInviteAvatarsValueLambda + ) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isFalse() + } + store.setHideInviteAvatars(true) + + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + } + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + assertThat(state.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Loading::class.java) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isFalse() + assertThat(state.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Failure::class.java) + } + assert(setHideInviteAvatarsValueLambda).isCalledOnce() + } + } + + @Test + fun `setTimelineMediaPreviewValue updates state and calls service on success`() = runTest { + val setMediaPreviewValueLambda = lambdaRecorder> { Result.success(Unit) } + val mediaPreviewService = FakeMediaPreviewService( + setMediaPreviewValueResult = setMediaPreviewValueLambda + ) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + } + store.setTimelineMediaPreviewValue(MediaPreviewValue.Off) + + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + } + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + assertThat(state.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + assertThat(state.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Success::class.java) + } + assert(setMediaPreviewValueLambda).isCalledOnce() + } + } + + @Test + fun `setTimelineMediaPreviewValue reverts state on failure`() = runTest { + val setMediaPreviewValueLambda = lambdaRecorder> { + Result.failure(Exception()) + } + val mediaPreviewService = FakeMediaPreviewService( + setMediaPreviewValueResult = setMediaPreviewValueLambda + ) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + } + store.setTimelineMediaPreviewValue(MediaPreviewValue.Off) + + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + } + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + assertThat(state.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Loading::class.java) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + assertThat(state.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Failure::class.java) + } + assert(setMediaPreviewValueLambda).isCalledOnce() + } + } + + private fun TestScope.createMediaPreviewConfigStateStore( + mediaPreviewService: FakeMediaPreviewService = FakeMediaPreviewService(), + snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher() + ): MediaPreviewConfigStateStore = DefaultMediaPreviewConfigStateStore( + sessionCoroutineScope = backgroundScope, + mediaPreviewService = mediaPreviewService, + snackbarDispatcher = snackbarDispatcher + ) +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt index 5e5f6585802..bb68063797b 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Test @@ -40,9 +41,9 @@ class BlockedUsersPresenterTest { @Test fun `present - initial state with blocked users`() = runTest { - val matrixClient = FakeMatrixClient().apply { - ignoredUsersFlow.value = persistentListOf(A_USER_ID) - } + val matrixClient = FakeMatrixClient( + ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID)) + ) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -56,9 +57,10 @@ class BlockedUsersPresenterTest { @Test fun `present - blocked users list updates with new emissions`() = runTest { - val matrixClient = FakeMatrixClient().apply { - ignoredUsersFlow.value = persistentListOf(A_USER_ID) - } + val ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID)) + val matrixClient = FakeMatrixClient( + ignoredUsersFlow = ignoredUsersFlow + ) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -66,7 +68,7 @@ class BlockedUsersPresenterTest { with(awaitItem()) { assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID))) } - matrixClient.ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2) + ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2) skipItems(1) with(awaitItem()) { assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID), MatrixUser(A_USER_ID_2))) @@ -77,8 +79,9 @@ class BlockedUsersPresenterTest { @Test fun `present - blocked users list with data`() = runTest { val alice = MatrixUser(A_USER_ID, displayName = "Alice", avatarUrl = "aliceAvatar") - val matrixClient = FakeMatrixClient().apply { - ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2) + val matrixClient = FakeMatrixClient( + ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2)) + ).apply { givenGetProfileResult(A_USER_ID, Result.success(alice)) givenGetProfileResult(A_USER_ID_2, Result.failure(AN_EXCEPTION)) } @@ -103,9 +106,9 @@ class BlockedUsersPresenterTest { @Test fun `present - unblock user`() = runTest { - val matrixClient = FakeMatrixClient().apply { - ignoredUsersFlow.value = persistentListOf(A_USER_ID) - } + val matrixClient = FakeMatrixClient( + ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID)) + ) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -125,10 +128,10 @@ class BlockedUsersPresenterTest { @Test fun `present - unblock user handles failure`() = runTest { - val matrixClient = FakeMatrixClient().apply { - ignoredUsersFlow.value = persistentListOf(A_USER_ID) - givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned"))) - } + val matrixClient = FakeMatrixClient( + unIgnoreUserResult = { Result.failure(IllegalStateException("User not banned")) }, + ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID)) + ) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -147,10 +150,10 @@ class BlockedUsersPresenterTest { @Test fun `present - unblock user then cancel`() = runTest { - val matrixClient = FakeMatrixClient().apply { - ignoredUsersFlow.value = persistentListOf(A_USER_ID) - givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned"))) - } + val matrixClient = FakeMatrixClient( + unIgnoreUserResult = { Result.failure(IllegalStateException("User not banned")) }, + ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID)) + ) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index a73114ea2c7..9e2c8ca1755 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -10,8 +10,6 @@ package io.element.android.features.preferences.impl.developer import com.google.common.truth.Truth.assertThat -import io.element.android.appconfig.ElementCallConfig -import io.element.android.features.logout.test.FakeLogoutUseCase import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase @@ -25,11 +23,8 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -48,8 +43,6 @@ class DeveloperSettingsPresenterTest { assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized) assertThat(state.customElementCallBaseUrlState).isNotNull() assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() - assertThat(state.isSimpleSlidingSyncEnabled).isFalse() - assertThat(state.hideImagesAndVideos).isFalse() assertThat(state.rageshakeState.isEnabled).isFalse() assertThat(state.rageshakeState.isSupported).isTrue() assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f) @@ -85,7 +78,7 @@ class DeveloperSettingsPresenterTest { presenter.test { skipItems(2) awaitItem().also { state -> - val feature = state.features.first() + val feature = state.features.first { !it.isEnabled } state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, !feature.isEnabled)) } awaitItem().also { state -> @@ -134,7 +127,6 @@ class DeveloperSettingsPresenterTest { } awaitItem().also { state -> assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy") - assertThat(state.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL) } } } @@ -153,56 +145,6 @@ class DeveloperSettingsPresenterTest { } } - @Test - fun `present - toggling simplified sliding sync changes the preferences and logs out the user`() = runTest { - val logoutCallRecorder = lambdaRecorder { "" } - val logoutUseCase = FakeLogoutUseCase(logoutLambda = logoutCallRecorder) - val preferences = InMemoryAppPreferencesStore() - val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase) - presenter.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.isSimpleSlidingSyncEnabled).isFalse() - state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true)) - } - awaitItem().also { state -> - assertThat(state.isSimpleSlidingSyncEnabled).isTrue() - assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue() - advanceUntilIdle() - logoutCallRecorder.assertions().isCalledOnce() - state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false)) - } - awaitItem().also { state -> - assertThat(state.isSimpleSlidingSyncEnabled).isFalse() - assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse() - advanceUntilIdle() - logoutCallRecorder.assertions().isCalledExactly(2) - } - } - } - - @Test - fun `present - toggling hide image and video`() = runTest { - val preferences = InMemoryAppPreferencesStore() - val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences) - presenter.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.hideImagesAndVideos).isFalse() - state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true)) - } - awaitItem().also { state -> - assertThat(state.hideImagesAndVideos).isTrue() - assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue() - state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false)) - } - awaitItem().also { state -> - assertThat(state.hideImagesAndVideos).isFalse() - assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse() - } - } - } - @Test fun `present - changing tracing log level`() = runTest { val preferences = InMemoryAppPreferencesStore() @@ -225,7 +167,6 @@ class DeveloperSettingsPresenterTest { clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(), preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), buildMeta: BuildMeta = aBuildMeta(), - logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }) ): DeveloperSettingsPresenter { return DeveloperSettingsPresenter( featureFlagService = featureFlagService, @@ -234,7 +175,6 @@ class DeveloperSettingsPresenterTest { rageshakePresenter = { aRageshakePreferencesState() }, appPreferencesStore = preferencesStore, buildMeta = buildMeta, - logoutUseCase = logoutUseCase, ) } } 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 7d31517bf5e..5d0030af8e0 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 @@ -8,10 +8,16 @@ package io.element.android.features.preferences.impl.developer import androidx.activity.ComponentActivity +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.isDialog +import androidx.compose.ui.test.isEditable +import androidx.compose.ui.test.isFocusable import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem @@ -56,11 +62,13 @@ class DeveloperSettingsViewTest { ), ) rule.clickOn(R.string.screen_advanced_settings_element_call_base_url) + val textInputNode = rule.onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) + textInputNode.performTextInput("https://call.element.dev") rule.clickOn(CommonStrings.action_ok) - eventsRecorder.assertSingle(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.io")) + eventsRecorder.assertSingle(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.dev")) } - @Config(qualifiers = "h1024dp") + @Config(qualifiers = "h1200dp") @Test fun `clicking on open showkase invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) @@ -89,7 +97,7 @@ class DeveloperSettingsViewTest { eventsRecorder.assertSingle(DeveloperSettingsEvents.SetTracingLogLevel(LogLevelItem.DEBUG)) } - @Config(qualifiers = "h1500dp") + @Config(qualifiers = "h1700dp") @Test fun `clicking on clear cache emits the expected event`() { val eventsRecorder = EventsRecorder() @@ -101,31 +109,6 @@ class DeveloperSettingsViewTest { rule.onNodeWithText("Clear cache").performClick() eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache) } - - @Config(qualifiers = "h1500dp") - @Test - fun `clicking on the simplified sliding sync switch emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setDeveloperSettingsView( - state = aDeveloperSettingsState( - eventSink = eventsRecorder - ), - ) - rule.onNodeWithText("Enable Simplified Sliding Sync").performClick() - eventsRecorder.assertSingle(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true)) - } - - @Test - fun `clicking on the hide images and videos switch emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setDeveloperSettingsView( - state = aDeveloperSettingsState( - eventSink = eventsRecorder - ), - ) - rule.onNodeWithText("Hide image & video previews").performClick() - eventsRecorder.assertSingle(DeveloperSettingsEvents.SetHideImagesAndVideos(true)) - } } private fun AndroidComposeTestRule.setDeveloperSettingsView( 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 aa5191d201d..b452b9c7442 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 @@ -14,7 +14,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.AN_EXCEPTION 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.FakeRoomListService @@ -81,7 +81,7 @@ class EditDefaultNotificationSettingsPresenterTest { fun `present - edit default notification setting failed`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService) - notificationSettingsService.givenSetDefaultNotificationModeError(A_THROWABLE) + notificationSettingsService.givenSetDefaultNotificationModeError(AN_EXCEPTION) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { 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 57179955a89..8517d9d2859 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 @@ -16,7 +16,7 @@ import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermiss import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.push.api.PushService @@ -208,7 +208,7 @@ class NotificationSettingsPresenterTest { fun `present - clear notification settings change error`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() val presenter = createNotificationSettingsPresenter(notificationSettingsService) - notificationSettingsService.givenSetAtRoomError(A_THROWABLE) + notificationSettingsService.givenSetAtRoomError(AN_EXCEPTION) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -240,8 +240,11 @@ class NotificationSettingsPresenterTest { presenter.present() }.test { val initialState = awaitLastSequentialItem() - assertThat(initialState.currentPushDistributor).isEqualTo(AsyncData.Success("aDistributorName0")) - assertThat(initialState.availablePushDistributors).containsExactly("aDistributorName0", "aDistributorName1") + assertThat(initialState.currentPushDistributor).isEqualTo(AsyncData.Success(Distributor(value = "aDistributorValue0", name = "aDistributorName0"))) + assertThat(initialState.availablePushDistributors).containsExactly( + Distributor(value = "aDistributorValue0", name = "aDistributorName0"), + Distributor(value = "aDistributorValue1", name = "aDistributorName1"), + ) initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) val withDialog = awaitItem() assertThat(withDialog.showChangePushProviderDialog).isTrue() @@ -257,11 +260,35 @@ class NotificationSettingsPresenterTest { assertThat(withNewProvider.currentPushDistributor).isInstanceOf(AsyncData.Loading::class.java) skipItems(1) val lastItem = awaitItem() - assertThat(lastItem.currentPushDistributor).isEqualTo(AsyncData.Success("aDistributorName1")) + assertThat(lastItem.currentPushDistributor).isEqualTo(AsyncData.Success(Distributor(value = "aDistributorValue1", name = "aDistributorName1"))) cancelAndIgnoreRemainingEvents() } } + @Test + fun `present - change push provider to the same value is no op`() = runTest { + val presenter = createNotificationSettingsPresenter( + pushService = createFakePushService(), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitLastSequentialItem() + assertThat(initialState.currentPushDistributor).isEqualTo(AsyncData.Success(Distributor(value = "aDistributorValue0", name = "aDistributorName0"))) + assertThat(initialState.availablePushDistributors).containsExactly( + Distributor(value = "aDistributorValue0", name = "aDistributorName0"), + Distributor(value = "aDistributorValue1", name = "aDistributorName1"), + ) + initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) + assertThat(awaitItem().showChangePushProviderDialog).isTrue() + // Choose the same value (index 0) + initialState.eventSink(NotificationSettingsEvents.SetPushProvider(0)) + val withNewProvider = awaitItem() + assertThat(withNewProvider.showChangePushProviderDialog).isFalse() + expectNoEvents() + } + } + @Test fun `present - RefreshSystemNotificationsEnabled also refreshes fullScreenIntentState`() = runTest { var lambdaResult = aFullScreenIntentPermissionsState(permissionGranted = false) 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 01cc8179411..3b2a2663a51 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 @@ -66,6 +66,22 @@ class NotificationSettingsViewTest { eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on push history notification invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnce { + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + eventSink = eventsRecorder + ), + onPushHistoryClick = it + ) + rule.clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title) + } + eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + } + @Config(qualifiers = "h1024dp") @Test fun `clicking on group chats invokes the expected callback`() { @@ -267,7 +283,7 @@ class NotificationSettingsViewTest { state = aValidNotificationSettingsState( eventSink = eventsRecorder, showChangePushProviderDialog = true, - availablePushDistributors = listOf("P1", "P2") + availablePushDistributors = listOf(aDistributor("P1"), aDistributor("P2")) ), ) rule.onNodeWithText("P2").performClick() @@ -284,6 +300,7 @@ private fun AndroidComposeTestRule.setNotif state: NotificationSettingsState, onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(), onTroubleshootNotificationsClick: () -> Unit = EnsureNeverCalled(), + onPushHistoryClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(), ) { setContent { @@ -291,6 +308,7 @@ private fun AndroidComposeTestRule.setNotif state = state, onOpenEditDefault = onOpenEditDefault, onTroubleshootNotificationsClick = onTroubleshootNotificationsClick, + onPushHistoryClick = onPushHistoryClick, onBackClick = onBackClick, ) } 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 6f85c436210..1f1b527a45c 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 @@ -5,28 +5,33 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.preferences.impl.root -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import 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 import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.indicator.impl.DefaultIndicatorService +import io.element.android.libraries.indicator.api.IndicatorService +import io.element.android.libraries.indicator.test.FakeIndicatorService +import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_AVATAR_URL 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 -import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -37,11 +42,16 @@ class PreferencesRootPresenterTest { @Test fun `present - initial state`() = runTest { - val matrixClient = FakeMatrixClient(canDeactivateAccountResult = { true }) - val presenter = createPresenter(matrixClient = matrixClient) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + val accountManagementUrlResult = lambdaRecorder> { action -> + Result.success("$action url") + } + val matrixClient = FakeMatrixClient( + canDeactivateAccountResult = { true }, + accountManagementUrlResult = accountManagementUrlResult, + ) + createPresenter( + matrixClient = matrixClient, + ).test { val initialState = awaitItem() assertThat(initialState.myUser).isEqualTo( MatrixUser( @@ -61,7 +71,7 @@ class PreferencesRootPresenterTest { ) assertThat(initialState.version).isEqualTo("A Version") assertThat(loadedState.showSecureBackup).isFalse() - assertThat(loadedState.showSecureBackupBadge).isTrue() + assertThat(loadedState.showSecureBackupBadge).isFalse() assertThat(loadedState.accountManagementUrl).isNull() assertThat(loadedState.devicesManagementUrl).isNull() assertThat(loadedState.showAnalyticsSettings).isFalse() @@ -69,21 +79,66 @@ class PreferencesRootPresenterTest { assertThat(loadedState.showLockScreenSettings).isTrue() assertThat(loadedState.showNotificationSettings).isTrue() assertThat(loadedState.canDeactivateAccount).isTrue() + assertThat(loadedState.canReportBug).isTrue() assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState()) assertThat(loadedState.snackbarMessage).isNull() + skipItems(1) + val finalState = awaitItem() + accountManagementUrlResult.assertions().isCalledExactly(2) + .withSequence( + listOf(value(AccountManagementAction.Profile)), + listOf(value(AccountManagementAction.SessionsList)), + ) + assertThat(finalState.accountManagementUrl).isEqualTo("Profile url") + assertThat(finalState.devicesManagementUrl).isEqualTo("SessionsList url") + } + } + + @Test + fun `present - cannot report bug`() = runTest { + val matrixClient = FakeMatrixClient( + canDeactivateAccountResult = { true }, + accountManagementUrlResult = { Result.success("") }, + ) + createPresenter( + matrixClient = matrixClient, + rageshakeFeatureAvailability = { false }, + ).test { + val initialState = awaitItem() + assertThat(initialState.canReportBug).isFalse() + skipItems(1) + } + } + + @Test + fun `present - secure backup badge`() = runTest { + val matrixClient = FakeMatrixClient( + canDeactivateAccountResult = { true }, + accountManagementUrlResult = { Result.success("") }, + ) + val indicatorService = FakeIndicatorService() + createPresenter( + matrixClient = matrixClient, + rageshakeFeatureAvailability = { false }, + indicatorService = indicatorService, + ).test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.showSecureBackupBadge).isFalse() + indicatorService.setShowSettingChatBackupIndicator(true) + val finalState = awaitItem() + assertThat(finalState.showSecureBackupBadge).isTrue() } } @Test fun `present - can deactivate account is false if the Matrix client say so`() = runTest { - val presenter = createPresenter( + createPresenter( matrixClient = FakeMatrixClient( - canDeactivateAccountResult = { false } - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + canDeactivateAccountResult = { false }, + accountManagementUrlResult = { Result.success(null) }, + ), + ).test { val loadedState = awaitFirstItem() assertThat(loadedState.canDeactivateAccount).isFalse() } @@ -91,12 +146,13 @@ class PreferencesRootPresenterTest { @Test fun `present - developer settings is hidden by default in release builds`() = runTest { - val presenter = createPresenter( + createPresenter( + matrixClient = FakeMatrixClient( + canDeactivateAccountResult = { true }, + accountManagementUrlResult = { Result.success(null) }, + ), showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE)) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + ).test { val loadedState = awaitFirstItem() assertThat(loadedState.showDeveloperSettings).isFalse() } @@ -104,12 +160,13 @@ class PreferencesRootPresenterTest { @Test fun `present - developer settings can be enabled in release builds`() = runTest { - val presenter = createPresenter( + createPresenter( + matrixClient = FakeMatrixClient( + canDeactivateAccountResult = { true }, + accountManagementUrlResult = { Result.success(null) }, + ), showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE)) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + ).test { val loadedState = awaitFirstItem() repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) { assertThat(loadedState.showDeveloperSettings).isFalse() @@ -125,9 +182,11 @@ class PreferencesRootPresenterTest { } private fun createPresenter( - matrixClient: FakeMatrixClient = FakeMatrixClient(canDeactivateAccountResult = { true }), + matrixClient: FakeMatrixClient = FakeMatrixClient(), sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)), + rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, + indicatorService: IndicatorService = FakeIndicatorService(), ) = PreferencesRootPresenter( matrixClient = matrixClient, sessionVerificationService = sessionVerificationService, @@ -135,11 +194,9 @@ class PreferencesRootPresenterTest { versionFormatter = FakeVersionFormatter(), snackbarDispatcher = SnackbarDispatcher(), featureFlagService = FakeFeatureFlagService(), - indicatorService = DefaultIndicatorService( - sessionVerificationService = sessionVerificationService, - encryptionService = FakeEncryptionService(), - ), + indicatorService = indicatorService, directLogoutPresenter = { aDirectLogoutState() }, showDeveloperSettingsProvider = showDeveloperSettingsProvider, + rageshakeFeatureAvailability = rageshakeFeatureAvailability, ) } 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 778db4a4a88..cfdc63984ca 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 @@ -11,13 +11,19 @@ import androidx.test.platform.app.InstrumentationRegistry import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.ftue.test.FakeFtueService +import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.preferences.impl.DefaultCacheService 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.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.push.test.FakePushService +import io.element.android.services.appnavstate.api.ActiveRoomsHolder 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.flow.first import kotlinx.coroutines.test.runTest import okhttp3.OkHttpClient import org.junit.Test @@ -28,8 +34,10 @@ import org.robolectric.RobolectricTestRunner class DefaultClearCacheUseCaseTest { @Test fun `execute clear cache should do all the expected tasks`() = runTest { + val activeRoomsHolder = ActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) } val clearCacheLambda = lambdaRecorder { } val matrixClient = FakeMatrixClient( + sessionId = A_SESSION_ID, clearCacheLambda = clearCacheLambda, ) val defaultCacheService = DefaultCacheService() @@ -38,9 +46,13 @@ class DefaultClearCacheUseCaseTest { resetLambda = resetFtueLambda, ) val setIgnoreRegistrationErrorLambda = lambdaRecorder { _, _ -> } + val resetBatteryOptimizationStateResult = lambdaRecorder { } val pushService = FakePushService( - setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda + setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda, + resetBatteryOptimizationStateResult = resetBatteryOptimizationStateResult, ) + val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID)) + assertThat(seenInvitesStore.seenRoomIds().first()).isNotEmpty() val sut = DefaultClearCacheUseCase( context = InstrumentationRegistry.getInstrumentation().context, matrixClient = matrixClient, @@ -49,6 +61,8 @@ class DefaultClearCacheUseCaseTest { okHttpClient = { OkHttpClient.Builder().build() }, ftueService = ftueService, pushService = pushService, + seenInvitesStore = seenInvitesStore, + activeRoomsHolder = activeRoomsHolder, ) defaultCacheService.clearedCacheEventFlow.test { sut.invoke() @@ -56,7 +70,10 @@ class DefaultClearCacheUseCaseTest { resetFtueLambda.assertions().isCalledOnce() setIgnoreRegistrationErrorLambda.assertions().isCalledOnce() .with(value(matrixClient.sessionId), value(false)) + resetBatteryOptimizationStateResult.assertions().isCalledOnce() assertThat(awaitItem()).isEqualTo(matrixClient.sessionId) + assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() + assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull() } } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt index 47830d2449f..01e56e8f59b 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt @@ -393,7 +393,7 @@ class EditUserProfilePresenterTest { ), ) fakePickerProvider.givenResult(anotherAvatarUri) - fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no"))) + fakeMediaPreProcessor.givenResult(Result.failure(RuntimeException("Oh no"))) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -410,7 +410,7 @@ class EditUserProfilePresenterTest { fun `present - sets save action to failure if name update fails`() = runTest { val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val matrixClient = FakeMatrixClient().apply { - givenSetDisplayNameResult(Result.failure(Throwable("!"))) + givenSetDisplayNameResult(Result.failure(RuntimeException("!"))) } saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name")) } @@ -419,7 +419,7 @@ class EditUserProfilePresenterTest { fun `present - sets save action to failure if removing avatar fails`() = runTest { val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val matrixClient = FakeMatrixClient().apply { - givenRemoveAvatarResult(Result.failure(Throwable("!"))) + givenRemoveAvatarResult(Result.failure(RuntimeException("!"))) } saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) } @@ -429,7 +429,7 @@ class EditUserProfilePresenterTest { givenPickerReturnsFile() val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val matrixClient = FakeMatrixClient().apply { - givenUploadAvatarResult(Result.failure(Throwable("!"))) + givenUploadAvatarResult(Result.failure(RuntimeException("!"))) } saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) } @@ -439,7 +439,7 @@ class EditUserProfilePresenterTest { givenPickerReturnsFile() val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) val matrixClient = FakeMatrixClient().apply { - givenSetDisplayNameResult(Result.failure(Throwable("!"))) + givenSetDisplayNameResult(Result.failure(RuntimeException("!"))) } val presenter = createEditUserProfilePresenter(matrixUser = user, matrixClient = matrixClient) moleculeFlow(RecompositionMode.Immediate) { diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt new file mode 100644 index 00000000000..34e740d4ab6 --- /dev/null +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt @@ -0,0 +1,12 @@ +/* + * 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.rageshake.api + +fun interface RageshakeFeatureAvailability { + fun isAvailable(): Boolean +} diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesState.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesState.kt index 9207d5ff380..40a9d0282be 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesState.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesState.kt @@ -8,6 +8,7 @@ package io.element.android.features.rageshake.api.preferences data class RageshakePreferencesState( + val isFeatureEnabled: Boolean, val isEnabled: Boolean, val isSupported: Boolean, val sensitivity: Float, diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt index d18123cd8d1..a98c75a02a4 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt @@ -12,14 +12,21 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider open class RageshakePreferencesStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aRageshakePreferencesState().copy(isEnabled = true, isSupported = true, sensitivity = 0.5f), - aRageshakePreferencesState().copy(isEnabled = true, isSupported = false, sensitivity = 0.5f), + aRageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f), + aRageshakePreferencesState(isEnabled = true, isSupported = false, sensitivity = 0.5f), ) } -fun aRageshakePreferencesState() = RageshakePreferencesState( - isEnabled = false, - isSupported = true, - sensitivity = 0.3f, - eventSink = {} +fun aRageshakePreferencesState( + isFeatureEnabled: Boolean = true, + isEnabled: Boolean = false, + isSupported: Boolean = true, + sensitivity: Float = 0.3f, + eventSink: (RageshakePreferencesEvents) -> Unit = {} +) = RageshakePreferencesState( + isFeatureEnabled = isFeatureEnabled, + isEnabled = isEnabled, + isSupported = isSupported, + sensitivity = sensitivity, + eventSink = eventSink, ) diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt index 51194a2d9cc..86f1c052476 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt @@ -16,9 +16,10 @@ import io.element.android.features.rageshake.api.R import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceSlide import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -35,24 +36,30 @@ fun RageshakePreferencesView( } Column(modifier = modifier) { - PreferenceCategory(title = stringResource(id = R.string.settings_rageshake)) { - if (state.isSupported) { - PreferenceSwitch( - title = stringResource(id = CommonStrings.preference_rageshake), - isChecked = state.isEnabled, - onCheckedChange = ::onEnabledChanged - ) - PreferenceSlide( - title = stringResource(id = R.string.settings_rageshake_detection_threshold), - // summary = stringResource(id = CommonStrings.settings_rageshake_detection_threshold_summary), - value = state.sensitivity, - enabled = state.isEnabled, - // 5 possible values - steps are in ]0, 1[ - steps = 3, - onValueChange = ::onSensitivityChanged - ) - } else { - PreferenceText(title = "Rageshaking is not supported by your device") + if (state.isFeatureEnabled) { + PreferenceCategory(title = stringResource(id = R.string.settings_rageshake)) { + if (state.isSupported) { + PreferenceSwitch( + title = stringResource(id = CommonStrings.preference_rageshake), + isChecked = state.isEnabled, + onCheckedChange = ::onEnabledChanged + ) + PreferenceSlide( + title = stringResource(id = R.string.settings_rageshake_detection_threshold), + // summary = stringResource(id = CommonStrings.settings_rageshake_detection_threshold_summary), + value = state.sensitivity, + enabled = state.isEnabled, + // 5 possible values - steps are in ]0, 1[ + steps = 3, + onValueChange = ::onSensitivityChanged + ) + } else { + ListItem( + headlineContent = { + Text("Rageshaking is not supported by your device") + }, + ) + } } } } diff --git a/features/rageshake/api/src/main/res/values-cy/translations.xml b/features/rageshake/api/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..c576aed469e --- /dev/null +++ b/features/rageshake/api/src/main/res/values-cy/translations.xml @@ -0,0 +1,7 @@ + + + "Chwalodd %1$s y tro diwethaf iddo gael ei ddefnyddio. Hoffech chi rannu adroddiad gwall gyda ni?" + "Mae\'n ymddangos eich bod yn ysgwyd y ffôn mewn rhwystredigaeth. Hoffech chi agor y sgrin adrodd gwallau?" + "Rageshake" + "Trothwy canfod" + diff --git a/features/rageshake/api/src/main/res/values-da/translations.xml b/features/rageshake/api/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..45486f41da1 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-da/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s crashede sidste gang den blev brugt. Vil du dele en ulykkesrapport med os?" + "Det ser ud til, at du ryster telefonen i frustration. Vil du åbne fejlrapporteringsskærmen?" + "Ryst enheden i frustration" + "Tærskel for registrering" + diff --git a/features/rageshake/api/src/main/res/values-de/translations.xml b/features/rageshake/api/src/main/res/values-de/translations.xml index 46d2be277e7..dbee250bfa7 100644 --- a/features/rageshake/api/src/main/res/values-de/translations.xml +++ b/features/rageshake/api/src/main/res/values-de/translations.xml @@ -1,7 +1,7 @@ - "%1$s ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?" - "Du scheinst das Telefon aus Frustration zu schütteln. Möchtest du den Bildschirm für den Fehlerbericht öffnen?" + "%1$s ist bei der letzten Nutzung abgestürzt. Möchten Sie einen Absturzbericht mit uns teilen?" + "Sie scheinen das Telefon aus Frustration zu schütteln. Möchten Sie den Bildschirm für einen Fehlerbericht öffnen?" "Rageshake" "Erkennungsschwelle" diff --git a/features/rageshake/api/src/main/res/values-eu/translations.xml b/features/rageshake/api/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..48f9914a302 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-eu/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s kraskatu zen azkenekoz erabili zenean. Gurekin partekatu nahi al duzu kraskatzearen txostena?" + "Frustrazioaren eraginez mugikorra astintzen ari zarela dirudi. Erroreen berri emateko pantaila ireki nahi al duzu?" + diff --git a/features/rageshake/api/src/main/res/values-fa/translations.xml b/features/rageshake/api/src/main/res/values-fa/translations.xml index 2c9e513ff80..5641d22e69f 100644 --- a/features/rageshake/api/src/main/res/values-fa/translations.xml +++ b/features/rageshake/api/src/main/res/values-fa/translations.xml @@ -1,5 +1,6 @@ + "%1$sآخرین باری که استفاده شد، از کار افتاد. آیا مایلید گزارش خرابی را با ما به اشتراک بگذارید؟" "به نظر می‌رسد دارید گوشی خود را به دلیل کار نکردن تکان می‌دهید! آیا می‌خواهید یک اشکال در برنامه گزارش نمایید؟" "تکان دادن" "آستانهٔ تشخیص" diff --git a/features/rageshake/api/src/main/res/values-lt/translations.xml b/features/rageshake/api/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..49762d57ca3 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-lt/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s nulūžo paskutinį kartą, kai buvo naudojama. Ar norėtumėte su mumis pasidalyti strigčių ataskaita?" + "Atrodo, kad nusivylęs purtote telefoną. Ar norėtumėte atidaryti pranešimo apie klaidas ekraną?" + "Rageshake" + "Aptikimo riba" + diff --git a/features/rageshake/api/src/main/res/values-nb/translations.xml b/features/rageshake/api/src/main/res/values-nb/translations.xml index ca37b6e6b6f..dfc2d710adf 100644 --- a/features/rageshake/api/src/main/res/values-nb/translations.xml +++ b/features/rageshake/api/src/main/res/values-nb/translations.xml @@ -2,5 +2,6 @@ "%1$s krasjet sist gang den ble brukt. Vil du dele en krasjrapport med oss?" "Du ser ut til å riste på telefonen i frustrasjon. Vil du åpne feilrapportskjermen?" + "Rageshake" "Gjenkjenningsterskel" diff --git a/features/rageshake/api/src/main/res/values-tr/translations.xml b/features/rageshake/api/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..c2d0449fbd6 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-tr/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s son kullanıldığında çöktü. Bizimle bir çökme raporu paylaşmak ister misiniz?" + "Sinirden telefonu sallıyor gibi görünüyorsunuz. Hata raporu ekranını açmak ister misiniz?" + "Rageshake" + "Algılama eşiği" + diff --git a/features/rageshake/api/src/main/res/values-ur/translations.xml b/features/rageshake/api/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..9532f4eb568 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-ur/translations.xml @@ -0,0 +1,7 @@ + + + "%1$sآخری بار استعمال ہونے پر ٹکرا گیا۔ کیا آپ ہمارے ساتھ ٹکر کی گزارش (رپورٹ) کا اشتراک کرنا چاہیں گے؟" + "لگتا ہے آپ مایوسی میں ہاتف ہلا رہے ہیں۔ کیا آپ گزارش خطاء نمائش کھولنا چاہیں گے؟" + "غصے سے جھٹکانا" + "حد کھوج" + diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt new file mode 100644 index 00000000000..5f7548e9ecd --- /dev/null +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt @@ -0,0 +1,22 @@ +/* + * 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.rageshake.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.RageshakeConfig +import io.element.android.appconfig.isEnabled +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultRageshakeFeatureAvailability @Inject constructor() : RageshakeFeatureAvailability { + override fun isAvailable(): Boolean { + return RageshakeConfig.isEnabled + } +} diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportNode.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportNode.kt index e1e394c46f5..c124971efbd 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportNode.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportNode.kt @@ -7,10 +7,9 @@ package io.element.android.features.rageshake.impl.bugreport -import android.app.Activity +import androidx.activity.compose.LocalActivity 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 @@ -38,7 +37,7 @@ class BugReportNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = presenter.present() - val activity = LocalContext.current as? Activity + val activity = LocalActivity.current BugReportView( state = state, modifier = modifier, diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt index 88bba32f23c..f42d2520800 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt @@ -16,12 +16,13 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.features.rageshake.api.crash.CrashDataStore import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporterListener -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder +import io.element.android.features.rageshake.impl.crash.CrashDataStore +import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.annotations.AppCoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -30,6 +31,7 @@ class BugReportPresenter @Inject constructor( private val bugReporter: BugReporter, private val crashDataStore: CrashDataStore, private val screenshotHolder: ScreenshotHolder, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : Presenter { private class BugReporterUploadListener( @@ -64,9 +66,9 @@ class BugReportPresenter @Inject constructor( screenshotHolder.getFileUri() ) } - val crashInfo: String by crashDataStore - .crashInfo() - .collectAsState(initial = "") + val crashInfo: String by remember { + crashDataStore.crashInfo() + }.collectAsState(initial = "") val sendingProgress = remember { mutableFloatStateOf(0f) diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt index fbf91fa46d8..760106b0fb2 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt @@ -28,8 +28,8 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.ImageRequest +import coil3.compose.AsyncImage +import coil3.request.ImageRequest import io.element.android.features.rageshake.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.async.AsyncActionView @@ -38,13 +38,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.PreferenceRow import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TextFieldValidity import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -72,8 +73,8 @@ fun BugReportView( TextField( value = descriptionFieldState, modifier = Modifier - .fillMaxWidth() - .onTabOrEnterKeyFocusNext(LocalFocusManager.current), + .fillMaxWidth() + .onTabOrEnterKeyFocusNext(LocalFocusManager.current), enabled = isFormEnabled, placeholder = stringResource(id = R.string.screen_bug_report_editor_placeholder), supportingText = stringResource(id = R.string.screen_bug_report_editor_description), @@ -90,13 +91,15 @@ fun BugReportView( keyboardController?.hide() }), minLines = 3, - isError = state.isDescriptionInError, + validity = if (state.isDescriptionInError) TextFieldValidity.Invalid else TextFieldValidity.None, ) } Spacer(modifier = Modifier.height(16.dp)) PreferenceDivider() - PreferenceText( - title = stringResource(id = R.string.screen_bug_report_view_logs), + ListItem( + headlineContent = { + Text(stringResource(id = R.string.screen_bug_report_view_logs)) + }, enabled = isFormEnabled, onClick = onViewLogs, ) @@ -135,7 +138,6 @@ fun BugReportView( modifier = Modifier.fillMaxWidth(fraction = 0.5f), model = model, contentDescription = null, - placeholder = debugPlaceholderBackground(), ) } } @@ -148,8 +150,8 @@ fun BugReportView( enabled = state.submitEnabled, showProgress = state.sending.isLoading(), modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp, bottom = 16.dp) + .fillMaxWidth() + .padding(top = 24.dp, bottom = 16.dp) ) } } diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/CrashDataStore.kt similarity index 88% rename from features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDataStore.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/CrashDataStore.kt index 5a13f44a0ad..3d6df9a4246 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/CrashDataStore.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.api.crash +package io.element.android.features.rageshake.impl.crash import kotlinx.coroutines.flow.Flow diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt index 749ba4255cb..fffb87722ad 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt @@ -9,15 +9,17 @@ package io.element.android.features.rageshake.impl.crash import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.rageshake.api.crash.CrashDataStore +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.crash.CrashDetectionEvents import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import javax.inject.Inject @@ -25,12 +27,18 @@ import javax.inject.Inject class DefaultCrashDetectionPresenter @Inject constructor( private val buildMeta: BuildMeta, private val crashDataStore: CrashDataStore, -) : - CrashDetectionPresenter { + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, +) : CrashDetectionPresenter { @Composable override fun present(): CrashDetectionState { val localCoroutineScope = rememberCoroutineScope() - val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false) + val crashDetected = remember { + if (rageshakeFeatureAvailability.isAvailable()) { + crashDataStore.appHasCrashed() + } else { + flowOf(false) + } + }.collectAsState(false) fun handleEvents(event: CrashDetectionEvents) { when (event) { diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt index 2cdea62b1a8..0b86bb4ef00 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt @@ -15,7 +15,6 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.rageshake.api.crash.CrashDataStore import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/detection/DefaultRageshakeDetectionPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/detection/DefaultRageshakeDetectionPresenter.kt index 29785d34fea..1a8aed7051e 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/detection/DefaultRageshakeDetectionPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/detection/DefaultRageshakeDetectionPresenter.kt @@ -20,9 +20,9 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionPre import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.features.rageshake.api.preferences.RageshakePreferencesEvents import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter -import io.element.android.features.rageshake.api.rageshake.RageShake import io.element.android.features.rageshake.api.screenshot.ImageResult -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder +import io.element.android.features.rageshake.impl.rageshake.RageShake +import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder import io.element.android.libraries.di.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -75,7 +75,8 @@ class DefaultRageshakeDetectionPresenter @Inject constructor( LaunchedEffect(preferencesState.sensitivity) { rageShake.setSensitivity(preferencesState.sensitivity) } - val shouldStart = preferencesState.isEnabled && + val shouldStart = preferencesState.isFeatureEnabled && + preferencesState.isEnabled && preferencesState.isSupported && isStarted.value && !takeScreenshot.value && diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt index e129bed8d5e..b4f45a3bdc8 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt @@ -10,15 +10,18 @@ package io.element.android.features.rageshake.impl.preferences 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 import androidx.compose.runtime.saveable.rememberSaveable import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.preferences.RageshakePreferencesEvents import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState -import io.element.android.features.rageshake.api.rageshake.RageShake -import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore +import io.element.android.features.rageshake.impl.rageshake.RageShake +import io.element.android.features.rageshake.impl.rageshake.RageshakeDataStore import io.element.android.libraries.di.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -28,6 +31,7 @@ import javax.inject.Inject class DefaultRageshakePreferencesPresenter @Inject constructor( private val rageshake: RageShake, private val rageshakeDataStore: RageshakeDataStore, + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, ) : RageshakePreferencesPresenter { @Composable override fun present(): RageshakePreferencesState { @@ -35,13 +39,14 @@ class DefaultRageshakePreferencesPresenter @Inject constructor( val isSupported: MutableState = rememberSaveable { mutableStateOf(rageshake.isAvailable()) } - val isEnabled = rageshakeDataStore - .isEnabled() - .collectAsState(initial = false) + val isFeatureAvailable = remember { rageshakeFeatureAvailability.isAvailable() } + val isEnabled by remember { + rageshakeDataStore.isEnabled() + }.collectAsState(initial = false) - val sensitivity = rageshakeDataStore - .sensitivity() - .collectAsState(initial = 0f) + val sensitivity by remember { + rageshakeDataStore.sensitivity() + }.collectAsState(initial = 0f) fun handleEvents(event: RageshakePreferencesEvents) { when (event) { @@ -51,9 +56,10 @@ class DefaultRageshakePreferencesPresenter @Inject constructor( } return RageshakePreferencesState( - isEnabled = isEnabled.value, + isFeatureEnabled = isFeatureAvailable, + isEnabled = isEnabled, isSupported = isSupported.value, - sensitivity = sensitivity.value, + sensitivity = sensitivity, eventSink = ::handleEvents ) } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/DefaultRageShake.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/DefaultRageShake.kt index 21c5fffdb72..651b71c0796 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/DefaultRageShake.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/DefaultRageShake.kt @@ -13,7 +13,6 @@ import android.hardware.SensorManager import androidx.core.content.getSystemService import com.squareup.anvil.annotations.ContributesBinding import com.squareup.seismic.ShakeDetector -import io.element.android.features.rageshake.api.rageshake.RageShake import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt index f9379de16d7..9d7171b8a0f 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt @@ -15,7 +15,6 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageShake.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageShake.kt similarity index 91% rename from features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageShake.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageShake.kt index 548d11a41dd..d75d5e56660 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageShake.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageShake.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.api.rageshake +package io.element.android.features.rageshake.impl.rageshake interface RageShake { /** diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageshakeDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageshakeDataStore.kt similarity index 88% rename from features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageshakeDataStore.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageshakeDataStore.kt index a59c3670d88..f13419bfb21 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageshakeDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageshakeDataStore.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.api.rageshake +package io.element.android.features.rageshake.impl.rageshake import kotlinx.coroutines.flow.Flow 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 4ec8500417c..b9ed1c7778b 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 @@ -13,10 +13,10 @@ import androidx.core.net.toFile import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.RageshakeConfig -import io.element.android.features.rageshake.api.crash.CrashDataStore import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporterListener -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder +import io.element.android.features.rageshake.impl.crash.CrashDataStore +import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder import io.element.android.libraries.androidutils.file.compressFile import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -308,7 +308,7 @@ class DefaultBugReporter @Inject constructor( */ private fun getLogFiles(): List { return tryOrNull( - onError = { Timber.e(it, "## getLogFiles() failed") } + onException = { Timber.e(it, "## getLogFiles() failed") } ) { val logDirectory = logDirectory() logDirectory.listFiles()?.toList() diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/DefaultScreenshotHolder.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/DefaultScreenshotHolder.kt index 270c5628b36..dd3674ab260 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/DefaultScreenshotHolder.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/DefaultScreenshotHolder.kt @@ -11,7 +11,6 @@ import android.content.Context import android.graphics.Bitmap import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder import io.element.android.libraries.androidutils.bitmap.writeBitmap import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.di.AppScope diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/screenshot/ScreenshotHolder.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/ScreenshotHolder.kt similarity index 84% rename from features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/screenshot/ScreenshotHolder.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/ScreenshotHolder.kt index 9a7e64da596..c746a573d31 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/screenshot/ScreenshotHolder.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/ScreenshotHolder.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.api.screenshot +package io.element.android.features.rageshake.impl.screenshot import android.graphics.Bitmap diff --git a/features/rageshake/impl/src/main/res/values-cy/translations.xml b/features/rageshake/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..123c23ef7f4 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,18 @@ + + + "Atodwch lun sgrin" + "Gallwch gysylltu â mi os oes gennych unrhyw gwestiynau dilynol." + "Cysylltwch â mi" + "Golygu\'r llun sgrin" + "Disgrifiwch y broblem. Beth wnaethoch chi? Beth oeddech chi\'n disgwyl i ddigwydd? Beth ddigwyddodd mewn gwirionedd. Ewch i gymaint o fanylion ag y gallwch." + "Disgrifiwch y broblem…" + "Os yn bosibl, ysgrifennwch y disgrifiad yn Saesneg." + "Mae\'r disgrifiad yn rhy fyr, rhowch fwy o fanylion am yr hyn ddigwyddodd. Diolch!" + "Anfonwch logiau chwalu" + "Caniatáu logiau" + "Mae eich logiau\'n rhy fawr felly nid oes modd eu cynnwys yn yr adroddiad hwn, anfonwch nhw atom mewn ffordd arall." + "Anfon luniau sgrin" + "Bydd cofnodion yn cael eu cynnwys gyda\'ch neges i wneud yn siŵr bod popeth yn gweithio\'n iawn. I anfon eich neges heb logiau, diffoddwch y gosodiad hwn." + "Chwalodd %1$s y tro diwethaf iddo gael ei ddefnyddio. Hoffech chi rannu adroddiad gwall gyda ni?" + "Gweld logiau" + diff --git a/features/rageshake/impl/src/main/res/values-da/translations.xml b/features/rageshake/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..47ee436e8b4 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,18 @@ + + + "Vedhæft skærmbillede" + "Du kan kontakte mig, hvis du har opfølgende spørgsmål." + "Kontakt mig" + "Rediger skærmbillede" + "Beskriv venligst problemet: Hvad gjorde du? Hvad forventede du, at der skulle ske? Hvad skete der faktisk? - Beskriv det med så mange detaljer som du kan." + "Beskriv problemet…" + "Hvis det er muligt, må du meget gerne lave beskrivelsen på engelsk." + "Beskrivelsen er for kort, giv venligst flere detaljer om, hvad der skete. Tak!" + "Send nedbrudslogfiler" + "Tillad logfiler" + "Dine logfiler er for store, så de kan ikke medtages i denne rapport, send dem venligst til os på en anden måde." + "Send skærmbillede" + "Logfiler vil blive inkluderet i din besked for at sikre, at alt fungerer korrekt. Hvis du vil sende din besked uden logfiler, skal du deaktivere denne indstilling." + "%1$s crashede sidste gang den blev brugt. Vil du dele en ulykkesrapport med os?" + "Se logfiler" + diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml index ed85e17f85c..ac26cd075c5 100644 --- a/features/rageshake/impl/src/main/res/values-de/translations.xml +++ b/features/rageshake/impl/src/main/res/values-de/translations.xml @@ -4,14 +4,15 @@ "Sie können mich kontaktieren, wenn Sie weitere Fragen haben." "Kontaktieren Sie mich" "Bildschirmfoto bearbeiten" - "Bitte beschreibe den Fehler. Was hast du getan? Was hast du erwartet, was passiert? Was ist tatsächlich passiert? Bitte gehe so detailliert wie möglich vor." + "Bitte beschreiben Sie das Problem. Was haben Sie getan? Was haben Sie erwartet, was passiert? Was ist tatsächlich passiert? Bitte geben Sie so viele Details wie möglich an." "Beschreibe den Fehler…" "Wenn möglich, verfasse die Beschreibung bitte auf Englisch." "Die Beschreibung ist zu kurz. Bitte geben Sie weitere Informationen darüber an, was passiert ist." "Absturzprotokolle senden" - "Protokolle zulassen" + "Logdateien mitsenden" + "Deine Logs sind zu groß und können daher nicht in diesen Bericht aufgenommen werden. Bitte sende sie uns auf einem anderen Weg." "Bildschirmfoto senden" "Die Protokolle werden deiner Nachricht beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um deine Nachricht ohne Protokolle zu senden, deaktiviere diese Einstellung." - "%1$s ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?" + "%1$s ist bei der letzten Nutzung abgestürzt. Möchten Sie einen Absturzbericht mit uns teilen?" "Logs ansehen" diff --git a/features/rageshake/impl/src/main/res/values-es/translations.xml b/features/rageshake/impl/src/main/res/values-es/translations.xml index 7398dbc6de0..cdeb467cfa0 100644 --- a/features/rageshake/impl/src/main/res/values-es/translations.xml +++ b/features/rageshake/impl/src/main/res/values-es/translations.xml @@ -6,12 +6,12 @@ "Editar captura de pantalla" "Describe el problema. ¿Qué has hecho? ¿Qué esperabas que ocurriera? ¿Qué ocurrió realmente? Por favor, detállalo todo lo que puedas." "Describe el problema…" - "Si es posible, escriba la descripción en inglés." - "La descripción es demasiado corta. Proporcione más detalles sobre lo sucedido. ¡Gracias!" + "Si es posible, escribe la descripción en inglés." + "La descripción es demasiado corta. Proporciona más detalles sobre lo sucedido. ¡Gracias!" "Enviar registros de fallos" "Permitir registros" "Enviar captura de pantalla" - "Los registros se incluirán con su mensaje para asegurarse de que todo funciona correctamente. Para enviar tu mensaje sin registros, desactiva esta configuración." + "Los registros se incluirán con tu mensaje para asegurarse de que todo funciona correctamente. Para enviar tu mensaje sin registros, desactiva esta opción." "%1$s se cerró inesperadamente la última vez que se lo usaste. ¿Quieres compartir un informe de error con nosotros?" "Ver los registros" diff --git a/features/rageshake/impl/src/main/res/values-et/translations.xml b/features/rageshake/impl/src/main/res/values-et/translations.xml index 77144c6c43a..64e0f9b3078 100644 --- a/features/rageshake/impl/src/main/res/values-et/translations.xml +++ b/features/rageshake/impl/src/main/res/values-et/translations.xml @@ -10,6 +10,7 @@ "Kirjeldus on liiga lühike. Palun jaga täpsemat teavet selle kohta, mis juhtus. Tänud juba ette!" "Saada krahhilogid" "Luba logide saatmine" + "Sinu logid on väga mahukad ja neid ei saa siia lisada. Palun saada logid meile mõnel muul viisil." "Saada ekraanitõmmis" "Tõhusama veaotsingu nimel lisame sinu veateatele logid. Kui sa seda ei soovi, siis lülita antud valik välja." "%1$s jooksis kokku viimati, kui seda kasutasid. Kas tahaksid selle kohta meile veateate saata?" diff --git a/features/rageshake/impl/src/main/res/values-eu/translations.xml b/features/rageshake/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..1ba3ba9a6f1 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,16 @@ + + + "Erantsi pantaila-argazkia" + "Galderarik baduzu, nirekin jar zaitezke harremanetan." + "Jarri nirekin harremanetan" + "Editatu pantaila-argazkia" + "Deskribatu arazoa. Zer egin duzu? Zer espero zenuen gertatzea? Benetan gertatu dena. Eman ahalik eta xehetasun gehien." + "Deskribatu arazoa…" + "Ahal izanez gero, idatzi deskribapena ingelesez." + "Deskribapena laburregia da; eman gertatutakoari buruzko xehetasun gehiago. Eskerrik asko!" + "Bidali kraskaduraren erregistroak" + "Baimendu erregistroak" + "Bidali pantaila-argazkia" + "%1$s kraskatu zen azkenekoz erabili zenean. Gurekin partekatu nahi al duzu kraskatzearen txostena?" + "Ikusi erregistroak" + 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 c037a38792f..020ca83ecba 100644 --- a/features/rageshake/impl/src/main/res/values-fa/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fa/translations.xml @@ -4,10 +4,13 @@ "اگر پرسش دیگری دارید، می‌توانید با من در تماس باشید." "تماس با من" "ویرایش نماگرفت" + "لطفا مشکل را توصیف کنید. چيکار کردي؟ انتظار داشتید چه اتفاقی بیفتد؟ واقعا چه اتفاقی افتاد لطفا تا جایی که می توانید جزئیات را وارد کنید." "شرح مشکل…" "ترجیحاً توضیحات را به زبان انگلیسی بنویسید." "ارسال رخدادنگارهای خطا" "اجازه به گزارش‌ها" "ارسال تصویر صفحه" + "گزارش ها در پیام شما گنجانده می شوند تا مطمئن شوید همه چیز به درستی کار می کند. برای ارسال پیام بدون گزارش، این تنظیم را خاموش کنید." + "%1$sآخرین باری که استفاده شد، از کار افتاد. آیا مایلید گزارش خرابی را با ما به اشتراک بگذارید؟" "دیدن گزارش‌ها" diff --git a/features/rageshake/impl/src/main/res/values-fr/translations.xml b/features/rageshake/impl/src/main/res/values-fr/translations.xml index ed576f74bee..0d67e42db04 100644 --- a/features/rageshake/impl/src/main/res/values-fr/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fr/translations.xml @@ -10,6 +10,7 @@ "La description est trop courte, veuillez fournir plus de détails sur ce qui s’est passé. Merci !" "Envoyer des journaux d’incident" "Autoriser à inclure les journaux techniques" + "Vos fichiers de log sont trop volumineux et ne peuvent donc pas être inclus dans ce rapport, veuillez nous les envoyer par un autre moyen." "Envoyer une capture d’écran" "Pour vérifier que les choses fonctionnent comme prévu, des journaux techniques seront envoyés avec votre message. Pour ne pas envoyer ces journaux, désactivez ce paramètre." "%1$s s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?" diff --git a/features/rageshake/impl/src/main/res/values-hu/translations.xml b/features/rageshake/impl/src/main/res/values-hu/translations.xml index 1ac8c4c2857..f6fcbaa1c94 100644 --- a/features/rageshake/impl/src/main/res/values-hu/translations.xml +++ b/features/rageshake/impl/src/main/res/values-hu/translations.xml @@ -10,6 +10,7 @@ "A leírás túl rövid, adjon meg további részleteket a történtekről. Köszönjük!" "Összeomlásnaplók küldése" "Naplók engedélyezése" + "A naplófájlok túl nagyok, ezért nem szerepelhetnek ebben a jelentésben. Más módon küldje el őket." "Képernyőkép küldése" "A naplók szerepelni fognak az üzenetben, hogy megbizonyosodhassunk arról, hogy minden megfelelően működik-e. Ha naplók nélkül szeretné elküldeni az üzenetet, akkor kapcsolja ki ezt a beállítást." "Az %1$s összeomlott a legutóbbi használata óta. Megosztja velünk az összeomlás-jelentést?" diff --git a/features/rageshake/impl/src/main/res/values-in/translations.xml b/features/rageshake/impl/src/main/res/values-in/translations.xml index 75efeb28f63..dd80d0aa019 100644 --- a/features/rageshake/impl/src/main/res/values-in/translations.xml +++ b/features/rageshake/impl/src/main/res/values-in/translations.xml @@ -10,6 +10,7 @@ "Deskripsinya terlalu pendek, silakan menyediakan detail tambahan tentang apa yang terjadi. Terima kasih!" "Kirim log kerusakan" "Izinkan log" + "Log Anda terlalu besar sehingga tidak dapat dimasukkan dalam laporan ini, kirimkan kepada kami dengan cara lain." "Kirim tangkapan layar" "Log akan disertakan dengan pesan Anda untuk memastikan bahwa semuanya berfungsi dengan baik. Untuk mengirimkan pesan Anda tanpa log, matikan pengaturan ini." "%1$s mengalami kemogokan saat terakhir kali digunakan. Apakah Anda ingin berbagi laporan kerusakan dengan kami?" diff --git a/features/rageshake/impl/src/main/res/values-it/translations.xml b/features/rageshake/impl/src/main/res/values-it/translations.xml index bbc5da3ce90..87098817094 100644 --- a/features/rageshake/impl/src/main/res/values-it/translations.xml +++ b/features/rageshake/impl/src/main/res/values-it/translations.xml @@ -10,6 +10,7 @@ "La descrizione è troppo breve, ti preghiamo di fornire maggiori dettagli sull\'accaduto. Grazie!" "Invia i log degli arresti anomali" "Consenti i log" + "I tuoi log sono troppo grandi, quindi non possono essere inclusi in questo rapporto, inviaceli in un altro modo." "Invia istantanea schermo" "Per verificare che le cose funzionino come previsto, i log verranno inviati con il tuo messaggio. Per inviare solo il tuo messaggio, disattiva questa impostazione." "%1$s si è chiuso inaspettatamente l\'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull\'arresto anomalo?" diff --git a/features/rageshake/impl/src/main/res/values-ka/translations.xml b/features/rageshake/impl/src/main/res/values-ka/translations.xml index 6f0afb86b18..3fd04f0dd23 100644 --- a/features/rageshake/impl/src/main/res/values-ka/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ka/translations.xml @@ -7,9 +7,11 @@ "გთხოვთ, აღწეროთ პრობლემა. რა გააკეთე? რა შედეგს ელოდებოდით? რა მოხდა სინამდვილეში? გთხოვთ, ყველაფერი დაწვრილებით თქვათ." "აღწერეთ პრობლემა…" "თუ შესაძლებელია, გთხოვთ, დაწეროთ აღწერა ინგლისურ ენაზე." + "აღწერა ძალიან მოკლეა, გთხოვთ მოგვაწოდოთ მეტი დეტალი მომხდარის შესახებ. მადლობა!" "გაუმართაობის ჟურნალის გაგზავნა" "ჟურნალების დაშვება" "ეკრანის ანაბეჭდის გაგზავნა" "ჟურნალები თქვენს შეტყობინებაში შევა იმაში დასარწმუნებლად, რომ ყველაფერი სწორად მუშაობს. ჟურნალების გარეშე გასაგზავნად გათიშეთ ეს პარამეტრი." "%1$s ავარიულად გაითიშა ბოლოს გამოიყენებისას. გსურთ, გამოგვიგზავნოთ ავარიული გათიშვის ჟურნალი?" + "ჟურნალის ნახვა" diff --git a/features/rageshake/impl/src/main/res/values-lt/translations.xml b/features/rageshake/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..9a3889bbbce --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,14 @@ + + + "Pridėti ekrano nuotrauką" + "Jei turite papildomų klausimų, galite susisiekti su manimi." + "Redaguoti ekrano nuotrauką" + "Apibūdinkite problemą. Ką padarėte? Ko tikėjotės? Kas iš tikrųjų įvyko? Pateikite kuo daugiau detalių." + "Apibūdinkite problemą…" + "Jei įmanoma, aprašymą parašykite anglų kalba." + "Siųsti gedimų žurnalus" + "Leisti žurnalus" + "Siųsti ekrano nuotrauką" + "Prie žinutės bus pridėti žurnalai, kad įsitikintumėme, jog viskas veikia tinkamai. Jei norite išsiųsti savo žinutę be žurnalų, išjunkite šį nustatymą." + "%1$s nulūžo paskutinį kartą, kai buvo naudojama. Ar norėtumėte su mumis pasidalyti strigčių ataskaita?" + diff --git a/features/rageshake/impl/src/main/res/values-nb/translations.xml b/features/rageshake/impl/src/main/res/values-nb/translations.xml index 75b0cdf69c5..277effef920 100644 --- a/features/rageshake/impl/src/main/res/values-nb/translations.xml +++ b/features/rageshake/impl/src/main/res/values-nb/translations.xml @@ -2,13 +2,17 @@ "Legg ved skjermbilde" "Du kan kontakte meg hvis du har noen oppfølgingsspørsmål." + "Kontakt meg" "Rediger skjermbilde" "Vennligst beskriv problemet. Hva har du gjort? Hva forventet du skulle skje? Hva som faktisk skjedde. Vær så detaljert som mulig." "Beskriv problemet…" "Hvis mulig, vennligst skriv beskrivelsen på engelsk." + "Beskrivelsen er for kort, vennligst gi flere detaljer om hva som skjedde. Takk skal du ha!" "Send krasjlogger" "Tillat logger" + "Loggene dine er for store og kan derfor ikke inkluderes i denne rapporten. Vennligst send dem til oss på en annen måte." "Send skjermbilde" "Logger vil bli inkludert i meldingen din, for å sikre at alt fungerer som det skal. For å sende meldingen uten logger, slå av denne innstillingen." "%1$s krasjet sist gang den ble brukt. Vil du dele en krasjrapport med oss?" + "Vis logger" diff --git a/features/rageshake/impl/src/main/res/values-pt-rBR/translations.xml b/features/rageshake/impl/src/main/res/values-pt-rBR/translations.xml index 885c9a6fe6f..c679bbd1c7a 100644 --- a/features/rageshake/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/rageshake/impl/src/main/res/values-pt-rBR/translations.xml @@ -7,8 +7,10 @@ "Descreva o problema. O que você fez? O que você esperava que acontecesse? O que realmente aconteceu? Por favor, forneça o máximo de detalhes possível." "Descreva o problema…" "Se possível, escreva a descrição em inglês." + "A descrição é muito curta, por favor, forneça mais detalhes sobre o que aconteceu. Obrigado!" "Enviar registros de falhas" "Permitir registros" + "Seus registros são grandes demais portanto não podem serem inclusos no relatório, por favor envie-os para a gente de outra maneira." "Enviar captura de tela" "Os registros serão incluídos com sua mensagem para garantir que tudo esteja funcionando corretamente. Para enviar sua mensagem sem registros, desative essa configuração." "%1$s fechou inesperadamente na última vez que foi usado. Gostaria de compartilhar um relatório de falhas conosco?" diff --git a/features/rageshake/impl/src/main/res/values-pt/translations.xml b/features/rageshake/impl/src/main/res/values-pt/translations.xml index 59a066a16e2..d2e637db560 100644 --- a/features/rageshake/impl/src/main/res/values-pt/translations.xml +++ b/features/rageshake/impl/src/main/res/values-pt/translations.xml @@ -10,6 +10,7 @@ "A descrição é demasiado curta. Por favor dá mais detalhes sobre o que aconteceu. Obrigado!" "Enviar registos de falha" "Permitir registos" + "Os teus registos são excessivamente grandes e não podem ser incluídos neste relatório, por favor envia-no-los de outra forma." "Enviar captura de ecrã" "Os registos serão incluídos na tua mensagem para garantir que tudo está a funcionar corretamente. Para enviares a tua mensagem sem registos, desativa esta definição." "A %1$s teve uma falha da última vez que foi utilizada. Gostarias de partilhar um relatório de acidente connosco?" diff --git a/features/rageshake/impl/src/main/res/values-sk/translations.xml b/features/rageshake/impl/src/main/res/values-sk/translations.xml index f9152d844c2..c46181ad29a 100644 --- a/features/rageshake/impl/src/main/res/values-sk/translations.xml +++ b/features/rageshake/impl/src/main/res/values-sk/translations.xml @@ -10,6 +10,7 @@ "Popis je príliš krátky, uveďte viac podrobností o tom, čo sa stalo. Ďakujeme!" "Odoslať záznamy o zlyhaní" "Povoliť záznamy" + "Vaše záznamy sú príliš veľké, takže ich nemožno zahrnúť do tejto správy, pošlite nám ich prosím iným spôsobom." "Odoslať snímku obrazovky" "K vašej správe budú priložené záznamy o chybe, aby sme sa uistili, že všetko funguje správne. Ak chcete odoslať správu bez záznamov o chybe, vypnite toto nastavenie." "%1$s zlyhal pri poslednom použití. Chcete zdieľať správu o páde s našim tímom?" diff --git a/features/rageshake/impl/src/main/res/values-tr/translations.xml b/features/rageshake/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..d558126f214 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,17 @@ + + + "Ekran görüntüsü ekle" + "Herhangi bir sorunuz olursa benimle iletişime geçebilirsiniz." + "Bana ulaş" + "Ekran görüntüsünü düzenle" + "Lütfen sorunu açıklayın. Ne yaptınız? Ne olmasını bekliyordunuz? Gerçekten ne oldu. Lütfen olabildiğince ayrıntılı bilgi verin." + "Sorunu açıklayın…" + "Mümkünse, lütfen açıklamayı İngilizce olarak yazın." + "Açıklama çok kısa, lütfen ne olduğu hakkında daha fazla ayrıntı verin. Teşekkürler!" + "Hata günlüklerini gönder" + "Günlüklere izin ver" + "Ekran görüntüsü gönder" + "Her şeyin düzgün çalıştığından emin olmak için günlükler mesajınıza dahil edilecektir. Mesajınızı kayıt tutmadan göndermek için bu ayarı kapatın." + "%1$s son kullanıldığında çöktü. Bizimle bir çökme raporu paylaşmak ister misiniz?" + "Günlükleri görüntüle" + 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 30465c4ca48..39991335376 100644 --- a/features/rageshake/impl/src/main/res/values-uk/translations.xml +++ b/features/rageshake/impl/src/main/res/values-uk/translations.xml @@ -10,6 +10,7 @@ "Опис закороткий, будь ласка, надайте докладнішу інформацію про те, що сталося. Дякуємо!" "Надіслати журнали збоїв" "Дозволити журнали" + "Ваші журнали надмірно великі, тому їх не можна включити в цей звіт, будь ласка, надішліть їх нам іншим способом." "Надіслати знімок екрана" "Журнали будуть додані до вашого повідомлення, щоб переконатися, що все працює належним чином. Щоб надіслати повідомлення без журналів, вимкніть це налаштування." "Стався збій %1$s під час останнього користування. Хочете поділитися з нами звітом про збій?" diff --git a/features/rageshake/impl/src/main/res/values-ur/translations.xml b/features/rageshake/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..695c4ec64fb --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,17 @@ + + + "پردۂ عکس منسلک کریں" + "اگر آپ کے پاس کوئی متابعہ سوالات ہیں، تو آپ مجھ سے رابطہ کر سکتے ہیں۔" + "مجھ سے رابطہ کریں" + "پردۂ عکس میں ترمیم کریں" + "براہ کرم مسئلہ بیان کریں۔ آپ نے کیا کیا؟ آپ کو کیا ہونے کی امید تھی؟ اصل میں کیا ہوا۔ براہ کرم جتنا ہو سکے اس کی تفصیل میں جائیں۔" + "مسئلہ بیان کریں…" + "اگر ممکن ہو تو، برائے مہربانی بیان انگریزی میں لکھیں۔" + "تفصیل بہت مختصر ہے، براہ کرم مزید تفصیلات فراہم کریں کہ کیا ہوا۔ شکریہ!" + "ٹکر کے نوشتے بھیجیں" + "نوشتہجات کی اجازت دیں۔" + "پردۂ عکس بھیجیں" + "نوشتہ جات آپکے پیغام کیساتھ شامل کیے جائینگے تاکہ یقینی بنایا جا سکے کہ سب کچھ ٹھیک سے کام کر رہا ہے۔ بغیر نوشتہ جات کے اپنا پیغام بھیجنے کے لیے، اس ترتیب کو بند کریں۔" + "%1$sآخری بار استعمال ہونے پر ٹکرا گیا۔ کیا آپ ہمارے ساتھ ٹکر کی گزارش (رپورٹ) کا اشتراک کرنا چاہیں گے؟" + "نوشتہ جات ملحوظ کریں" + diff --git a/features/rageshake/impl/src/main/res/values/localazy.xml b/features/rageshake/impl/src/main/res/values/localazy.xml index 2d377c1cc74..27939684c8f 100644 --- a/features/rageshake/impl/src/main/res/values/localazy.xml +++ b/features/rageshake/impl/src/main/res/values/localazy.xml @@ -10,6 +10,7 @@ "The description is too short, please provide more details about what happened. Thanks!" "Send crash logs" "Allow logs" + "Your logs are excessively large so cannot be included in this report, please send them to us another way." "Send screenshot" "Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting." "%1$s crashed the last time it was used. Would you like to share a crash report with us?" diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt index cae6f6e5009..027e2fb38c4 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt @@ -11,13 +11,13 @@ 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.rageshake.api.crash.CrashDataStore import io.element.android.features.rageshake.api.reporter.BugReporter -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder -import io.element.android.features.rageshake.test.crash.A_CRASH_DATA -import io.element.android.features.rageshake.test.crash.FakeCrashDataStore -import io.element.android.features.rageshake.test.screenshot.A_SCREENSHOT_URI -import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder +import io.element.android.features.rageshake.impl.crash.A_CRASH_DATA +import io.element.android.features.rageshake.impl.crash.CrashDataStore +import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore +import io.element.android.features.rageshake.impl.screenshot.A_SCREENSHOT_URI +import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder +import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.A_FAILURE_REASON import io.element.android.tests.testutils.WarmUpRule diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/crash/FakeCrashDataStore.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt similarity index 88% rename from features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/crash/FakeCrashDataStore.kt rename to features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt index 145e582decb..1a89a52bf70 100644 --- a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/crash/FakeCrashDataStore.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.test.crash +package io.element.android.features.rageshake.impl.crash -import io.element.android.features.rageshake.api.crash.CrashDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt index a5fa6671d7c..92dfbb02a2d 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt @@ -12,9 +12,9 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.rageshake.api.crash.CrashDetectionEvents +import io.element.android.features.rageshake.impl.crash.A_CRASH_DATA import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter -import io.element.android.features.rageshake.test.crash.A_CRASH_DATA -import io.element.android.features.rageshake.test.crash.FakeCrashDataStore +import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.WarmUpRule @@ -51,6 +51,20 @@ class CrashDetectionPresenterTest { } } + @Test + fun `present - initial state crash is ignored if the feature is not available`() = runTest { + val presenter = createPresenter( + FakeCrashDataStore(appHasCrashed = true), + isFeatureAvailable = false, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.crashDetected).isFalse() + } + } + @Test fun `present - reset app has crashed`() = runTest { val presenter = createPresenter( @@ -86,8 +100,10 @@ class CrashDetectionPresenterTest { private fun createPresenter( crashDataStore: FakeCrashDataStore = FakeCrashDataStore(), buildMeta: BuildMeta = aBuildMeta(), + isFeatureAvailable: Boolean = true, ) = DefaultCrashDetectionPresenter( buildMeta = buildMeta, crashDataStore = crashDataStore, + rageshakeFeatureAvailability = { isFeatureAvailable }, ) } diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt index 5aa9679b8cd..6f433a78d78 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt @@ -15,9 +15,9 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents import io.element.android.features.rageshake.api.screenshot.ImageResult import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter -import io.element.android.features.rageshake.test.rageshake.FakeRageShake -import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore -import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder +import io.element.android.features.rageshake.impl.rageshake.FakeRageShake +import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore +import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.tests.testutils.WarmUpRule import io.mockk.mockk @@ -52,6 +52,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -76,6 +77,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -101,6 +103,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -135,6 +138,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -169,6 +173,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt index 796545c8596..ba683454404 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt @@ -12,9 +12,9 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.rageshake.api.preferences.RageshakePreferencesEvents -import io.element.android.features.rageshake.test.rageshake.A_SENSITIVITY -import io.element.android.features.rageshake.test.rageshake.FakeRageShake -import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore +import io.element.android.features.rageshake.impl.rageshake.A_SENSITIVITY +import io.element.android.features.rageshake.impl.rageshake.FakeRageShake +import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -28,7 +28,8 @@ class RageshakePreferencesPresenterTest { fun `present - initial state available`() = runTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = true), - FakeRageshakeDataStore(isEnabled = true) + FakeRageshakeDataStore(isEnabled = true), + rageshakeFeatureAvailability = { true }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -44,7 +45,8 @@ class RageshakePreferencesPresenterTest { fun `present - initial state not available`() = runTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = false), - FakeRageshakeDataStore(isEnabled = true) + FakeRageshakeDataStore(isEnabled = true), + rageshakeFeatureAvailability = { true }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -60,7 +62,8 @@ class RageshakePreferencesPresenterTest { fun `present - enable and disable`() = runTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = true), - FakeRageshakeDataStore(isEnabled = true) + FakeRageshakeDataStore(isEnabled = true), + rageshakeFeatureAvailability = { true }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -79,7 +82,8 @@ class RageshakePreferencesPresenterTest { fun `present - set sensitivity`() = runTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = true), - FakeRageshakeDataStore(isEnabled = true) + FakeRageshakeDataStore(isEnabled = true), + rageshakeFeatureAvailability = { true }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageShake.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageShake.kt similarity index 84% rename from features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageShake.kt rename to features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageShake.kt index 1e3ac967702..b35bde295ac 100644 --- a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageShake.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageShake.kt @@ -5,9 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.test.rageshake - -import io.element.android.features.rageshake.api.rageshake.RageShake +package io.element.android.features.rageshake.impl.rageshake class FakeRageShake( private var isAvailableValue: Boolean = true diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageshakeDataStore.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageshakeDataStore.kt similarity index 87% rename from features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageshakeDataStore.kt rename to features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageshakeDataStore.kt index b8fcc22d23d..ec67ed13c01 100644 --- a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageshakeDataStore.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageshakeDataStore.kt @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.test.rageshake +package io.element.android.features.rageshake.impl.rageshake -import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt index b38c52e6ef2..e73c7863b41 100755 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt @@ -8,9 +8,10 @@ package io.element.android.features.rageshake.impl.reporter import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.RageshakeConfig import io.element.android.features.rageshake.api.reporter.BugReporterListener -import io.element.android.features.rageshake.test.crash.FakeCrashDataStore -import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder +import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore +import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeSdkMetadata @@ -138,7 +139,7 @@ class DefaultBugReporterTest { val foundValues = collectValuesFromFormData(request) - assertThat(foundValues["app"]).isEqualTo("element-x-android") + assertThat(foundValues["app"]).isEqualTo(RageshakeConfig.BUG_REPORT_APP_NAME) assertThat(foundValues["can_contact"]).isEqualTo("true") assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH") assertThat(foundValues["sdk_sha"]).isEqualTo("123456789") diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt index 415440b164f..71563892dc8 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt @@ -16,7 +16,9 @@ class DefaultBugReporterUrlProviderTest { @Test fun `test DefaultBugReporterUrlProvider`() { val sut = DefaultBugReporterUrlProvider() - val result = sut.provide() - assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl()) + if (RageshakeConfig.BUG_REPORT_URL.isNotEmpty()) { + val result = sut.provide() + assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl()) + } } } diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/screenshot/FakeScreenshotHolder.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/screenshot/FakeScreenshotHolder.kt similarity index 78% rename from features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/screenshot/FakeScreenshotHolder.kt rename to features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/screenshot/FakeScreenshotHolder.kt index 3f7d83f1c2a..8e37da19101 100644 --- a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/screenshot/FakeScreenshotHolder.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/screenshot/FakeScreenshotHolder.kt @@ -5,10 +5,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.test.screenshot +package io.element.android.features.rageshake.impl.screenshot import android.graphics.Bitmap -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder const val A_SCREENSHOT_URI = "file://content/uri" diff --git a/features/reportroom/api/build.gradle.kts b/features/reportroom/api/build.gradle.kts new file mode 100644 index 00000000000..f5499b7a742 --- /dev/null +++ b/features/reportroom/api/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.reportroom.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) +} diff --git a/features/reportroom/api/src/main/kotlin/io/element/android/features/reportroom/api/ReportRoomEntryPoint.kt b/features/reportroom/api/src/main/kotlin/io/element/android/features/reportroom/api/ReportRoomEntryPoint.kt new file mode 100644 index 00000000000..7f531fb9e33 --- /dev/null +++ b/features/reportroom/api/src/main/kotlin/io/element/android/features/reportroom/api/ReportRoomEntryPoint.kt @@ -0,0 +1,17 @@ +/* + * 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.reportroom.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId + +interface ReportRoomEntryPoint : FeatureEntryPoint { + fun createNode(parentNode: Node, buildContext: BuildContext, roomId: RoomId): Node +} diff --git a/features/reportroom/impl/build.gradle.kts b/features/reportroom/impl/build.gradle.kts new file mode 100644 index 00000000000..d60cc76f8d3 --- /dev/null +++ b/features/reportroom/impl/build.gradle.kts @@ -0,0 +1,45 @@ +/* + * 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. + */ + +import extension.setupAnvil + +plugins { + id("io.element.android-compose-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.reportroom.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +setupAnvil() + +dependencies { + api(projects.features.reportroom.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) + testImplementation(libs.test.robolectric) +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPoint.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPoint.kt new file mode 100644 index 00000000000..d3131040d9b --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomEntryPoint.kt @@ -0,0 +1,24 @@ +/* + * 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.reportroom.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.reportroom.api.ReportRoomEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.RoomId +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultReportRoomEntryPoint @Inject constructor() : ReportRoomEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext, roomId: RoomId): Node { + return parentNode.createNode(buildContext, plugins = listOf(ReportRoomNode.Inputs(roomId))) + } +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoom.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoom.kt new file mode 100644 index 00000000000..55ccb25417c --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoom.kt @@ -0,0 +1,60 @@ +/* + * 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.reportroom.impl + +import com.squareup.anvil.annotations.ContributesBinding +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 javax.inject.Inject + +interface ReportRoom { + suspend operator fun invoke( + roomId: RoomId, + shouldReport: Boolean, + reason: String, + shouldLeave: Boolean, + ): Result + + sealed class Exception : kotlin.Exception() { + data object RoomNotFound : Exception() + data object LeftRoomFailed : Exception() + data object ReportRoomFailed : Exception() + } +} + +@ContributesBinding(SessionScope::class) +class DefaultReportRoom @Inject constructor( + private val client: MatrixClient, +) : ReportRoom { + override suspend operator fun invoke( + roomId: RoomId, + shouldReport: Boolean, + reason: String, + shouldLeave: Boolean + ): Result { + val room = client.getRoom(roomId) + ?: return Result.failure(ReportRoom.Exception.RoomNotFound) + + if (shouldReport) { + room + .reportRoom(reason.takeIf { it.isNotBlank() }) + .onFailure { + return Result.failure(ReportRoom.Exception.ReportRoomFailed) + } + } + if (shouldLeave) { + room + .leave() + .onFailure { + return Result.failure(ReportRoom.Exception.LeftRoomFailed) + } + } + return Result.success(Unit) + } +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt new file mode 100644 index 00000000000..e72f00e2a92 --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt @@ -0,0 +1,15 @@ +/* + * 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.reportroom.impl + +sealed interface ReportRoomEvents { + data class UpdateReason(val reason: String) : ReportRoomEvents + data object ToggleLeaveRoom : ReportRoomEvents + data object Report : ReportRoomEvents + data object ClearReportAction : ReportRoomEvents +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomNode.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomNode.kt new file mode 100644 index 00000000000..0c24d6db744 --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomNode.kt @@ -0,0 +1,43 @@ +/* + * 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.reportroom.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId + +@ContributesNode(SessionScope::class) +class ReportRoomNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: ReportRoomPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + data class Inputs(val roomId: RoomId) : NodeInputs + + private val roomId = inputs().roomId + private val presenter: ReportRoomPresenter = presenterFactory.create(roomId = roomId) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ReportRoomView( + state = state, + modifier = modifier, + onBackClick = ::navigateUp, + ) + } +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt new file mode 100644 index 00000000000..30ccb9e20c6 --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt @@ -0,0 +1,83 @@ +/* + * 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.reportroom.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +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.matrix.api.core.RoomId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class ReportRoomPresenter @AssistedInject constructor( + @Assisted private val roomId: RoomId, + private val reportRoom: ReportRoom, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(roomId: RoomId): ReportRoomPresenter + } + + @Composable + override fun present(): ReportRoomState { + var reason by rememberSaveable { mutableStateOf("") } + var leaveRoom by rememberSaveable { mutableStateOf(false) } + var reportAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: ReportRoomEvents) { + when (event) { + ReportRoomEvents.Report -> coroutineScope.reportRoom(reason, leaveRoom, reportAction) + ReportRoomEvents.ToggleLeaveRoom -> { + leaveRoom = !leaveRoom + } + is ReportRoomEvents.UpdateReason -> { + reason = event.reason + } + ReportRoomEvents.ClearReportAction -> { + reportAction.value = AsyncAction.Uninitialized + } + } + } + return ReportRoomState( + reason = reason, + leaveRoom = leaveRoom, + reportAction = reportAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.reportRoom( + reason: String, + shouldLeave: Boolean, + action: MutableState> + ) = launch { + val previousFailure = action.value as? AsyncAction.Failure + val shouldReport = previousFailure?.error !is ReportRoom.Exception.LeftRoomFailed + runUpdatingState(action) { + reportRoom( + roomId = roomId, + shouldReport = shouldReport, + reason = reason, + shouldLeave = shouldLeave + ) + } + } +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt new file mode 100644 index 00000000000..a04f24123bf --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt @@ -0,0 +1,19 @@ +/* + * 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.reportroom.impl + +import io.element.android.libraries.architecture.AsyncAction + +data class ReportRoomState( + val reason: String, + val leaveRoom: Boolean, + val reportAction: AsyncAction, + val eventSink: (ReportRoomEvents) -> Unit +) { + val canReport: Boolean = reason.isNotBlank() +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt new file mode 100644 index 00000000000..1e89a31adbb --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt @@ -0,0 +1,38 @@ +/* + * 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.reportroom.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction + +open class ReportRoomStateProvider : PreviewParameterProvider { + companion object { + private const val A_REPORT_ROOM_REASON = "Inappropriate content" + } + + override val values: Sequence + get() = sequenceOf( + aReportRoomState(), + aReportRoomState(reason = A_REPORT_ROOM_REASON), + aReportRoomState(leaveRoom = true), + aReportRoomState(reason = A_REPORT_ROOM_REASON, reportAction = AsyncAction.Loading), + aReportRoomState(reason = A_REPORT_ROOM_REASON, reportAction = AsyncAction.Failure(Exception("Failed to report"))), + ) +} + +fun aReportRoomState( + reason: String = "", + leaveRoom: Boolean = false, + reportAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (ReportRoomEvents) -> Unit = {} +) = ReportRoomState( + reason = reason, + leaveRoom = leaveRoom, + reportAction = reportAction, + eventSink = eventSink, +) diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt new file mode 100644 index 00000000000..5af0beefcd1 --- /dev/null +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt @@ -0,0 +1,147 @@ +/* + * 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.reportroom.impl + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +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.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.list.ListItemContent +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.ListItem +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReportRoomView( + state: ReportRoomState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + + val isReporting = state.reportAction is AsyncAction.Loading + AsyncActionView( + async = state.reportAction, + onSuccess = { onBackClick() }, + errorTitle = { failure -> + when (failure) { + is ReportRoom.Exception.LeftRoomFailed -> stringResource(R.string.screen_report_room_leave_failed_alert_title) + else -> stringResource(CommonStrings.dialog_title_error) + } + }, + errorMessage = { failure -> + when (failure) { + is ReportRoom.Exception.LeftRoomFailed -> stringResource(R.string.screen_report_room_leave_failed_alert_message) + else -> stringResource(CommonStrings.error_unknown) + } + }, + onRetry = { + state.eventSink(ReportRoomEvents.Report) + }, + onErrorDismiss = { state.eventSink(ReportRoomEvents.ClearReportAction) } + ) + + Scaffold( + topBar = { + TopAppBar( + titleStr = stringResource(R.string.screen_report_room_title), + navigationIcon = { + BackButton(onClick = onBackClick) + } + ) + }, + modifier = modifier + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .imePadding() + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(vertical = 16.dp) + ) { + TextField( + value = state.reason, + onValueChange = { state.eventSink(ReportRoomEvents.UpdateReason(it)) }, + placeholder = stringResource(R.string.screen_report_room_reason_placeholder), + minLines = 3, + enabled = !isReporting, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .heightIn(min = 90.dp), + supportingText = stringResource(R.string.screen_report_room_reason_footer), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + ListItem( + modifier = Modifier.padding(end = 8.dp), + headlineContent = { + Text(text = stringResource(CommonStrings.action_leave_room)) + }, + onClick = { + state.eventSink(ReportRoomEvents.ToggleLeaveRoom) + }, + trailingContent = ListItemContent.Switch(checked = state.leaveRoom) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + text = stringResource(CommonStrings.action_report), + enabled = state.canReport && !isReporting, + destructive = true, + showProgress = isReporting, + onClick = { + focusManager.clearFocus(force = true) + state.eventSink(ReportRoomEvents.Report) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun ReportRoomViewPreview( + @PreviewParameter(ReportRoomStateProvider::class) state: ReportRoomState +) = ElementPreview { + ReportRoomView( + state = state, + onBackClick = {}, + ) +} diff --git a/features/reportroom/impl/src/main/res/values-cs/translations.xml b/features/reportroom/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 00000000000..809cfb68a56 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,8 @@ + + + "Vaše hlášení bylo úspěšně odesláno, ale při pokusu o opuštění místnosti jsme narazili na problém. Zkuste to prosím znovu." + "Nelze opustit místnost" + "Nahlaste tuto místnost svému administrátorovi. Pokud jsou zprávy zašifrované, váš administrátor je nebude moci číst." + "Popište důvod…" + "Nahlásit místnost" + diff --git a/features/reportroom/impl/src/main/res/values-cy/translations.xml b/features/reportroom/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..89c44c2d0d1 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,8 @@ + + + "Cyflwynwyd eich adroddiad yn llwyddiannus, ond cododd problem wrth geisio gadael yr ystafell. Ceisiwch eto." + "Methu Gadael yr Ystafell" + "Adroddwch yr ystafell hon i\'ch gweinyddwr. Os yw\'r negeseuon wedi\'u hamgryptio, fydd eich gweinyddwr ddim yn gallu eu darllen." + "Disgrifiwch y rheswm…" + "Adrodd ar ystafell" + diff --git a/features/reportroom/impl/src/main/res/values-da/translations.xml b/features/reportroom/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..0f2a9b274c9 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,8 @@ + + + "Din anmeldelse blev indsendt med succes, men vi stødte på et problem, da vi forsøgte at forlade rummet. Prøv venligst igen." + "Ude af stand til at forlade rummet" + "Anmeld dette rum til din administrator. Hvis meddelelserne er krypteret, kan din administrator ikke læse dem." + "Beskriv årsagen til anmeldelsen…" + "Anmeld rummet" + diff --git a/features/reportroom/impl/src/main/res/values-de/translations.xml b/features/reportroom/impl/src/main/res/values-de/translations.xml new file mode 100644 index 00000000000..6d379818d64 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,8 @@ + + + "Ihr Bericht wurde erfolgreich übermittelt, aber beim Versuch, den Raum zu verlassen, ist ein Problem aufgetreten. Bitte versuchen Sie es erneut." + "Der Chatroom kann nicht verlassen werden" + "Melden Sie diesen Chatroom Ihrem Administrator. Wenn die Nachrichten verschlüsselt sind, kann Ihr Administrator sie nicht lesen." + "Beschreiben Sie den Grund…" + "Chatroom melden" + diff --git a/features/reportroom/impl/src/main/res/values-el/translations.xml b/features/reportroom/impl/src/main/res/values-el/translations.xml new file mode 100644 index 00000000000..274c6bc4c78 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-el/translations.xml @@ -0,0 +1,8 @@ + + + "Η αναφορά σας υποβλήθηκε με επιτυχία, αλλά αντιμετωπίσαμε ένα πρόβλημα κατά την προσπάθεια εξόδου από την αίθουσα. Παρακαλώ προσπαθήστε ξανά." + "Δεν είναι δυνατή η έξοδος από την αίθουσα" + "Αναφέρετε αυτήν την αίθουσα στον διαχειριστή σας. Εάν τα μηνύματα είναι κρυπτογραφημένα, ο διαχειριστής σας δεν θα μπορεί να τα διαβάσει." + "Περιγράψτε τον λόγο αναφοράς…" + "Αναφορά αίθουσας" + diff --git a/features/reportroom/impl/src/main/res/values-es/translations.xml b/features/reportroom/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..7391dc7ca34 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,8 @@ + + + "Tu denuncia se ha enviado correctamente, pero hemos encontrado un problema al intentar salir de la sala. Inténtalo de nuevo." + "No se pudo salir de la sala" + "Denuncia esta sala a tu administrador. Si los mensajes están cifrados, tu administrador no podrá leerlos." + "Describe el motivo de la denuncia…" + "Denunciar sala" + diff --git a/features/reportroom/impl/src/main/res/values-et/translations.xml b/features/reportroom/impl/src/main/res/values-et/translations.xml new file mode 100644 index 00000000000..1cc6e6b2774 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-et/translations.xml @@ -0,0 +1,8 @@ + + + "Jututoast haldajale teatamine õnnestus, kuid jututost lahkumisel tekkis viga. Palun proovi uuesti lahkuda." + "Pole võimalik lahkuda jututoast" + "Teata sellest jututoast süsteemi haldajale. Kui sõnumid on krüptitud, ei saa haldaja neid lugeda." + "Kirjelda põhjust…" + "Teata jututoast" + diff --git a/features/reportroom/impl/src/main/res/values-fa/translations.xml b/features/reportroom/impl/src/main/res/values-fa/translations.xml new file mode 100644 index 00000000000..9930e9ceac9 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,6 @@ + + + "ناتوان از ترک اتاق" + "شرح دلیل گزارش…" + "گزارش اتاق" + diff --git a/features/reportroom/impl/src/main/res/values-fi/translations.xml b/features/reportroom/impl/src/main/res/values-fi/translations.xml new file mode 100644 index 00000000000..cf23910f8b5 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-fi/translations.xml @@ -0,0 +1,8 @@ + + + "Ilmoituksesi lähetettiin onnistuneesti, mutta kohtasimme ongelman yrittäessämme poistua huoneesta. Yritä uudelleen." + "Huoneesta poistuminen epäonnistui" + "Ilmoita tästä huoneesta palvelimesi ylläpitäjälle. Jos viestit on salattu, ylläpitäjäsi ei voi lukea niitä." + "Kuvaile syytä…" + "Ilmoita huoneesta" + diff --git a/features/reportroom/impl/src/main/res/values-fr/translations.xml b/features/reportroom/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 00000000000..51376e34eaa --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,8 @@ + + + "Votre rapport a été envoyé avec succès, mais nous avons rencontré un problème en essayant de quitter le salon. Veuillez réessayer." + "Impossible de quitter le salon" + "Signaler ce salon à votre admin. Si les messages sont chiffrés, votre admin ne pourra pas les lire." + "Décrivez la raison…" + "Signaler le salon" + diff --git a/features/reportroom/impl/src/main/res/values-hu/translations.xml b/features/reportroom/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 00000000000..dcf563eb8e3 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,8 @@ + + + "A jelentése sikeresen el lett küldve, de hibát találtunk a szoba elhagyása során. Próbálja újra." + "Nem tudja elhagyni a szobát" + "A szoba jelentése a rendszergazdának. Ha az üzenetek titkosítva vannak, akkor a rendszergazda nem fogja tudni elolvasni őket." + "Írja le az okot…" + "Szoba jelentése" + diff --git a/features/reportroom/impl/src/main/res/values-in/translations.xml b/features/reportroom/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..c276158707d --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,8 @@ + + + "Laporan Anda berhasil dikirimkan, tetapi kami mengalami masalah saat mencoba meninggalkan ruangan. Silakan coba lagi." + "Tidak Dapat Meninggalkan Ruangan" + "Laporkan ruangan ini ke admin Anda. Jika pesan dienkripsi, admin Anda tidak akan dapat membacanya." + "Jelaskan alasan untuk melaporkan…" + "Laporkan ruangan" + diff --git a/features/reportroom/impl/src/main/res/values-it/translations.xml b/features/reportroom/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..bdb74b9ef15 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,8 @@ + + + "La tua segnalazione è stata inviata con successo, ma abbiamo riscontrato un problema durante il tentativo di lasciare la stanza. Per favore riprova." + "Impossibile lasciare la stanza" + "Segnala questa stanza al tuo amministratore. Se i messaggi sono cifrati, l\'amministratore non sarà in grado di leggerli." + "Descrivi il motivo della segnalazione…" + "Segnala stanza" + diff --git a/features/reportroom/impl/src/main/res/values-nb/translations.xml b/features/reportroom/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..bda13342106 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,8 @@ + + + "Rapporten din ble sendt inn, men vi oppdaget et problem da vi prøvde å forlate rommet. Prøv igjen." + "Kan ikke forlate rommet" + "Rapporter dette rommet til administratoren din. Hvis meldingene er kryptert, vil administratoren ikke kunne lese dem." + "Beskriv årsaken…" + "Rapporter rom" + diff --git a/features/reportroom/impl/src/main/res/values-pl/translations.xml b/features/reportroom/impl/src/main/res/values-pl/translations.xml new file mode 100644 index 00000000000..dd1b928e81c --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-pl/translations.xml @@ -0,0 +1,8 @@ + + + "Twoje zgłoszenie zostało wysłane pomyślnie, ale napotkaliśmy problem podczas opuszczania pokoju. Spróbuj ponownie." + "Nie można wyjść z pokoju" + "Zgłoś ten pokój swojemu administratorowi. Jeśli wiadomości są zaszyfrowane, administrator nie będzie mógł ich odczytać." + "Opisz powód…" + "Zgłoś pokój" + diff --git a/features/reportroom/impl/src/main/res/values-pt-rBR/translations.xml b/features/reportroom/impl/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..53f19677ef9 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,8 @@ + + + "Sua denúncia foi enviada com sucesso, mas encontramos um problema ao tentar sair da sala. Por favor, tente novamente." + "Não foi possível deixar a sala" + "Denuncie esta sala ao seu administrador. Se as mensagens estiverem criptografadas, seu administrador não poderá lê-las." + "Descreva o motivo para denunciar…" + "Denunciar sala" + diff --git a/features/reportroom/impl/src/main/res/values-pt/translations.xml b/features/reportroom/impl/src/main/res/values-pt/translations.xml new file mode 100644 index 00000000000..f83626a9e4b --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-pt/translations.xml @@ -0,0 +1,8 @@ + + + "O teu relatório foi submetido com sucesso, mas houve um problema ao tentar sair da sala. Por favor, tenta novamente." + "Não foi possível sair da sala" + "Denuncia esta sala aos administradores. Se as mensagens estiverem cifradas, os administradores não as poderão ler." + "Descrever a razão de denúncia…" + "Denunciar sala" + diff --git a/features/reportroom/impl/src/main/res/values-ru/translations.xml b/features/reportroom/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 00000000000..d413f25b442 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,8 @@ + + + "Ваш отчет был успешно отправлен, но мы столкнулись с проблемой при попытке покинуть комнату. Пожалуйста, попробуйте еще раз." + "Невозможно покинуть комнату" + "Сообщите об этой комнате своему администратору. Если сообщения зашифрованы, ваш администратор не сможет их прочитать." + "Опишите причину…" + "Комната отчетов" + diff --git a/features/reportroom/impl/src/main/res/values-sk/translations.xml b/features/reportroom/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 00000000000..87abe2f4af7 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,8 @@ + + + "Vaša správa bola úspešne odoslaná, ale pri pokuse o opustenie miestnosti sme narazili na problém. Skúste to prosím znova." + "Nie je možné opustiť miestnosť" + "Nahláste túto miestnosť svojmu správcovi. Ak sú správy zašifrované, váš správca ich nebude môcť prečítať." + "Popíšte dôvod…" + "Nahlásiť miestnosť" + diff --git a/features/reportroom/impl/src/main/res/values-sv/translations.xml b/features/reportroom/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 00000000000..174949b87dc --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,8 @@ + + + "Din anmälan skickades in framgångsrikt, men vi stötte på ett problem när vi försökte lämna rummet. Vänligen försök igen." + "Kunde inte lämna rummet" + "Anmäl det här rummet till din administratör. Om meddelandena är krypterade kommer din administratör inte att kunna läsa dem." + "Beskriv anledningen …" + "Rapportera rum" + diff --git a/features/reportroom/impl/src/main/res/values-uk/translations.xml b/features/reportroom/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 00000000000..e7208528483 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,8 @@ + + + "Ваша скарга надіслана, але ми зіткнулися з проблемою під час спроби вийти з кімнати. Повторіть спробу." + "Не вдалося вийти з кімнати" + "Поскаржтеся на цю кімнату своєму адміністратору. Якщо повідомлення зашифровані, ваш адміністратор не зможе їх прочитати." + "Опишіть причину…" + "Поскаржитися на кімнату" + diff --git a/features/reportroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/reportroom/impl/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 00000000000..1118a72c97c --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,8 @@ + + + "您的回報已成功遞交,但我們嘗試離開聊天室時遇到了問題。請再試一次。" + "無法離開聊天室" + "將此聊天室回報給您的管理員。若訊息已加密,您的管理員將無法讀取它們。" + "說明回報的原因……" + "回報聊天室" + diff --git a/features/reportroom/impl/src/main/res/values/localazy.xml b/features/reportroom/impl/src/main/res/values/localazy.xml new file mode 100644 index 00000000000..16183e6f1ba --- /dev/null +++ b/features/reportroom/impl/src/main/res/values/localazy.xml @@ -0,0 +1,8 @@ + + + "Your report was submitted successfully, but we encountered an issue while trying to leave the room. Please try again." + "Unable to Leave Room" + "Report this room to your admin. If the messages are encrypted, your admin will not be able to read them." + "Describe the reason to report…" + "Report room" + diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomTest.kt new file mode 100644 index 00000000000..fc410ab08f1 --- /dev/null +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/DefaultReportRoomTest.kt @@ -0,0 +1,150 @@ +/* + * 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.reportroom.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +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 kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultReportRoomTest { + private val roomId = A_ROOM_ID + private val successLeaveRoomLambda = lambdaRecorder> { -> Result.success(Unit) } + private val successReportRoomLambda = + lambdaRecorder> { _ -> Result.success(Unit) } + + private val failureLeaveRoomLambda = + lambdaRecorder> { -> Result.failure(Exception("Leave room error")) } + private val failureReportRoomLambda = + lambdaRecorder> { _ -> Result.failure(Exception("Report room error")) } + + @Test + fun `report room, leave=false, report=false, nothing is called`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = false, reason = "", shouldLeave = false) + + assertThat(result.isSuccess).isTrue() + assert(successLeaveRoomLambda).isNeverCalled() + assert(successReportRoomLambda).isNeverCalled() + } + + @Test + fun `report room, leave=false, report=true, report room success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = false) + + assertThat(result.isSuccess).isTrue() + assert(successLeaveRoomLambda).isNeverCalled() + assert(successReportRoomLambda) + .isCalledOnce() + .with(value("Spam")) + } + + @Test + fun `report room, leave=true, report=false, leave room success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = false, reason = "", shouldLeave = true) + + assertThat(result.isSuccess).isTrue() + assert(successLeaveRoomLambda).isCalledOnce() + assert(successReportRoomLambda).isNeverCalled() + } + + @Test + fun `report room, leave=true, report=true, leave room success`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true) + + assertThat(result.isSuccess).isTrue() + assert(successLeaveRoomLambda).isCalledOnce() + assert(successReportRoomLambda) + .isCalledOnce() + .with(value("Spam")) + } + + @Test + fun `report room, leave=true, report=true, leave room failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = failureLeaveRoomLambda, + reportRoomResult = successReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true) + + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isEqualTo(ReportRoom.Exception.LeftRoomFailed) + assert(failureLeaveRoomLambda).isCalledOnce() + assert(successReportRoomLambda).isCalledOnce() + } + + @Test + fun `report room, leave=true, report=true, report room failed`() = runTest { + val room = FakeBaseRoom( + roomId = roomId, + leaveRoomLambda = successLeaveRoomLambda, + reportRoomResult = failureReportRoomLambda + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(roomId, room) + } + val reportRoom = DefaultReportRoom(client = client) + + val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true) + + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isEqualTo(ReportRoom.Exception.ReportRoomFailed) + assert(successLeaveRoomLambda).isNeverCalled() + assert(failureReportRoomLambda).isCalledOnce() + } +} diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt new file mode 100644 index 00000000000..d85f2b86e83 --- /dev/null +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt @@ -0,0 +1,151 @@ +/* + * 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.reportroom.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.reportroom.impl.fakes.FakeReportRoom +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.tests.testutils.lambda.any +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ReportRoomPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createReportRoomPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.reason).isEmpty() + assertThat(state.leaveRoom).isFalse() + assertThat(state.reportAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(state.canReport).isFalse() + } + } + } + + @Test + fun `present - update form values`() = runTest { + val presenter = createReportRoomPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.reason).isEmpty() + assertThat(state.canReport).isFalse() + assertThat(state.leaveRoom).isFalse() + state.eventSink(ReportRoomEvents.UpdateReason("Spam")) + } + awaitItem().also { state -> + assertThat(state.reason).isEqualTo("Spam") + assertThat(state.canReport).isTrue() + assertThat(state.leaveRoom).isFalse() + state.eventSink(ReportRoomEvents.ToggleLeaveRoom) + } + awaitItem().also { state -> + assertThat(state.leaveRoom).isTrue() + assertThat(state.canReport).isTrue() + assertThat(state.canReport).isTrue() + } + } + } + + @Test + fun `present - report room success`() = runTest { + val roomId = A_ROOM_ID + val reportRoomLambda = lambdaRecorder> { _, _, _, _ -> Result.success(Unit) } + val reportRoom = FakeReportRoom( + lambda = reportRoomLambda + ) + val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom) + presenter.test { + awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom) + awaitItem().eventSink(ReportRoomEvents.Report) + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Success::class.java) + } + assert(reportRoomLambda) + .isCalledOnce() + .with(value(roomId), value(true), any(), value(true)) + } + } + + @Test + fun `present - report failed`() = runTest { + val roomId = A_ROOM_ID + val reportRoomLambda = lambdaRecorder> { _, _, _, _ -> + Result.failure(ReportRoom.Exception.ReportRoomFailed) + } + val reportRoom = FakeReportRoom( + lambda = reportRoomLambda + ) + val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom) + presenter.test { + awaitItem().eventSink(ReportRoomEvents.Report) + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java) + } + assert(reportRoomLambda) + .isCalledOnce() + .with(value(roomId), value(true), any(), any()) + } + } + + @Test + fun `present - leave room failed after report room success`() = runTest { + val roomId = A_ROOM_ID + val reportRoomLambda = lambdaRecorder> { _, _, _, _ -> + Result.failure(ReportRoom.Exception.LeftRoomFailed) + } + val reportRoom = FakeReportRoom( + lambda = reportRoomLambda + ) + val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom) + presenter.test { + awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom) + awaitItem().eventSink(ReportRoomEvents.Report) + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink(ReportRoomEvents.Report) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java) + } + assert(reportRoomLambda) + .isCalledExactly(2) + .withSequence( + // The first call should report the room and try leaving it + listOf(value(roomId), value(true), any(), value(true)), + // The second call should not report the room again + listOf(value(roomId), value(false), any(), value(true)) + ) + } + } + + fun createReportRoomPresenter( + roomId: RoomId = A_ROOM_ID, + reportRoom: ReportRoom = FakeReportRoom() + ): ReportRoomPresenter { + return ReportRoomPresenter(roomId, reportRoom) + } +} 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 new file mode 100644 index 00000000000..13dc5eb204d --- /dev/null +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt @@ -0,0 +1,101 @@ +/* + * 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.reportroom.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +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.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`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setReportRoomView( + aReportRoomState( + eventSink = eventsRecorder, + ), + onBackClick = it + ) + rule.pressBack() + } + } + + @Test + fun `clicking on report when enabled emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setReportRoomView( + aReportRoomState( + reason = "Spam", + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_report) + eventsRecorder.assertSingle(ReportRoomEvents.Report) + } + + @Test + fun `clicking on decline when disabled does not emit event`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setReportRoomView( + aReportRoomState(eventSink = eventsRecorder), + ) + rule.clickOn(CommonStrings.action_report) + } + + @Test + fun `clicking on leave room option emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setReportRoomView( + aReportRoomState(eventSink = eventsRecorder), + ) + rule.clickOn(CommonStrings.action_leave_room) + eventsRecorder.assertSingle(ReportRoomEvents.ToggleLeaveRoom) + } + + @Test + fun `typing text in the reason field emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setReportRoomView( + aReportRoomState( + eventSink = eventsRecorder, + reason = "" + ), + ) + rule.onNodeWithText("").performTextInput("Spam!") + eventsRecorder.assertSingle(ReportRoomEvents.UpdateReason("Spam!")) + } +} + +private fun AndroidComposeTestRule.setReportRoomView( + state: ReportRoomState, + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + ReportRoomView( + state = state, + onBackClick = onBackClick, + ) + } +} diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/fakes/FakeReportRoom.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/fakes/FakeReportRoom.kt new file mode 100644 index 00000000000..35d2db3ad82 --- /dev/null +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/fakes/FakeReportRoom.kt @@ -0,0 +1,26 @@ +/* + * 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.reportroom.impl.fakes + +import io.element.android.features.reportroom.impl.ReportRoom +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +class FakeReportRoom( + var lambda: (RoomId, Boolean, String, Boolean) -> Result = { _, _, _, _ -> lambdaError() } +) : ReportRoom { + override suspend fun invoke( + roomId: RoomId, + shouldReport: Boolean, + reason: String, + shouldLeave: Boolean + ): Result = simulateLongTask { + lambda(roomId, shouldReport, reason, shouldLeave) + } +} diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvents.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvents.kt index b1faa6ef6db..34a96eaeb8e 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvents.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvents.kt @@ -9,4 +9,5 @@ package io.element.android.features.roomaliasresolver.impl sealed interface RoomAliasResolverEvents { data object Retry : RoomAliasResolverEvents + data object DismissError : RoomAliasResolverEvents } diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt index 7391a35f414..34c655bd68b 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt @@ -46,6 +46,7 @@ class RoomAliasResolverPresenter @AssistedInject constructor( fun handleEvents(event: RoomAliasResolverEvents) { when (event) { RoomAliasResolverEvents.Retry -> coroutineScope.resolveAlias(resolveState) + RoomAliasResolverEvents.DismissError -> resolveState.value = AsyncData.Uninitialized } } @@ -60,7 +61,7 @@ class RoomAliasResolverPresenter @AssistedInject constructor( suspend { matrixClient.resolveRoomAlias(roomAlias) .getOrThrow() - .getOrElse { error("Failed to resolve room alias $roomAlias") } + .getOrElse { throw RoomAliasResolverFailures.UnknownAlias } }.runCatchingUpdatingState(resolveState) } } diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt index d03b004027f..dc858621f7f 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt @@ -18,3 +18,7 @@ data class RoomAliasResolverState( val resolveState: AsyncData, val eventSink: (RoomAliasResolverEvents) -> Unit ) + +sealed class RoomAliasResolverFailures : Exception() { + data object UnknownAlias : RoomAliasResolverFailures() +} diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt index 933d4acdf16..49786b6436c 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomaliasresolver.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias open class RoomAliasResolverStateProvider : PreviewParameterProvider { @@ -17,10 +18,10 @@ open class RoomAliasResolverStateProvider : PreviewParameterProvider Unit, modifier: Modifier = Modifier, ) { - val latestOnSuccess by rememberUpdatedState(onSuccess) - LaunchedEffect(state.resolveState) { - if (state.resolveState is AsyncData.Success) { - latestOnSuccess(state.resolveState.data) - } - } Box( modifier = modifier.fillMaxSize(), ) { - LightGradientBackground() HeaderFooterPage( containerColor = Color.Transparent, - paddingValues = PaddingValues(16.dp), + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 32.dp + ), topBar = { RoomAliasResolverTopBar(onBackClick = onBackClick) }, content = { - RoomAliasResolverContent(state = state) + RoomAliasResolverContent(roomAlias = state.roomAlias, isLoading = state.resolveState.isLoading()) }, - footer = { - RoomAliasResolverFooter( - state = state, - ) + ) + ResolvedRoomAliasView( + resolvedRoomAlias = state.resolveState, + onSuccess = onSuccess, + onRetry = { state.eventSink(RoomAliasResolverEvents.Retry) }, + onDismissError = { + state.eventSink(RoomAliasResolverEvents.DismissError) + onBackClick() } ) } } @Composable -private fun RoomAliasResolverFooter( - state: RoomAliasResolverState, - modifier: Modifier = Modifier, +private fun ResolvedRoomAliasView( + resolvedRoomAlias: AsyncData, + onSuccess: (ResolvedRoomAlias) -> Unit, + onRetry: () -> Unit, + onDismissError: () -> Unit, ) { - when (state.resolveState) { - is AsyncData.Failure -> { - Button( - text = stringResource(CommonStrings.action_retry), - onClick = { - state.eventSink(RoomAliasResolverEvents.Retry) - }, - modifier = modifier.fillMaxWidth(), - size = ButtonSize.Large, - ) + when (resolvedRoomAlias) { + is AsyncData.Success -> { + val latestOnSuccess by rememberUpdatedState(onSuccess) + LaunchedEffect(Unit) { + latestOnSuccess(resolvedRoomAlias.data) + } } - is AsyncData.Loading -> { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - CircularProgressIndicator() + is AsyncData.Failure -> { + if (resolvedRoomAlias.error is RoomAliasResolverFailures.UnknownAlias) { + ErrorDialog( + title = stringResource(id = R.string.screen_join_room_loading_alert_title), + content = stringResource(id = R.string.screen_room_alias_resolver_resolve_alias_failure), + onSubmit = onDismissError + ) + } else { + RetryDialog( + title = stringResource(id = R.string.screen_join_room_loading_alert_title), + content = stringResource(id = CommonStrings.error_network_or_server_issue), + onRetry = onRetry, + onDismiss = onDismissError + ) } } - AsyncData.Uninitialized, - is AsyncData.Success -> Unit + else -> Unit } } @Composable private fun RoomAliasResolverContent( - state: RoomAliasResolverState, + roomAlias: RoomAlias, + isLoading: Boolean, modifier: Modifier = Modifier, ) { RoomPreviewOrganism( @@ -118,20 +120,13 @@ private fun RoomAliasResolverContent( PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) }, title = { - RoomPreviewTitleAtom(state.roomAlias.value) + RoomPreviewSubtitleAtom(roomAlias.value) }, subtitle = { - }, - description = { - if (state.resolveState.isFailure()) { - Text( - text = stringResource(id = R.string.screen_room_alias_resolver_resolve_alias_failure), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.error, - ) + if (isLoading) { + Spacer(Modifier.height(8.dp)) + CircularProgressIndicator() } - }, - memberCount = { } ) } diff --git a/features/roomaliasresolver/impl/src/main/res/values-cs/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-cs/translations.xml index 234a9f71981..58bb797c8f4 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-cs/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-cs/translations.xml @@ -1,4 +1,5 @@ + "Náhled této místnosti jsme nemohli zobrazit" "Nepodařilo se přeložit alias místnosti." diff --git a/features/roomaliasresolver/impl/src/main/res/values-cy/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..8e87e3a91de --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,5 @@ + + + "Doedd dim modd dangos rhagolwg yr ystafell hon" + "Wedi methu â datrys arallenw ystafell." + diff --git a/features/roomaliasresolver/impl/src/main/res/values-da/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..68cde2d29b9 --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,5 @@ + + + "Vi kunne ikke forhåndsvise rummet" + "Kunne ikke løse rummets alias." + diff --git a/features/roomaliasresolver/impl/src/main/res/values-de/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-de/translations.xml index 0ed5be402b6..6faabb5b11d 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-de/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-de/translations.xml @@ -1,4 +1,5 @@ + "Wir konnten diese Chatroomvorschau nicht anzeigen" "Der Raum-Alias konnte nicht ermittelt werden." diff --git a/features/roomaliasresolver/impl/src/main/res/values-el/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-el/translations.xml index 8b1725d64a9..266cb5080b3 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-el/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-el/translations.xml @@ -1,4 +1,5 @@ - "Αποτυχία επίλυσης ψευδωνύμου δωματίου." + "Δεν μπορέσαμε να εμφανίσουμε αυτή την προεπισκόπηση αίθουσας" + "Αποτυχία επίλυσης του ψευδώνυμου αίθουσας." diff --git a/features/roomaliasresolver/impl/src/main/res/values-es/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..73cc4936eac --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,5 @@ + + + "No hemos podido mostrar la vista previa de esta sala" + "No se pudo resolver el alias de la sala." + diff --git a/features/roomaliasresolver/impl/src/main/res/values-et/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-et/translations.xml index 6b712e00eef..b2eb5c0eb52 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-et/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-et/translations.xml @@ -1,4 +1,5 @@ + "Meil ei õnnestunud selle jututoa eelvaadet kuvada" "Jututoa aliasele vastava aadressi tuvastamine ei õnnestunud." diff --git a/features/roomaliasresolver/impl/src/main/res/values-fi/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-fi/translations.xml index 20b00dcdd7c..84e06154727 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-fi/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-fi/translations.xml @@ -1,4 +1,5 @@ + "Emme voineet näyttää tämän huoneen esikatselua" "Huoneen aliaksen ratkaiseminen epäonnistui." diff --git a/features/roomaliasresolver/impl/src/main/res/values-fr/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-fr/translations.xml index 72395bbb06b..6271998f19b 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-fr/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-fr/translations.xml @@ -1,4 +1,5 @@ + "Impossible d’afficher l’aperçu de ce salon" "Impossible de trouver un salon avec cet alias." diff --git a/features/roomaliasresolver/impl/src/main/res/values-hu/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-hu/translations.xml index d1d6d05c989..585a3be62e9 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-hu/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-hu/translations.xml @@ -1,4 +1,5 @@ + "Nem tudtuk megjeleníteni a szoba előnézetét" "Nem sikerült a szoba álnevének feloldása." diff --git a/features/roomaliasresolver/impl/src/main/res/values-in/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..2e84cbfe002 --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,5 @@ + + + "Kami tidak dapat menampilkan pratinjau ruangan ini" + "Gagal menyelesaikan alias ruangan." + diff --git a/features/roomaliasresolver/impl/src/main/res/values-it/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-it/translations.xml index e52051ae938..5860ac29390 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-it/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-it/translations.xml @@ -1,4 +1,5 @@ + "Non è stato possibile visualizzare l\'anteprima di questa stanza" "Impossibile risolvere l\'alias della stanza." diff --git a/features/roomaliasresolver/impl/src/main/res/values-nb/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..1a9a264e9fd --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,5 @@ + + + "Vi kunne ikke vise forhåndsvisning av dette rommet" + "Kunne ikke løse romalias." + diff --git a/features/roomaliasresolver/impl/src/main/res/values-pl/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-pl/translations.xml index 7f830ab944d..dabe170e2ed 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-pl/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-pl/translations.xml @@ -1,4 +1,5 @@ + "Nie udało nam się wyświetlić podglądu tego pokoju" "Nie udało się uzyskać aliasu pokoju." diff --git a/features/roomaliasresolver/impl/src/main/res/values-pt-rBR/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..461fb64f7c4 --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,5 @@ + + + "Não foi possível exibir a visualização desta sala" + "Falha ao descobrir o alias da sala." + diff --git a/features/roomaliasresolver/impl/src/main/res/values-pt/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-pt/translations.xml index aa469c7158b..3ae285d6ab2 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-pt/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-pt/translations.xml @@ -1,4 +1,5 @@ + "Não foi possível exibir a pré-visualização desta sala" "Não foi possível encontrar esse endereço de sala" diff --git a/features/roomaliasresolver/impl/src/main/res/values-ru/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-ru/translations.xml index 53e8da81cae..f7db24f4415 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-ru/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-ru/translations.xml @@ -1,4 +1,5 @@ + "Мы не смогли отобразить предварительный просмотр этой комнаты" "Не удалось определить псевдоним комнаты." diff --git a/features/roomaliasresolver/impl/src/main/res/values-sk/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-sk/translations.xml index 059788804f4..71315874e6e 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-sk/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-sk/translations.xml @@ -1,4 +1,5 @@ + "Ukážku tejto miestnosti sa nepodarilo zobraziť" "Nepodarilo sa nájsť alias miestnosti." diff --git a/features/roomaliasresolver/impl/src/main/res/values-sv/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-sv/translations.xml index 8bcfb3f809a..d2da1cae886 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-sv/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-sv/translations.xml @@ -1,4 +1,5 @@ + "Vi kunde inte visa förhandsgranskningen av rummet" "Misslyckades med att slå upp rumsalias." diff --git a/features/roomaliasresolver/impl/src/main/res/values-tr/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..395799e380c --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,5 @@ + + + "Bu oda önizlemesini görüntüleyemedik" + "Oda takma adı çözümlenemedi." + diff --git a/features/roomaliasresolver/impl/src/main/res/values-uk/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-uk/translations.xml index 0f5d40f6d83..91412c2cd11 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-uk/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-uk/translations.xml @@ -1,4 +1,5 @@ + "Ми не можемо показати попередній перегляд цієї кімнати" "Не вдалося розв\'язати псевдонім кімнати." diff --git a/features/roomaliasresolver/impl/src/main/res/values-ur/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..c415adca83b --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,4 @@ + + + "کمرے کے عرف کو حل کرنے میں ناکام۔" + diff --git a/features/roomaliasresolver/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-zh-rTW/translations.xml index 9aa1586b8a4..0e657405219 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,4 +1,5 @@ + "我們無法顯示此聊天室的預覽" "無法解析聊天室別名。" diff --git a/features/roomaliasresolver/impl/src/main/res/values/localazy.xml b/features/roomaliasresolver/impl/src/main/res/values/localazy.xml index 21d5c17135c..6ace0d7fb10 100644 --- a/features/roomaliasresolver/impl/src/main/res/values/localazy.xml +++ b/features/roomaliasresolver/impl/src/main/res/values/localazy.xml @@ -1,4 +1,5 @@ + "We couldn’t display this room preview" "Failed to resolve room alias." diff --git a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt index 13db67f4477..725915b91a3 100644 --- a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt +++ b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt @@ -13,6 +13,8 @@ import io.element.android.features.roomcall.api.RoomCallState.StandBy @Immutable sealed interface RoomCallState { + data object Unavailable : RoomCallState + data class StandBy( val canStartCall: Boolean, ) : RoomCallState @@ -25,6 +27,7 @@ sealed interface RoomCallState { } fun RoomCallState.hasPermissionToJoin() = when (this) { + RoomCallState.Unavailable -> false is StandBy -> canStartCall is OnGoing -> canJoinCall } diff --git a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt index cd83a066167..de3bc4ade87 100644 --- a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt +++ b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt @@ -16,6 +16,7 @@ open class RoomCallStateProvider : PreviewParameterProvider { anOngoingCallState(), anOngoingCallState(canJoinCall = false), anOngoingCallState(canJoinCall = true, isUserInTheCall = true), + RoomCallState.Unavailable, ) } diff --git a/features/roomcall/impl/build.gradle.kts b/features/roomcall/impl/build.gradle.kts index d4ed1e1fd4d..0ec555bfb5f 100644 --- a/features/roomcall/impl/build.gradle.kts +++ b/features/roomcall/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { api(projects.features.roomcall.api) implementation(libs.kotlinx.collections.immutable) implementation(projects.features.call.api) + implementation(projects.features.enterprise.api) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) @@ -32,6 +33,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.call.test) + testImplementation(projects.features.enterprise.test) testImplementation(projects.tests.testutils) testImplementation(libs.androidx.compose.ui.test.junit) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) 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 4c3ddddb174..47af2c24fc2 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 @@ -11,27 +11,33 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import io.element.android.features.call.api.CurrentCall import io.element.android.features.call.api.CurrentCallService +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.ui.room.canCall import javax.inject.Inject class RoomCallStatePresenter @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val currentCallService: CurrentCallService, + private val sessionEnterpriseService: SessionEnterpriseService, ) : Presenter { @Composable override fun present(): RoomCallState { - val roomInfo by room.roomInfoFlow.collectAsState(null) + val isAvailable by produceState(false) { + value = sessionEnterpriseService.isElementCallAvailable() + } + val roomInfo by room.roomInfoFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value) val isUserInTheCall by remember { derivedStateOf { - room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty() + room.sessionId in roomInfo.activeRoomCallParticipants } } val currentCall by currentCallService.currentCall.collectAsState() @@ -40,13 +46,18 @@ class RoomCallStatePresenter @Inject constructor( (currentCall as? CurrentCall.RoomCall)?.roomId == room.roomId } } - val callState = when { - roomInfo?.hasRoomCall == true -> RoomCallState.OnGoing( - canJoinCall = canJoinCall, - isUserInTheCall = isUserInTheCall, - isUserLocallyInTheCall = isUserLocallyInTheCall, - ) - else -> RoomCallState.StandBy(canStartCall = canJoinCall) + val callState by remember { + derivedStateOf { + when { + isAvailable.not() -> RoomCallState.Unavailable + roomInfo.hasRoomCall -> RoomCallState.OnGoing( + canJoinCall = canJoinCall, + isUserInTheCall = isUserInTheCall, + isUserLocallyInTheCall = isUserLocallyInTheCall, + ) + else -> RoomCallState.StandBy(canStartCall = canJoinCall) + } + } } return callState } 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 55559294567..9647fddc1de 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 @@ -11,9 +11,11 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.call.api.CurrentCall import io.element.android.features.call.api.CurrentCallService import io.element.android.features.call.test.FakeCurrentCallService +import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.roomcall.api.RoomCallState -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.MutableStateFlow @@ -23,11 +25,14 @@ import org.junit.Test class RoomCallStatePresenterTest { @Test fun `present - initial state`() = runTest { - val room = FakeMatrixRoom( - canUserJoinCallResult = { Result.success(false) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserJoinCallResult = { Result.success(false) }, + ) ) - val presenter = createRoomCallStatePresenter(matrixRoom = room) + val presenter = createRoomCallStatePresenter(joinedRoom = room) presenter.test { + skipItems(1) val initialState = awaitItem() assertThat(initialState).isEqualTo( RoomCallState.StandBy( @@ -37,12 +42,33 @@ class RoomCallStatePresenterTest { } } + @Test + fun `present - element call not available`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserJoinCallResult = { Result.success(false) }, + ) + ) + val presenter = createRoomCallStatePresenter( + joinedRoom = room, + isElementCallAvailable = false, + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + RoomCallState.Unavailable + ) + } + } + @Test fun `present - initial state - user can join call`() = runTest { - val room = FakeMatrixRoom( - canUserJoinCallResult = { Result.success(true) }, + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserJoinCallResult = { Result.success(true) }, + ) ) - val presenter = createRoomCallStatePresenter(matrixRoom = room) + val presenter = createRoomCallStatePresenter(joinedRoom = room) presenter.test { skipItems(1) val initialState = awaitItem() @@ -56,12 +82,13 @@ class RoomCallStatePresenterTest { @Test fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest { - val room = FakeMatrixRoom( - canUserJoinCallResult = { Result.success(false) }, - ).apply { - givenRoomInfo(aRoomInfo(hasRoomCall = true)) - } - val presenter = createRoomCallStatePresenter(matrixRoom = room) + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserJoinCallResult = { Result.success(false) }, + initialRoomInfo = aRoomInfo(hasRoomCall = true), + ) + ) + val presenter = createRoomCallStatePresenter(joinedRoom = room) presenter.test { skipItems(1) assertThat(awaitItem()).isEqualTo( @@ -76,17 +103,19 @@ class RoomCallStatePresenterTest { @Test fun `present - user has joined the call on another session`() = runTest { - val room = FakeMatrixRoom( - canUserJoinCallResult = { Result.success(true) }, - ).apply { - givenRoomInfo( - aRoomInfo( - hasRoomCall = true, - activeRoomCallParticipants = listOf(sessionId), + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserJoinCallResult = { Result.success(true) }, + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeRoomCallParticipants = listOf(sessionId), + ) ) - ) - } - val presenter = createRoomCallStatePresenter(matrixRoom = room) + } + ) + val presenter = createRoomCallStatePresenter(joinedRoom = room) presenter.test { skipItems(1) assertThat(awaitItem()).isEqualTo( @@ -101,18 +130,20 @@ class RoomCallStatePresenterTest { @Test fun `present - user has joined the call locally`() = runTest { - val room = FakeMatrixRoom( - canUserJoinCallResult = { Result.success(true) }, - ).apply { - givenRoomInfo( - aRoomInfo( - hasRoomCall = true, - activeRoomCallParticipants = listOf(sessionId), + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserJoinCallResult = { Result.success(true) }, + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeRoomCallParticipants = listOf(sessionId), + ) ) - ) - } + } + ) val presenter = createRoomCallStatePresenter( - matrixRoom = room, + joinedRoom = room, currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))), ) presenter.test { @@ -129,20 +160,22 @@ class RoomCallStatePresenterTest { @Test fun `present - user leaves the call`() = runTest { - val room = FakeMatrixRoom( - canUserJoinCallResult = { Result.success(true) }, - ).apply { - givenRoomInfo( - aRoomInfo( - hasRoomCall = true, - activeRoomCallParticipants = listOf(sessionId), + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserJoinCallResult = { Result.success(true) }, + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeRoomCallParticipants = listOf(sessionId), + ) ) - ) - } + } + ) val currentCall = MutableStateFlow(CurrentCall.RoomCall(room.roomId)) val currentCallService = FakeCurrentCallService(currentCall = currentCall) val presenter = createRoomCallStatePresenter( - matrixRoom = room, + joinedRoom = room, currentCallService = currentCallService ) presenter.test { @@ -190,12 +223,16 @@ class RoomCallStatePresenterTest { } private fun createRoomCallStatePresenter( - matrixRoom: MatrixRoom, + joinedRoom: JoinedRoom, currentCallService: CurrentCallService = FakeCurrentCallService(), + isElementCallAvailable: Boolean = true, ): RoomCallStatePresenter { return RoomCallStatePresenter( - room = matrixRoom, + room = joinedRoom, currentCallService = currentCallService, + sessionEnterpriseService = FakeSessionEnterpriseService( + isElementCallAvailableResult = { isElementCallAvailable }, + ), ) } } diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 1c092ec8053..4e87946ba4a 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -24,6 +24,7 @@ android { setupAnvil() dependencies { + implementation(projects.appconfig) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) @@ -51,6 +52,9 @@ dependencies { implementation(projects.features.messages.api) implementation(projects.features.roomcall.api) implementation(projects.features.knockrequests.api) + implementation(projects.features.verifysession.api) + implementation(projects.features.reportroom.api) + implementation(projects.features.roommembermoderation.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -63,6 +67,7 @@ dependencies { testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.permissions.test) + testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.tests.testutils) 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 57d1cc6df02..93c93c345a1 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 @@ -11,5 +11,6 @@ sealed interface RoomDetailsEvent { data object LeaveRoom : RoomDetailsEvent data object MuteNotification : RoomDetailsEvent data object UnmuteNotification : RoomDetailsEvent + data class CopyToClipboard(val text: String) : RoomDetailsEvent data class SetFavorite(val isFavorite: Boolean) : 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 774abfe8cdd..986d86ea411 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 @@ -9,6 +9,7 @@ package io.element.android.features.roomdetails.impl import android.os.Parcelable import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -21,11 +22,13 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.Interaction import io.element.android.anvilannotations.ContributesNode +import io.element.android.appconfig.LearnMoreConfig 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 import io.element.android.features.poll.api.history.PollHistoryEntryPoint +import io.element.android.features.reportroom.api.ReportRoomEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode @@ -35,18 +38,21 @@ import io.element.android.features.roomdetails.impl.notificationsettings.RoomNot import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyFlowNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper +import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.overlay.operation.hide import io.element.android.libraries.architecture.overlay.operation.show +import io.element.android.libraries.designsystem.utils.OpenUrlInTabView import io.element.android.libraries.di.RoomScope 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.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.services.analytics.api.AnalyticsService @@ -59,12 +65,14 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Assisted plugins: List, private val pollHistoryEntryPoint: PollHistoryEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint, - private val room: MatrixRoom, + private val room: BaseRoom, private val analyticsService: AnalyticsService, private val messagesEntryPoint: MessagesEntryPoint, private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint, private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val mediaGalleryEntryPoint: MediaGalleryEntryPoint, + private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint, + private val reportRoomEntryPoint: ReportRoomEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), @@ -118,6 +126,12 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data object SecurityAndPrivacy : NavTarget + + @Parcelize + data class VerifyUser(val userId: UserId) : NavTarget + + @Parcelize + data object ReportRoom : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -168,6 +182,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( backstack.push(NavTarget.SecurityAndPrivacy) } + override fun openDmUserProfile(userId: UserId) { + backstack.push(NavTarget.RoomMemberDetails(userId)) + } + override fun onJoinCall() { val inputs = CallType.RoomCall( sessionId = room.sessionId, @@ -176,6 +194,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) elementCallEntryPoint.startCall(inputs) } + + override fun openReportRoom() { + backstack.push(NavTarget.ReportRoom) + } } createNode(buildContext, listOf(roomDetailsCallback)) } @@ -224,6 +246,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun onStartCall(dmRoomId: RoomId) { elementCallEntryPoint.startCall(CallType.RoomCall(roomId = dmRoomId, sessionId = room.sessionId)) } + + override fun onVerifyUser(userId: UserId) { + backstack.push(NavTarget.VerifyUser(userId)) + } } val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback) createNode(buildContext, plugins) @@ -301,11 +327,40 @@ class RoomDetailsFlowNode @AssistedInject constructor( NavTarget.SecurityAndPrivacy -> { createNode(buildContext) } + is NavTarget.VerifyUser -> { + val params = OutgoingVerificationEntryPoint.Params( + showDeviceVerifiedScreen = true, + verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId,) + ) + outgoingVerificationEntryPoint.nodeBuilder(this, buildContext) + .params(params) + .callback(object : OutgoingVerificationEntryPoint.Callback { + override fun onDone() { + backstack.pop() + } + + override fun onBack() { + backstack.pop() + } + + override fun onLearnMoreAboutEncryption() { + learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL + } + }) + .build() + } + is NavTarget.ReportRoom -> { + reportRoomEntryPoint.createNode(this, buildContext, room.roomId) + } } } + private val learnMoreUrl = mutableStateOf(null) + @Composable override fun View(modifier: Modifier) { BackstackWithOverlayBox(modifier) + + OpenUrlInTabView(learnMoreUrl) } } 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 6ae3c555032..1c76f561e2f 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 @@ -23,7 +23,8 @@ import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.androidutils.system.startSharePlainTextIntent import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -35,7 +36,7 @@ class RoomDetailsNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: RoomDetailsPresenter, - private val room: MatrixRoom, + private val room: BaseRoom, private val analyticsService: AnalyticsService, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { @@ -50,7 +51,9 @@ class RoomDetailsNode @AssistedInject constructor( fun openPinnedMessagesList() fun openKnockRequestsList() fun openSecurityAndPrivacy() + fun openDmUserProfile(userId: UserId) fun onJoinCall() + fun openReportRoom() } private val callbacks = plugins() @@ -126,6 +129,14 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openSecurityAndPrivacy() } } + private fun onProfileClick(userId: UserId) { + callbacks.forEach { it.openDmUserProfile(userId) } + } + + private fun onReportRoomClick() { + callbacks.forEach { it.openReportRoom() } + } + @Composable override fun View(modifier: Modifier) { val context = LocalContext.current @@ -158,7 +169,9 @@ class RoomDetailsNode @AssistedInject constructor( onJoinCallClick = ::onJoinCall, onPinnedMessagesClick = ::openPinnedMessages, onKnockRequestsClick = ::openKnockRequestsLists, - onSecurityAndPrivacyClick = ::openSecurityAndPrivacy + onSecurityAndPrivacyClick = ::openSecurityAndPrivacy, + onProfileClick = ::onProfileClick, + onReportRoomClick = ::onReportRoomClick, ) } } 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 1bf44730e2f..d35d1a313ba 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 @@ -16,7 +16,6 @@ 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 im.vector.app.features.analytics.plan.Interaction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState @@ -24,18 +23,21 @@ import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEn import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.securityAndPrivacyPermissionsAsState +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.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.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +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.RoomMembersState import io.element.android.libraries.matrix.api.room.StateEventType -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.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canSendState @@ -43,7 +45,11 @@ import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.ui.room.canHandleKnockRequestsAsState 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.isDmAsState import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin +import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +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.toPersistentList @@ -55,7 +61,7 @@ import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( private val client: MatrixClient, - private val room: MatrixRoom, + private val room: JoinedRoom, private val featureFlagService: FeatureFlagService, private val notificationSettingsService: NotificationSettingsService, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, @@ -64,28 +70,30 @@ class RoomDetailsPresenter @Inject constructor( private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled, + private val clipboardHelper: ClipboardHelper, + private val appPreferencesStore: AppPreferencesStore, ) : Presenter { @Composable override fun present(): RoomDetailsState { val scope = rememberCoroutineScope() val leaveRoomState = leaveRoomPresenter.present() val canShowNotificationSettings = remember { mutableStateOf(false) } - val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val roomInfo by room.roomInfoFlow.collectAsState() val isUserAdmin = room.isOwnUserAdmin() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } } + val roomAvatar by remember { derivedStateOf { roomInfo.avatarUrl } } - val roomName by remember { derivedStateOf { (roomInfo?.name ?: room.displayName).trim() } } - val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } } - val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } } - val joinRule by remember { derivedStateOf { roomInfo?.joinRule } } + val roomName by remember { derivedStateOf { roomInfo.name?.trim().orEmpty() } } + val roomTopic by remember { derivedStateOf { roomInfo.topic } } + val isFavorite by remember { derivedStateOf { roomInfo.isFavorite } } + val joinRule by remember { derivedStateOf { roomInfo.joinRule } } val canShowPinnedMessages = isPinnedMessagesFeatureEnabled() - var canShowMediaGallery by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - canShowMediaGallery = featureFlagService.isFeatureEnabled(FeatureFlags.MediaGallery) - } - val pinnedMessagesCount by remember { derivedStateOf { roomInfo?.pinnedEventIds?.size } } + val pinnedMessagesCount by remember { derivedStateOf { roomInfo.pinnedEventIds.size } } + + val canShowMediaGallery by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaGallery) + }.collectAsState(false) LaunchedEffect(Unit) { canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) @@ -98,6 +106,9 @@ class RoomDetailsPresenter @Inject constructor( val membersState by room.membersStateFlow.collectAsState() val canInvite by getCanInvite(membersState) + val canonicalAlias by remember { derivedStateOf { roomInfo.canonicalAlias } } + val isEncrypted by remember { derivedStateOf { roomInfo.isEncrypted == true } } + val isDm by room.isDmAsState() val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME) val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR) val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC) @@ -106,6 +117,7 @@ class RoomDetailsPresenter @Inject constructor( val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomType = getRoomType(dmMember, currentMember) val roomCallState = roomCallStatePresenter.present() + val joinedMemberCount by remember { derivedStateOf { roomInfo.joinedMembersCount } } val topicState = remember(canEditTopic, roomTopic, roomType) { val topic = roomTopic @@ -117,16 +129,24 @@ class RoomDetailsPresenter @Inject constructor( } val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value) - val isKnockRequestsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(false) + val isKnockRequestsEnabled by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) + }.collectAsState(false) val knockRequestsCount by produceState(null) { room.knockRequestsFlow.collect { value = it.size } } val canShowKnockRequests by remember { derivedStateOf { isKnockRequestsEnabled && canHandleKnockRequests && joinRule == JoinRule.Knock } } + val isDeveloperModeEnabled by remember { + appPreferencesStore.isDeveloperModeEnabledFlow() + }.collectAsState(initial = false) val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState() + val snackbarDispatcher = LocalSnackbarDispatcher.current + val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() + fun handleEvents(event: RoomDetailsEvent) { when (event) { RoomDetailsEvent.LeaveRoom -> @@ -138,10 +158,14 @@ class RoomDetailsPresenter @Inject constructor( } RoomDetailsEvent.UnmuteNotification -> { scope.launch(dispatchers.io) { - client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.isOneToOne) + client.notificationSettingsService().unmuteRoom(room.roomId, isEncrypted, room.isOneToOne) } } is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite) + is RoomDetailsEvent.CopyToClipboard -> { + clipboardHelper.copyPlainText(event.text) + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) + } } } @@ -154,14 +178,22 @@ class RoomDetailsPresenter @Inject constructor( } } + val hasMemberVerificationViolations by produceState(false) { + room.roomMemberIdentityStateChange(waitForEncryption = true) + .onEach { identities -> value = identities.any { it.identityState == IdentityState.VerificationViolation } } + .launchIn(this) + } + + val canReportRoom by produceState(false) { value = client.canReportRoom() } + return RoomDetailsState( roomId = room.roomId, roomName = roomName, - roomAlias = room.canonicalAlias, + roomAlias = canonicalAlias, roomAvatarUrl = roomAvatar, roomTopic = topicState, - memberCount = room.joinedMemberCount, - isEncrypted = room.isEncrypted, + memberCount = joinedMemberCount, + isEncrypted = isEncrypted, canInvite = canInvite, canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room, canShowNotificationSettings = canShowNotificationSettings.value, @@ -171,15 +203,20 @@ class RoomDetailsPresenter @Inject constructor( leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), isFavorite = isFavorite, - displayRolesAndPermissionsSettings = !room.isDm && isUserAdmin, + displayRolesAndPermissionsSettings = !isDm && isUserAdmin, isPublic = joinRule == JoinRule.Public, - heroes = roomInfo?.heroes.orEmpty().toPersistentList(), + heroes = roomInfo.heroes.toPersistentList(), canShowPinnedMessages = canShowPinnedMessages, canShowMediaGallery = canShowMediaGallery, pinnedMessagesCount = pinnedMessagesCount, + snackbarMessage = snackbarMessage, canShowKnockRequests = canShowKnockRequests, knockRequestsCount = knockRequestsCount, canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, + hasMemberVerificationViolations = hasMemberVerificationViolations, + canReportRoom = canReportRoom, + isTombstoned = roomInfo.successorRoom != null, + showDebugInfo = isDeveloperModeEnabled, eventSink = ::handleEvents, ) } @@ -207,12 +244,12 @@ class RoomDetailsPresenter @Inject constructor( } @Composable - private fun getCanInvite(membersState: MatrixRoomMembersState) = produceState(false, membersState) { + private fun getCanInvite(membersState: RoomMembersState) = produceState(false, membersState) { value = room.canInvite().getOrElse { false } } @Composable - private fun getCanSendState(membersState: MatrixRoomMembersState, type: StateEventType) = produceState(false, membersState) { + private fun getCanSendState(membersState: RoomMembersState, type: StateEventType) = produceState(false, membersState) { value = room.canSendState(type).getOrElse { false } } 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 8a9401d5f01..331329a0340 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 @@ -11,6 +11,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.userprofile.api.UserProfileState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMember @@ -42,9 +43,14 @@ data class RoomDetailsState( val canShowPinnedMessages: Boolean, val canShowMediaGallery: Boolean, val pinnedMessagesCount: Int?, + val snackbarMessage: SnackbarMessage?, val canShowKnockRequests: Boolean, val knockRequestsCount: Int?, val canShowSecurityAndPrivacy: Boolean, + val hasMemberVerificationViolations: Boolean, + val canReportRoom: Boolean, + val isTombstoned: Boolean, + val showDebugInfo: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) { val roomBadges = buildList { 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 59601f065da..a5e81040e82 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 @@ -14,8 +14,10 @@ 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 import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage 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 @@ -31,7 +33,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider override val values: Sequence get() = sequenceOf( aRoomDetailsState(displayAdminSettings = true), - aRoomDetailsState(roomTopic = RoomTopicState.Hidden), + aRoomDetailsState(roomTopic = RoomTopicState.Hidden, showDebugInfo = true), aRoomDetailsState(roomTopic = RoomTopicState.CanAddTopic), aRoomDetailsState(isEncrypted = false), aRoomDetailsState(roomAlias = null), @@ -50,6 +52,10 @@ open class RoomDetailsStateProvider : PreviewParameterProvider aRoomDetailsState(pinnedMessagesCount = 3), aRoomDetailsState(knockRequestsCount = null, canShowKnockRequests = true), aRoomDetailsState(knockRequestsCount = 4, canShowKnockRequests = true), + aRoomDetailsState(hasMemberVerificationViolations = true), + aRoomDetailsState(isTombstoned = true), + aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFIED), + aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFICATION_VIOLATION), // Add other state here ) } @@ -64,6 +70,7 @@ fun aDmRoomMember( normalizedPowerLevel: Long = powerLevel, isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.USER, + membershipChangeReason: String? = null, ) = RoomMember( userId = userId, displayName = displayName, @@ -74,6 +81,7 @@ fun aDmRoomMember( normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, role = role, + membershipChangeReason = membershipChangeReason ) fun aRoomDetailsState( @@ -105,9 +113,14 @@ fun aRoomDetailsState( canShowPinnedMessages: Boolean = true, canShowMediaGallery: Boolean = true, pinnedMessagesCount: Int? = null, + snackbarMessage: SnackbarMessage? = null, canShowKnockRequests: Boolean = false, knockRequestsCount: Int? = null, canShowSecurityAndPrivacy: Boolean = true, + hasMemberVerificationViolations: Boolean = false, + canReportRoom: Boolean = true, + isTombstoned: Boolean = false, + showDebugInfo: Boolean = false, eventSink: (RoomDetailsEvent) -> Unit = {}, ) = RoomDetailsState( roomId = roomId, @@ -132,10 +145,15 @@ fun aRoomDetailsState( canShowPinnedMessages = canShowPinnedMessages, canShowMediaGallery = canShowMediaGallery, pinnedMessagesCount = pinnedMessagesCount, + snackbarMessage = snackbarMessage, canShowKnockRequests = canShowKnockRequests, knockRequestsCount = knockRequestsCount, canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, - eventSink = eventSink + hasMemberVerificationViolations = hasMemberVerificationViolations, + canReportRoom = canReportRoom, + isTombstoned = isTombstoned, + showDebugInfo = showDebugInfo, + eventSink = eventSink, ) fun aRoomNotificationSettings( @@ -150,6 +168,7 @@ fun aDmRoomDetailsState( isDmMemberIgnored: Boolean = false, roomName: String = "Daniel", isEncrypted: Boolean = true, + dmRoomMemberVerificationState: UserProfileVerificationState = UserProfileVerificationState.UNKNOWN, ) = aRoomDetailsState( roomName = roomName, isPublic = false, @@ -160,5 +179,6 @@ fun aDmRoomDetailsState( ), roomMemberDetailsState = aUserProfileState( isBlocked = AsyncData.Success(isDmMemberIgnored), + verificationState = dmRoomMemberVerificationState, ) ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index e35de9f5ec9..4ffe29a8bf1 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 @@ -21,9 +21,6 @@ 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.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.outlined.Add import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -33,6 +30,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.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter @@ -42,22 +40,25 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.leaveroom.api.LeaveRoomView import io.element.android.features.roomcall.api.hasPermissionToJoin +import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs import io.element.android.features.userprofile.shared.blockuser.BlockUserSection +import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule import io.element.android.libraries.designsystem.components.ClickableLinkText +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.CompositeAvatar +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 import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceText +import io.element.android.libraries.designsystem.modifiers.niceClickable import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight @@ -72,8 +73,11 @@ import io.element.android.libraries.designsystem.theme.components.ListItemStyle import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost +import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.getBestName @@ -105,8 +109,11 @@ fun RoomDetailsView( onPinnedMessagesClick: () -> Unit, onKnockRequestsClick: () -> Unit, onSecurityAndPrivacyClick: () -> Unit, + onProfileClick: (UserId) -> Unit, + onReportRoomClick: () -> Unit, modifier: Modifier = Modifier, ) { + val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) Scaffold( modifier = modifier, topBar = { @@ -116,6 +123,7 @@ fun RoomDetailsView( onActionClick = onActionClick ) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, ) { padding -> Column( modifier = Modifier @@ -133,9 +141,13 @@ fun RoomDetailsView( roomName = state.roomName, roomAlias = state.roomAlias, heroes = state.heroes, + isTombstoned = state.isTombstoned, openAvatarPreview = { avatarUrl -> openAvatarPreview(state.roomName, avatarUrl) }, + onSubtitleClick = { subtitle -> + state.eventSink(RoomDetailsEvent.CopyToClipboard(subtitle)) + } ) } is RoomDetailsType.Dm -> { @@ -146,6 +158,9 @@ fun RoomDetailsView( openAvatarPreview = { name, avatarUrl -> openAvatarPreview(name, avatarUrl) }, + onSubtitleClick = { subtitle -> + state.eventSink(RoomDetailsEvent.CopyToClipboard(subtitle)) + } ) } } @@ -183,17 +198,26 @@ fun RoomDetailsView( state.eventSink(RoomDetailsEvent.SetFavorite(it)) } ) + if (state.canShowSecurityAndPrivacy) { SecurityAndPrivacyItem( onClick = onSecurityAndPrivacyClick ) } + + state.roomMemberDetailsState?.let { dmMemberDetails -> + ProfileItem( + verificationState = dmMemberDetails.verificationState, + onClick = { onProfileClick(dmMemberDetails.userId) } + ) + } } if (state.roomType is RoomDetailsType.Room) { PreferenceCategory { MembersItem( memberCount = state.memberCount, + hasVerificationViolations = state.hasMemberVerificationViolations, openRoomMemberList = openRoomMemberList, ) if (state.canShowKnockRequests) { @@ -236,9 +260,16 @@ fun RoomDetailsView( } OtherActionsSection( - isDm = state.roomType is RoomDetailsType.Dm, - onLeaveRoom = { state.eventSink(RoomDetailsEvent.LeaveRoom) } + canReportRoom = state.canReportRoom, + onReportRoomClick = onReportRoomClick, + onLeaveRoomClick = { state.eventSink(RoomDetailsEvent.LeaveRoom) } ) + + if (state.showDebugInfo) { + DebugInfoSection( + roomId = state.roomId, + ) + } } } } @@ -272,7 +303,7 @@ private fun RoomDetailsTopBar( actions = { if (showEdit) { IconButton(onClick = { showMenu = !showMenu }) { - Icon(Icons.Default.MoreVert, stringResource(id = CommonStrings.a11y_user_menu)) + Icon(CompoundIcons.OverflowVertical(), stringResource(id = CommonStrings.a11y_user_menu)) } DropdownMenu( expanded = showMenu, @@ -359,7 +390,9 @@ private fun RoomHeaderSection( roomName: String, roomAlias: RoomAlias?, heroes: ImmutableList, + isTombstoned: Boolean, openAvatarPreview: (url: String) -> Unit, + onSubtitleClick: (String) -> Unit, ) { Column( modifier = Modifier @@ -367,16 +400,29 @@ private fun RoomHeaderSection( .padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - CompositeAvatar( + Avatar( avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader), - heroes = heroes.map { user -> - user.getAvatarData(size = AvatarSize.RoomHeader) - }.toPersistentList(), + avatarType = AvatarType.Room( + heroes = heroes.map { user -> + user.getAvatarData(size = AvatarSize.RoomHeader) + }.toPersistentList(), + isTombstoned = isTombstoned, + ), + contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_room_avatar) }, modifier = Modifier - .clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) } + .clickable( + enabled = avatarUrl != null, + onClickLabel = stringResource(CommonStrings.action_view), + ) { + openAvatarPreview(avatarUrl!!) + } .testTag(TestTags.roomDetailAvatar) ) - TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value) + TitleAndSubtitle( + title = roomName, + subtitle = roomAlias?.value, + onSubtitleClick = onSubtitleClick, + ) } } @@ -386,6 +432,7 @@ private fun DmHeaderSection( otherMember: RoomMember, roomName: String, openAvatarPreview: (name: String, url: String) -> Unit, + onSubtitleClick: (String) -> Unit, modifier: Modifier = Modifier ) { Column( @@ -403,6 +450,7 @@ private fun DmHeaderSection( TitleAndSubtitle( title = roomName, subtitle = otherMember.userId.value, + onSubtitleClick = onSubtitleClick, ) } } @@ -411,6 +459,7 @@ private fun DmHeaderSection( private fun TitleAndSubtitle( title: String, subtitle: String?, + onSubtitleClick: (String) -> Unit, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Spacer(modifier = Modifier.height(24.dp)) @@ -422,9 +471,10 @@ private fun TitleAndSubtitle( if (subtitle != null) { Spacer(modifier = Modifier.height(6.dp)) Text( + modifier = Modifier.niceClickable { onSubtitleClick(subtitle) }, text = subtitle, style = ElementTheme.typography.fontBodyLgRegular, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, textAlign = TextAlign.Center, ) } @@ -461,14 +511,14 @@ private fun RoomBadge.toMatrixBadgeData(): MatrixBadgeAtom.MatrixBadgeData { MatrixBadgeAtom.MatrixBadgeData( text = stringResource(R.string.screen_room_details_badge_not_encrypted), icon = CompoundIcons.LockOff(), - type = MatrixBadgeAtom.Type.Neutral, + type = MatrixBadgeAtom.Type.Info, ) } RoomBadge.PUBLIC -> { MatrixBadgeAtom.MatrixBadgeData( text = stringResource(R.string.screen_room_details_badge_public), icon = CompoundIcons.Public(), - type = MatrixBadgeAtom.Type.Neutral, + type = MatrixBadgeAtom.Type.Info, ) } } @@ -484,10 +534,14 @@ private fun TopicSection( showTopDivider = false, ) { if (roomTopic is RoomTopicState.CanAddTopic) { - PreferenceText( - title = stringResource(R.string.screen_room_details_add_topic_title), - icon = Icons.Outlined.Add, - onClick = { onActionClick(RoomDetailsAction.AddTopic) }, + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())), + headlineContent = { + Text(stringResource(id = R.string.screen_room_details_add_topic_title)) + }, + onClick = { + onActionClick(RoomDetailsAction.AddTopic) + }, ) } else if (roomTopic is RoomTopicState.ExistingTopic) { ClickableLinkText( @@ -546,15 +600,46 @@ private fun FavoriteItem( ) } +@Composable +private fun ProfileItem( + verificationState: UserProfileVerificationState, + onClick: () -> Unit, +) { + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())), + headlineContent = { Text(stringResource(id = R.string.screen_room_details_profile_row_title)) }, + trailingContent = when (verificationState) { + UserProfileVerificationState.VERIFIED -> ListItemContent.Icon( + iconSource = IconSource.Vector(CompoundIcons.Verified()), + tintColor = ElementTheme.colors.iconSuccessPrimary, + ) + UserProfileVerificationState.VERIFICATION_VIOLATION -> ListItemContent.Icon( + iconSource = IconSource.Vector(CompoundIcons.ErrorSolid()), + tintColor = ElementTheme.colors.iconCriticalPrimary, + ) + else -> null + }, + onClick = onClick, + ) +} + @Composable private fun MembersItem( memberCount: Long, + hasVerificationViolations: Boolean, openRoomMemberList: () -> Unit, ) { ListItem( headlineContent = { Text(stringResource(CommonStrings.common_people)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())), - trailingContent = ListItemContent.Text(memberCount.toString()), + trailingContent = if (hasVerificationViolations) { + ListItemContent.Icon( + iconSource = IconSource.Vector(CompoundIcons.ErrorSolid()), + tintColor = ElementTheme.colors.textCriticalPrimary, + ) + } else { + ListItemContent.Text(memberCount.toString()) + }, onClick = openRoomMemberList, ) } @@ -569,13 +654,13 @@ private fun PinnedMessagesItem( headlineContent = { Text(stringResource(R.string.screen_room_details_pinned_events_row_title)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())), trailingContent = - if (pinnedMessagesCount == null) { - ListItemContent.Custom { - CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(24.dp)) - } - } else { - ListItemContent.Text(pinnedMessagesCount.toString()) - }, + if (pinnedMessagesCount == null) { + ListItemContent.Custom { + CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(24.dp)) + } + } else { + ListItemContent.Text(pinnedMessagesCount.toString()) + }, onClick = { analyticsService.captureInteraction(Interaction.Name.PinnedMessageRoomInfoButton) onPinnedMessagesClick() @@ -606,22 +691,56 @@ private fun MediaGalleryItem( } @Composable -private fun OtherActionsSection(isDm: Boolean, onLeaveRoom: () -> Unit) { +private fun OtherActionsSection( + canReportRoom: Boolean, + onReportRoomClick: () -> Unit, + onLeaveRoomClick: () -> Unit, +) { PreferenceCategory(showTopDivider = true) { + if (canReportRoom) { + ListItem( + headlineContent = { + Text(stringResource(CommonStrings.action_report_room)) + }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())), + style = ListItemStyle.Destructive, + onClick = onReportRoomClick, + ) + } ListItem( headlineContent = { - val leaveText = stringResource( - id = if (isDm) { - R.string.screen_room_details_leave_conversation_title - } else { - R.string.screen_room_details_leave_room_title - } - ) - Text(leaveText) + Text(stringResource(CommonStrings.action_leave_room)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Leave())), style = ListItemStyle.Destructive, - onClick = onLeaveRoom, + onClick = onLeaveRoomClick, + ) + } +} + +@Composable +private fun DebugInfoSection(roomId: RoomId) { + val context = LocalContext.current + PreferenceCategory(showTopDivider = true) { + ListItem( + headlineContent = { + Text("Internal room ID") + }, + supportingContent = { + Text( + text = roomId.value, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Code())), + trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Copy())), + onClick = { + context.copyToClipboard( + roomId.value, + context.getString(CommonStrings.common_copied_to_clipboard) + ) + }, ) } } @@ -655,5 +774,7 @@ private fun ContentToPreview(state: RoomDetailsState) { onPinnedMessagesClick = {}, onKnockRequestsClick = {}, onSecurityAndPrivacyClick = {}, + onProfileClick = {}, + onReportRoomClick = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/analytics/AnalyticUtils.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/analytics/AnalyticUtils.kt index 75d193aef41..7e4ae1d7c24 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/analytics/AnalyticUtils.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/analytics/AnalyticUtils.kt @@ -9,7 +9,7 @@ package io.element.android.features.roomdetails.impl.analytics import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.services.analytics.api.AnalyticsService internal fun RoomMember.Role.toAnalyticsMemberRole(): RoomModeration.Role = when (this) { @@ -22,7 +22,7 @@ internal fun analyticsMemberRoleForPowerLevel(powerLevel: Long): RoomModeration. return RoomMember.Role.forPowerLevel(powerLevel).toAnalyticsMemberRole() } -internal fun AnalyticsService.trackPermissionChangeAnalytics(initial: MatrixRoomPowerLevels?, updated: MatrixRoomPowerLevels) { +internal fun AnalyticsService.trackPermissionChangeAnalytics(initial: RoomPowerLevelsValues?, updated: RoomPowerLevelsValues) { if (updated.ban != initial?.ban) { capture(RoomModeration(RoomModeration.Action.ChangePermissionsBanMembers, analyticsMemberRoleForPowerLevel(updated.ban))) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt deleted file mode 100644 index 0361cfb2ca5..00000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.impl.di - -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.RoomScope - -@Module -@ContributesTo(RoomScope::class) -interface RoomDetailsModule { - @Binds - fun bindRoomMembersModerationPresenter(presenter: RoomMembersModerationPresenter): Presenter -} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt index e6473eb4c3b..15afcbc99fa 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt @@ -12,17 +12,21 @@ import dagger.Module import dagger.Provides import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.userprofile.api.UserProfilePresenterFactory +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.room.JoinedRoom @Module @ContributesTo(RoomScope::class) object RoomMemberModule { @Provides fun provideRoomMemberDetailsPresenterFactory( - room: MatrixRoom, + room: JoinedRoom, userProfilePresenterFactory: UserProfilePresenterFactory, + encryptionService: EncryptionService, + clipboardHelper: ClipboardHelper, ): RoomMemberDetailsPresenter.Factory { return object : RoomMemberDetailsPresenter.Factory { override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { @@ -30,6 +34,8 @@ object RoomMemberModule { roomMemberId = roomMemberId, room = room, userProfilePresenterFactory = userProfilePresenterFactory, + encryptionService = encryptionService, + clipboardHelper = clipboardHelper, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt index 30e05101c8c..fb530e601a5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt @@ -24,8 +24,9 @@ import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.ui.media.AvatarAction @@ -43,7 +44,7 @@ import timber.log.Timber import javax.inject.Inject class RoomDetailsEditPresenter @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val mediaPickerProvider: PickerProvider, private val mediaPreProcessor: MediaPreProcessor, private val temporaryUriDeleter: TemporaryUriDeleter, @@ -216,7 +217,7 @@ class RoomDetailsEditPresenter @Inject constructor( } private suspend fun updateAvatar(avatarUri: Uri?): Result { - return runCatching { + return runCatchingExceptions { if (avatarUri != null) { val preprocessed = mediaPreProcessor.process( uri = avatarUri, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt index 46304e7aafd..7ae59febc01 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt @@ -9,6 +9,7 @@ package io.element.android.features.roomdetails.impl.edit import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.core.net.toUri import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.media.AvatarAction @@ -22,11 +23,11 @@ open class RoomDetailsEditStateProvider : PreviewParameterProvider, coroutineDispatchers: CoroutineDispatchers, - private val room: MatrixRoom, + private val room: JoinedRoom, private val presenter: RoomInviteMembersPresenter, private val appErrorStateService: AppErrorStateService, private val analyticsService: AnalyticsService, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt index 5f38e7e4513..caae6ff16db 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt @@ -28,7 +28,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.SearchBar @@ -108,12 +107,7 @@ private fun RoomInviteMembersTopBar( onSubmitClick: () -> Unit, ) { TopAppBar( - title = { - Text( - text = stringResource(R.string.screen_room_details_invite_people_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(R.string.screen_room_details_invite_people_title), navigationIcon = { BackButton(onClick = onBackClick) }, actions = { TextButton( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListDataSource.kt index e7df3749174..ef627a49921 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListDataSource.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListDataSource.kt @@ -9,14 +9,14 @@ package io.element.android.features.roomdetails.impl.members import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.roomMembers import kotlinx.coroutines.withContext import javax.inject.Inject class RoomMemberListDataSource @Inject constructor( - private val room: MatrixRoom, + private val room: BaseRoom, private val coroutineDispatchers: CoroutineDispatchers, ) { suspend fun search(query: String): List = withContext(coroutineDispatchers.io) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index b1eb4713918..3d7fecd0ae6 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -18,6 +18,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UserId import io.element.android.services.analytics.api.AnalyticsService @@ -26,8 +29,9 @@ import io.element.android.services.analytics.api.AnalyticsService class RoomMemberListNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - presenterFactory: RoomMemberListPresenter.Factory, + private val presenter: RoomMemberListPresenter, private val analyticsService: AnalyticsService, + private val roomMemberModerationRenderer: RoomMemberModerationRenderer, ) : Node(buildContext, plugins = plugins), RoomMemberListNavigator { interface Callback : Plugin { fun openRoomMemberDetails(roomMemberId: UserId) @@ -35,7 +39,6 @@ class RoomMemberListNode @AssistedInject constructor( } private val callbacks = plugins() - private val presenter = presenterFactory.create(this) init { lifecycle.subscribe( @@ -69,6 +72,16 @@ class RoomMemberListNode @AssistedInject constructor( modifier = modifier, navigator = this, ) + roomMemberModerationRenderer.Render( + state = state.moderationState, + onSelectAction = { action, target -> + when (action) { + is ModerationAction.DisplayProfile -> openRoomMemberDetails(target.userId) + else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) + } + }, + modifier = Modifier, + ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 4e9ceb5bf30..56268e69520 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -12,39 +12,45 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.EncryptionService +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.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.ui.room.canInviteAsState +import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext +import javax.inject.Inject -class RoomMemberListPresenter @AssistedInject constructor( - private val room: MatrixRoom, +class RoomMemberListPresenter @Inject constructor( + private val room: JoinedRoom, private val roomMemberListDataSource: RoomMemberListDataSource, private val coroutineDispatchers: CoroutineDispatchers, - private val roomMembersModerationPresenter: Presenter, - @Assisted private val navigator: RoomMemberListNavigator, + private val roomMembersModerationPresenter: Presenter, + private val encryptionService: EncryptionService, ) : Presenter { - @AssistedFactory - interface Factory { - fun create(navigator: RoomMemberListNavigator): RoomMemberListPresenter - } - @Composable override fun present(): RoomMemberListState { var roomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } @@ -57,20 +63,27 @@ class RoomMemberListPresenter @AssistedInject constructor( val membersState by room.membersStateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canInvite by room.canInviteAsState(syncUpdateFlow.value) - val roomModerationState = roomMembersModerationPresenter.present() + val roomMemberIdentityStates by produceState(persistentMapOf()) { + room.roomMemberIdentityStateChange(waitForEncryption = true) + .onEach { identities -> + value = identities.associateBy({ it.identityRoomMember.userId }, { it.identityState }).toPersistentMap() + } + .launchIn(this) + } + // Ensure we load the latest data when entering this screen LaunchedEffect(Unit) { room.updateMembers() } - LaunchedEffect(membersState) { - if (membersState is MatrixRoomMembersState.Unknown) { + LaunchedEffect(membersState, roomMemberIdentityStates) { + if (membersState is RoomMembersState.Unknown) { return@LaunchedEffect } val finalMembersState = membersState - if (finalMembersState is MatrixRoomMembersState.Error && finalMembersState.roomMembers().orEmpty().isEmpty()) { + if (finalMembersState is RoomMembersState.Error && finalMembersState.roomMembers().orEmpty().isEmpty()) { // Cannot fetch members and no cached members, display the error roomMembers = AsyncData.Failure(finalMembersState.failure) return@LaunchedEffect @@ -84,13 +97,19 @@ class RoomMemberListPresenter @AssistedInject constructor( return@withContext } val result = RoomMembers( - invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(), + invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()) + .map { it.withIdentityState(roomMemberIdentityStates) } + .toImmutableList(), joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList()) .sortedWith(PowerLevelRoomMemberComparator()) + .map { it.withIdentityState(roomMemberIdentityStates) } + .toImmutableList(), + banned = members.getOrDefault(RoomMembershipState.BAN, emptyList()) + .sortedBy { it.userId.value } + .map { it.withIdentityState(roomMemberIdentityStates) } .toImmutableList(), - banned = members.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(), ) - roomMembers = if (membersState is MatrixRoomMembersState.Pending) { + roomMembers = if (membersState is RoomMembersState.Pending) { AsyncData.Loading(result) } else { AsyncData.Success(result) @@ -108,14 +127,20 @@ class RoomMemberListPresenter @AssistedInject constructor( SearchBarResultState.NoResultsFound() } else { val result = RoomMembers( - invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(), + invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()) + .map { it.withIdentityState(roomMemberIdentityStates) } + .toImmutableList(), joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()) .sortedWith(PowerLevelRoomMemberComparator()) + .map { it.withIdentityState(roomMemberIdentityStates) } + .toImmutableList(), + banned = results.getOrDefault(RoomMembershipState.BAN, emptyList()) + .sortedBy { it.userId.value } + .map { it.withIdentityState(roomMemberIdentityStates) } .toImmutableList(), - banned = results.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(), ) SearchBarResultState.Results( - if (membersState is MatrixRoomMembersState.Pending) { + if (membersState is RoomMembersState.Pending) { AsyncData.Loading(result) } else { AsyncData.Success(result) @@ -131,10 +156,10 @@ class RoomMemberListPresenter @AssistedInject constructor( is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.RoomMemberSelected -> - if (roomModerationState.canDisplayModerationActions) { - roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember)) + if (event.roomMember.membership == RoomMembershipState.BAN) { + roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser, event.roomMember.toMatrixUser())) } else { - navigator.openRoomMemberDetails(event.roomMember.userId) + roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser())) } } } @@ -149,4 +174,13 @@ class RoomMemberListPresenter @AssistedInject constructor( eventSink = { handleEvents(it) }, ) } + + private suspend fun RoomMember.withIdentityState(identityStates: ImmutableMap): RoomMemberWithIdentityState { + return if (room.info().isEncrypted != true) { + RoomMemberWithIdentityState(this, null) + } else { + val identityState = identityStates[userId] ?: encryptionService.getUserIdentity(userId).getOrNull() + RoomMemberWithIdentityState(this, identityState) + } + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index c552dda52e0..8e492e53714 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -7,9 +7,10 @@ package io.element.android.features.roomdetails.impl.members -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.collections.immutable.ImmutableList @@ -19,12 +20,17 @@ data class RoomMemberListState( val searchResults: SearchBarResultState>, val isSearchActive: Boolean, val canInvite: Boolean, - val moderationState: RoomMembersModerationState, + val moderationState: RoomMemberModerationState, val eventSink: (RoomMemberListEvents) -> Unit, ) data class RoomMembers( - val invited: ImmutableList, - val joined: ImmutableList, - val banned: ImmutableList, + val invited: ImmutableList, + val joined: ImmutableList, + val banned: ImmutableList, +) + +data class RoomMemberWithIdentityState( + val roomMember: RoomMember, + val identityState: IdentityState?, ) 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 499c8290b88..09a7d9c910b 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 @@ -8,11 +8,12 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState -import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData 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.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState import kotlinx.collections.immutable.persistentListOf @@ -23,8 +24,21 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider = AsyncData.Loading(), searchResults: SearchBarResultState> = SearchBarResultState.Initial(), - moderationState: RoomMembersModerationState = aRoomMembersModerationState(), + moderationState: RoomMemberModerationState = aRoomMemberModerationState(), ) = RoomMemberListState( roomMembers = roomMembers, searchQuery = "", @@ -116,6 +130,17 @@ internal fun aRoomMemberListState( eventSink = {} ) +fun aRoomMemberModerationState( + canBan: Boolean = false, + canKick: Boolean = false, +): RoomMemberModerationState { + return object : RoomMemberModerationState { + override val canKick: Boolean = canKick + override val canBan: Boolean = canBan + override val eventSink: (RoomMemberModerationEvents) -> Unit = {} + } +} + fun aRoomMember( userId: UserId = UserId("@alice:server.org"), displayName: String? = null, @@ -126,6 +151,7 @@ fun aRoomMember( normalizedPowerLevel: Long = 0L, isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.USER, + membershipChangeReason: String? = null, ) = RoomMember( userId = userId, displayName = displayName, @@ -136,6 +162,7 @@ fun aRoomMember( normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, role = role, + membershipChangeReason = membershipChangeReason, ) fun aRoomMemberList() = persistentListOf( @@ -157,3 +184,5 @@ fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Rol fun aVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE) fun aWalter() = aRoomMember(UserId("@walter:server.org"), "Walter", membership = RoomMembershipState.INVITE) + +private fun RoomMember.withIdentity(identityState: IdentityState? = null) = RoomMemberWithIdentityState(this, identityState) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 5c17b93b8cf..443a40c6a2d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -12,21 +12,21 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.ExperimentalFoundationApi 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.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect @@ -42,14 +42,14 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.roomdetails.impl.R -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.SearchBar @@ -58,7 +58,9 @@ import io.element.android.libraries.designsystem.theme.components.SegmentedButto import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.ui.strings.CommonStrings @@ -94,7 +96,7 @@ fun RoomMemberListView( } ) { padding -> var selectedSection by remember { mutableStateOf(SelectedSection.entries[initialSelectedSectionIndex]) } - if (!state.moderationState.canDisplayBannedUsers && selectedSection == SelectedSection.BANNED) { + if (!state.moderationState.canBan && selectedSection == SelectedSection.BANNED) { SideEffect { selectedSection = SelectedSection.MEMBERS } @@ -122,7 +124,7 @@ fun RoomMemberListView( RoomMemberList( roomMembers = state.roomMembers, showMembersCount = true, - canDisplayBannedUsersControls = state.moderationState.canDisplayBannedUsers, + canDisplayBannedUsersControls = state.moderationState.canBan, selectedSection = selectedSection, onSelectedSectionChange = { selectedSection = it }, onSelectUser = ::onSelectUser, @@ -130,14 +132,8 @@ fun RoomMemberListView( } } } - - RoomMembersModerationView( - state = state.moderationState, - onDisplayMemberProfile = navigator::openRoomMemberDetails - ) } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable private fun RoomMemberList( roomMembers: AsyncData, @@ -269,7 +265,7 @@ private fun LazyListScope.failureItem(failure: Throwable) { private fun LazyListScope.roomMemberListSection( headerText: @Composable (() -> String)?, - members: ImmutableList?, + members: ImmutableList?, onMemberSelected: (RoomMember) -> Unit, ) { headerText?.let { @@ -278,41 +274,70 @@ private fun LazyListScope.roomMemberListSection( modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), text = it(), style = ElementTheme.typography.fontBodyLgRegular, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, ) } } items(members.orEmpty()) { matrixUser -> RoomMemberListItem( modifier = Modifier.fillMaxWidth(), - roomMember = matrixUser, - onClick = { onMemberSelected(matrixUser) } + roomMemberWithIdentity = matrixUser, + onClick = { onMemberSelected(matrixUser.roomMember) } ) } } @Composable private fun RoomMemberListItem( - roomMember: RoomMember, + roomMemberWithIdentity: RoomMemberWithIdentityState, onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val roleText = when (roomMember.role) { + val roleText = when (roomMemberWithIdentity.roomMember.role) { RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_member_list_role_administrator) RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_member_list_role_moderator) RoomMember.Role.USER -> null } + MatrixUserRow( modifier = modifier.clickable(onClick = onClick), - matrixUser = roomMember.toMatrixUser(), + matrixUser = roomMemberWithIdentity.roomMember.toMatrixUser(), avatarSize = AvatarSize.UserListItem, - trailingContent = roleText?.let { - @Composable { - Text( - text = it, - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, - ) + trailingContent = { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + when (roomMemberWithIdentity.identityState) { + IdentityState.Verified -> { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.Verified(), + contentDescription = stringResource(CommonStrings.common_verified), + tint = ElementTheme.colors.iconSuccessPrimary + ) + } + IdentityState.VerificationViolation -> { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.ErrorSolid(), + contentDescription = stringResource( + CommonStrings.crypto_identity_change_profile_pin_violation, + roomMemberWithIdentity.roomMember.getBestName() + ), + tint = ElementTheme.colors.iconCriticalPrimary + ) + } + else -> Unit + } + + roleText?.let { + Text( + text = it, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + } } } ) @@ -326,12 +351,7 @@ private fun RoomMemberListTopBar( onInviteClick: () -> Unit, ) { TopAppBar( - title = { - Text( - text = stringResource(CommonStrings.common_people), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(CommonStrings.common_people), navigationIcon = { BackButton(onClick = onBackClick) }, actions = { if (canInvite) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index 7b5ce77f2be..6a1af426946 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -37,7 +37,7 @@ class RoomMemberDetailsNode @AssistedInject constructor( presenterFactory: RoomMemberDetailsPresenter.Factory, ) : Node(buildContext, plugins = plugins) { data class RoomMemberDetailsInput( - val roomMemberId: UserId + val roomMemberId: UserId, ) : NodeInputs private val inputs = inputs() @@ -79,6 +79,7 @@ class RoomMemberDetailsNode @AssistedInject constructor( onOpenDm = ::onStartDM, onStartCall = ::onStartCall, openAvatarPreview = callback::openAvatarPreview, + onVerifyClick = callback::onVerifyUser, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index b2ec81b0c06..d2d5db00e97 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -9,16 +9,33 @@ package io.element.android.features.roomdetails.impl.members.details import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfilePresenterFactory import io.element.android.features.userprofile.api.UserProfileState +import io.element.android.features.userprofile.api.UserProfileVerificationState +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.Presenter +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.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState +import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch /** * Presenter for room member details screen. @@ -26,7 +43,9 @@ import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState */ class RoomMemberDetailsPresenter @AssistedInject constructor( @Assisted private val roomMemberId: UserId, - private val room: MatrixRoom, + private val room: JoinedRoom, + private val encryptionService: EncryptionService, + private val clipboardHelper: ClipboardHelper, userProfilePresenterFactory: UserProfilePresenterFactory, ) : Presenter { interface Factory { @@ -37,6 +56,10 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( @Composable override fun present(): UserProfileState { + val coroutineScope = rememberCoroutineScope() + + val snackbarDispatcher = LocalSnackbarDispatcher.current + val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() val roomMember by room.getRoomMemberAsState(roomMemberId) LaunchedEffect(Unit) { // Update room member info when opening this screen @@ -60,9 +83,52 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( val userProfileState = userProfilePresenter.present() + val identityStateChanges = produceState(initialValue = null) { + // Fetch the initial identity state manually + val identityState = encryptionService.getUserIdentity(roomMemberId).getOrNull() + value = identityState?.let { IdentityStateChange(roomMemberId, it) } + + // Subscribe to the identity changes + room.roomMemberIdentityStateChange(waitForEncryption = false) + .map { it.find { it.identityRoomMember.userId == roomMemberId } } + .map { roomMemberIdentityStateChange -> + // If we didn't receive any info, manually fetch it + roomMemberIdentityStateChange?.identityState ?: encryptionService.getUserIdentity(roomMemberId).getOrNull() + } + .filterNotNull() + .collect { value = IdentityStateChange(roomMemberId, it) } + } + + val verificationState by remember { + derivedStateOf { + when (identityStateChanges.value?.identityState) { + IdentityState.VerificationViolation -> UserProfileVerificationState.VERIFICATION_VIOLATION + IdentityState.Verified -> UserProfileVerificationState.VERIFIED + IdentityState.Pinned, IdentityState.PinViolation -> UserProfileVerificationState.UNVERIFIED + else -> UserProfileVerificationState.UNKNOWN + } + } + } + + fun eventSink(event: UserProfileEvents) { + when (event) { + UserProfileEvents.WithdrawVerification -> coroutineScope.launch { + encryptionService.withdrawVerification(roomMemberId) + } + is UserProfileEvents.CopyToClipboard -> { + clipboardHelper.copyPlainText(event.text) + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) + } + else -> userProfileState.eventSink(event) + } + } + return userProfileState.copy( userName = roomUserName ?: userProfileState.userName, avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl, + verificationState = verificationState, + snackbarMessage = snackbarMessage, + eventSink = ::eventSink ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt deleted file mode 100644 index b561dc9ac7d..00000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.room.RoomMember - -data class ConfirmingRoomMemberAction( - val roomMember: RoomMember, -) : AsyncAction.Confirming diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt deleted file mode 100644 index 824c239a91c..00000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.RoomMember - -sealed interface RoomMembersModerationEvents { - data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents - data object KickUser : RoomMembersModerationEvents - data object BanUser : RoomMembersModerationEvents - data class UnbanUser(val userId: UserId) : RoomMembersModerationEvents - data object Reset : RoomMembersModerationEvents -} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt deleted file mode 100644 index cbd9d3aca08..00000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import androidx.compose.runtime.Composable -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 -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import im.vector.app.features.analytics.plan.RoomModeration -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.coroutine.CoroutineDispatchers -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipState -import io.element.android.libraries.matrix.ui.room.canBanAsState -import io.element.android.libraries.matrix.ui.room.canKickAsState -import io.element.android.libraries.matrix.ui.room.isDmAsState -import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState -import io.element.android.services.analytics.api.AnalyticsService -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.launch -import javax.inject.Inject - -class RoomMembersModerationPresenter @Inject constructor( - private val room: MatrixRoom, - private val dispatchers: CoroutineDispatchers, - private val analyticsService: AnalyticsService, -) : Presenter { - private var selectedMember by mutableStateOf(null) - - @Composable - override fun present(): RoomMembersModerationState { - val coroutineScope = rememberCoroutineScope() - val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val canBan by room.canBanAsState(syncUpdateFlow.value) - val canKick by room.canKickAsState(syncUpdateFlow.value) - val isDm by room.isDmAsState(syncUpdateFlow.value) - val currentUserMemberPowerLevel by room.userPowerLevelAsState(syncUpdateFlow.value) - - val canDisplayModerationActions by remember { - derivedStateOf { !isDm && (canBan || canKick) } - } - val canDisplayBannedUsers by remember { - derivedStateOf { !isDm && canBan } - } - val moderationActions by remember { - derivedStateOf { - buildList { - selectedMember?.let { roomMember -> - add(ModerationAction.DisplayProfile(roomMember.userId)) - if (currentUserMemberPowerLevel > roomMember.powerLevel) { - if (canKick) { - add(ModerationAction.KickUser(roomMember.userId)) - } - if (canBan) { - add(ModerationAction.BanUser(roomMember.userId)) - } - } - } - }.toPersistentList() - } - } - - val kickUserAsyncAction = - remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - val banUserAsyncAction = - remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - val unbanUserAsyncAction = - remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - - fun handleEvent(event: RoomMembersModerationEvents) { - when (event) { - is RoomMembersModerationEvents.SelectRoomMember -> { - if (event.roomMember.membership == RoomMembershipState.BAN && canBan) { - // In this case the view will render a dialog to confirm the unbanning of the user - unbanUserAsyncAction.value = ConfirmingRoomMemberAction(event.roomMember) - } else { - // In this case the view will render a bottom sheet. - selectedMember = event.roomMember - } - } - is RoomMembersModerationEvents.KickUser -> { - selectedMember?.let { - coroutineScope.kickUser(it.userId, kickUserAsyncAction) - } - selectedMember = null - } - is RoomMembersModerationEvents.BanUser -> { - if (banUserAsyncAction.value.isConfirming()) { - selectedMember?.let { - coroutineScope.banUser(it.userId, banUserAsyncAction) - } - selectedMember = null - } else { - banUserAsyncAction.value = AsyncAction.ConfirmingNoParams - } - } - is RoomMembersModerationEvents.UnbanUser -> { - // We are already confirming when we are reaching this point - coroutineScope.unbanUser(event.userId, unbanUserAsyncAction) - } - is RoomMembersModerationEvents.Reset -> { - selectedMember = null - kickUserAsyncAction.value = AsyncAction.Uninitialized - banUserAsyncAction.value = AsyncAction.Uninitialized - unbanUserAsyncAction.value = AsyncAction.Uninitialized - } - } - } - - return RoomMembersModerationState( - canDisplayModerationActions = canDisplayModerationActions, - selectedRoomMember = selectedMember, - actions = moderationActions, - kickUserAsyncAction = kickUserAsyncAction.value, - banUserAsyncAction = banUserAsyncAction.value, - unbanUserAsyncAction = unbanUserAsyncAction.value, - canDisplayBannedUsers = canDisplayBannedUsers, - eventSink = { handleEvent(it) }, - ) - } - - private fun CoroutineScope.kickUser( - userId: UserId, - kickUserAction: MutableState>, - ) = runActionAndWaitForMembershipChange(kickUserAction) { - analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember)) - room.kickUser(userId) - } - - private fun CoroutineScope.banUser( - userId: UserId, - banUserAction: MutableState>, - ) = runActionAndWaitForMembershipChange(banUserAction) { - analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember)) - room.banUser(userId) - } - - private fun CoroutineScope.unbanUser( - userId: UserId, - unbanUserAction: MutableState>, - ) = runActionAndWaitForMembershipChange(unbanUserAction) { - analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember)) - room.unbanUser(userId) - } - - private fun CoroutineScope.runActionAndWaitForMembershipChange( - action: MutableState>, - block: suspend () -> Result - ) { - launch(dispatchers.io) { - action.runUpdatingState { - val result = block() - if (result.isSuccess) { - room.membersStateFlow.drop(1).take(1) - } - result - } - } - } -} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt deleted file mode 100644 index a6c9a3e7042..00000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.RoomMember -import kotlinx.collections.immutable.ImmutableList - -data class RoomMembersModerationState( - val canDisplayModerationActions: Boolean, - val selectedRoomMember: RoomMember?, - val actions: ImmutableList, - val kickUserAsyncAction: AsyncAction, - val banUserAsyncAction: AsyncAction, - val unbanUserAsyncAction: AsyncAction, - val canDisplayBannedUsers: Boolean, - val eventSink: (RoomMembersModerationEvents) -> Unit, -) - -sealed interface ModerationAction { - data class DisplayProfile(val userId: UserId) : ModerationAction - data class KickUser(val userId: UserId) : ModerationAction - data class BanUser(val userId: UserId) : ModerationAction -} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt deleted file mode 100644 index 41e61d8351b..00000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.roomdetails.impl.members.anAlice -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.room.RoomMember -import kotlinx.collections.immutable.toPersistentList - -class RoomMembersModerationStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - actions = listOf( - ModerationAction.DisplayProfile(anAlice().userId), - ), - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - actions = listOf( - ModerationAction.DisplayProfile(anAlice().userId), - ModerationAction.KickUser(userId = anAlice().userId), - ), - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - actions = listOf( - ModerationAction.DisplayProfile(anAlice().userId), - ModerationAction.KickUser(userId = anAlice().userId), - ModerationAction.BanUser(userId = anAlice().userId), - ), - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - kickUserAsyncAction = AsyncAction.Loading, - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - banUserAsyncAction = AsyncAction.Loading, - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - unbanUserAsyncAction = AsyncAction.Loading, - ), - aRoomMembersModerationState( - kickUserAsyncAction = AsyncAction.Failure(Exception("Failed to kick user")), - banUserAsyncAction = AsyncAction.Failure(Exception("Failed to ban user")), - unbanUserAsyncAction = AsyncAction.Failure(Exception("Failed to unban user")), - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - banUserAsyncAction = AsyncAction.ConfirmingNoParams, - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - unbanUserAsyncAction = ConfirmingRoomMemberAction(anAlice()), - ), - aRoomMembersModerationState( - kickUserAsyncAction = AsyncAction.Success(Unit), - banUserAsyncAction = AsyncAction.Success(Unit), - unbanUserAsyncAction = AsyncAction.Success(Unit), - ), - ) -} - -fun aRoomMembersModerationState( - canDisplayModerationActions: Boolean = false, - selectedRoomMember: RoomMember? = null, - actions: List = emptyList(), - kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, - banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, - unbanUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, - canDisplayBannedUsers: Boolean = false, - eventSink: (RoomMembersModerationEvents) -> Unit = {}, -) = RoomMembersModerationState( - canDisplayModerationActions = canDisplayModerationActions, - selectedRoomMember = selectedRoomMember, - actions = actions.toPersistentList(), - kickUserAsyncAction = kickUserAsyncAction, - banUserAsyncAction = banUserAsyncAction, - unbanUserAsyncAction = unbanUserAsyncAction, - canDisplayBannedUsers = canDisplayBannedUsers, - eventSink = eventSink, -) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt deleted file mode 100644 index 256deee3c44..00000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope -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.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.features.roomdetails.impl.R -import io.element.android.libraries.architecture.AsyncAction -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.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.designsystem.components.list.ListItemContent -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.IconSource -import io.element.android.libraries.designsystem.theme.components.ListItem -import io.element.android.libraries.designsystem.theme.components.ListItemStyle -import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet -import io.element.android.libraries.designsystem.theme.components.Text -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.getBestName -import io.element.android.libraries.matrix.ui.model.getAvatarData -import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.launch -import timber.log.Timber - -@Composable -fun RoomMembersModerationView( - state: RoomMembersModerationState, - onDisplayMemberProfile: (UserId) -> Unit, - modifier: Modifier = Modifier, -) { - Box(modifier = modifier) { - if (state.selectedRoomMember != null && state.actions.isNotEmpty()) { - RoomMemberActionsBottomSheet( - roomMember = state.selectedRoomMember, - actions = state.actions, - onSelectAction = { action -> - when (action) { - is ModerationAction.DisplayProfile -> { - onDisplayMemberProfile(action.userId) - } - is ModerationAction.KickUser -> { - state.eventSink(RoomMembersModerationEvents.KickUser) - } - is ModerationAction.BanUser -> { - state.eventSink(RoomMembersModerationEvents.BanUser) - } - } - }, - onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) }, - ) - } - - val asyncIndicatorState = rememberAsyncIndicatorState() - AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState) - - when (val action = state.kickUserAsyncAction) { - is AsyncAction.Loading -> { - LaunchedEffect(action) { - val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty() - asyncIndicatorState.enqueue { - AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_removing_user, userDisplayName)) - } - } - } - is AsyncAction.Failure -> { - Timber.e(action.error, "Failed to kick user.") - LaunchedEffect(action) { - asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { - AsyncIndicator.Failure( - text = stringResource(CommonStrings.common_failed), - ) - } - } - } - is AsyncAction.Success -> { - LaunchedEffect(action) { asyncIndicatorState.clear() } - } - else -> Unit - } - - when (val action = state.banUserAsyncAction) { - is AsyncAction.Confirming -> { - ConfirmationDialog( - title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title), - content = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description), - submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action), - onSubmitClick = { state.eventSink(RoomMembersModerationEvents.BanUser) }, - onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) } - ) - } - is AsyncAction.Loading -> { - LaunchedEffect(action) { - val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty() - asyncIndicatorState.enqueue { - AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_banning_user, userDisplayName)) - } - } - } - is AsyncAction.Failure -> { - Timber.e(action.error, "Failed to ban user.") - LaunchedEffect(action) { - asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { - AsyncIndicator.Failure( - text = stringResource(CommonStrings.common_failed), - ) - } - } - } - is AsyncAction.Success -> { - LaunchedEffect(action) { asyncIndicatorState.clear() } - } - else -> Unit - } - - when (val action = state.unbanUserAsyncAction) { - is AsyncAction.Confirming -> { - if (action is ConfirmingRoomMemberAction) { - ConfirmationDialog( - title = stringResource(R.string.screen_room_member_list_manage_member_unban_title), - content = stringResource(R.string.screen_room_member_list_manage_member_unban_message), - submitText = stringResource(R.string.screen_room_member_list_manage_member_unban_action), - onSubmitClick = { - val userDisplayName = action.roomMember.getBestName() - asyncIndicatorState.enqueue { - AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName)) - } - state.eventSink(RoomMembersModerationEvents.UnbanUser(action.roomMember.userId)) - }, - onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) }, - ) - } - } - is AsyncAction.Failure -> { - Timber.e(action.error, "Failed to unban user.") - LaunchedEffect(action) { - asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { - AsyncIndicator.Failure( - text = stringResource(CommonStrings.common_failed), - ) - } - } - } - is AsyncAction.Success -> { - LaunchedEffect(action) { asyncIndicatorState.clear() } - } - is AsyncAction.Loading, - AsyncAction.Uninitialized -> Unit - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun RoomMemberActionsBottomSheet( - roomMember: RoomMember, - actions: ImmutableList, - onSelectAction: (ModerationAction) -> Unit, - onDismiss: () -> Unit, -) { - val coroutineScope = rememberCoroutineScope() - val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - ModalBottomSheet( - modifier = Modifier.systemBarsPadding(), - sheetState = bottomSheetState, - onDismissRequest = { - coroutineScope.launch { - bottomSheetState.hide() - onDismiss() - } - }, - ) { - Column( - modifier = Modifier.padding(vertical = 16.dp) - ) { - Avatar( - avatarData = roomMember.getAvatarData(size = AvatarSize.RoomListManageUser), - modifier = Modifier - .padding(bottom = 28.dp) - .align(Alignment.CenterHorizontally) - ) - roomMember.displayName?.let { - Text( - text = it, - style = ElementTheme.typography.fontHeadingLgBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .fillMaxWidth() - ) - } - Text( - text = roomMember.userId.toString(), - style = ElementTheme.typography.fontBodyLgRegular, - color = ElementTheme.colors.textSecondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - ) - Spacer(modifier = Modifier.height(32.dp)) - - for (action in actions) { - when (action) { - is ModerationAction.DisplayProfile -> { - ListItem( - headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_user_info)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())), - onClick = { - coroutineScope.launch { - onSelectAction(action) - bottomSheetState.hide() - } - } - ) - } - is ModerationAction.KickUser -> { - ListItem( - headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_remove)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), - onClick = { - coroutineScope.launch { - bottomSheetState.hide() - onSelectAction(action) - } - } - ) - } - is ModerationAction.BanUser -> { - ListItem( - headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), - style = ListItemStyle.Destructive, - onClick = { - coroutineScope.launch { - bottomSheetState.hide() - onSelectAction(action) - } - } - ) - } - } - } - } - } -} - -@PreviewsDayNight -@Composable -internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMembersModerationStateProvider::class) state: RoomMembersModerationState) { - ElementPreview { - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp) - ) { - RoomMembersModerationView( - state = state, - onDisplayMemberProfile = {}, - ) - } - } -} 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 77bdafe94a8..fb7a3b03da7 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 @@ -12,6 +12,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable @@ -25,7 +26,7 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.coroutine.suspendWithMinimumDuration import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings import kotlinx.coroutines.CoroutineScope @@ -37,7 +38,7 @@ import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds class RoomNotificationSettingsPresenter @AssistedInject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val notificationSettingsService: NotificationSettingsService, @Assisted private val showUserDefinedSettingStyle: Boolean, ) : Presenter { @@ -78,11 +79,23 @@ class RoomNotificationSettingsPresenter @AssistedInject constructor( mutableStateOf(null) } + val displayName by produceState(room.info().name) { + room.roomInfoFlow.collect { value = it.name } + } + + val isRoomEncrypted by produceState(room.info().isEncrypted) { + room.roomInfoFlow.collect { value = it.isEncrypted } + } + LaunchedEffect(Unit) { getDefaultRoomNotificationMode(defaultRoomNotificationMode) fetchNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings) observeNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings) - shouldDisplayMentionsOnlyDisclaimer = room.isEncrypted && !notificationSettingsService.canHomeServerPushEncryptedEventsToDevice().getOrDefault(true) + } + + LaunchedEffect(isRoomEncrypted) { + shouldDisplayMentionsOnlyDisclaimer = isRoomEncrypted == true && + !notificationSettingsService.canHomeServerPushEncryptedEventsToDevice().getOrDefault(true) } fun handleEvents(event: RoomNotificationSettingsEvents) { @@ -113,7 +126,7 @@ class RoomNotificationSettingsPresenter @AssistedInject constructor( return RoomNotificationSettingsState( showUserDefinedSettingStyle = showUserDefinedSettingStyle, - roomName = room.displayName, + roomName = displayName.orEmpty(), roomNotificationSettings = roomNotificationSettings.value, pendingRoomNotificationMode = pendingRoomNotificationMode.value, pendingSetDefault = pendingSetDefault.value, @@ -143,16 +156,18 @@ class RoomNotificationSettingsPresenter @AssistedInject constructor( roomNotificationSettings: MutableState> ) = launch { suspend { + val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() pendingModeState.value = null - notificationSettingsService.getRoomNotificationSettings(room.roomId, room.isEncrypted, room.isOneToOne).getOrThrow() + notificationSettingsService.getRoomNotificationSettings(room.roomId, isEncrypted, room.isOneToOne).getOrThrow() }.runCatchingUpdatingState(roomNotificationSettings) } private fun CoroutineScope.getDefaultRoomNotificationMode( defaultRoomNotificationMode: MutableState ) = launch { + val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( - room.isEncrypted, + isEncrypted, room.isOneToOne ).getOrThrow() } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt index 7df66ad9db2..18e95f6ab73 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt @@ -19,9 +19,9 @@ internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider< aRoomNotificationSettingsState(), aRoomNotificationSettingsState(isDefault = false), aRoomNotificationSettingsState(setNotificationSettingAction = AsyncAction.Loading), - aRoomNotificationSettingsState(setNotificationSettingAction = AsyncAction.Failure(Throwable("error"))), + aRoomNotificationSettingsState(setNotificationSettingAction = AsyncAction.Failure(RuntimeException("error"))), aRoomNotificationSettingsState(restoreDefaultAction = AsyncAction.Loading), - aRoomNotificationSettingsState(restoreDefaultAction = AsyncAction.Failure(Throwable("error"))), + aRoomNotificationSettingsState(restoreDefaultAction = AsyncAction.Failure(RuntimeException("error"))), aRoomNotificationSettingsState(displayMentionsOnlyDisclaimer = true) ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt index 1eda6dc582c..bcd2e59d3bc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -31,9 +30,7 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart -import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.ui.strings.CommonStrings @@ -111,7 +108,7 @@ private fun RoomSpecificNotificationSettingsView( .padding(start = 16.dp, bottom = 16.dp, end = 16.dp), style = ElementTheme.typography.fontBodyMdRegular .copy( - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, ) ) if (state.defaultRoomNotificationMode != null) { @@ -169,12 +166,7 @@ private fun RoomNotificationSettingsTopBar( onBackClick: () -> Unit, ) { TopAppBar( - title = { - Text( - text = stringResource(R.string.screen_room_details_notification_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(R.string.screen_room_details_notification_title), navigationIcon = { BackButton(onClick = onBackClick) }, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt index 95a233b0e74..00c545b8536 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt @@ -96,11 +96,7 @@ private fun UserDefinedRoomNotificationSettingsTopBar( onBackClick: () -> Unit, ) { TopAppBar( - title = { - Text( - text = roomName, - ) - }, + titleStr = roomName, navigationIcon = { BackButton(onClick = onBackClick) }, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt index cf6b6cb5222..8b96bcda272 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt @@ -19,7 +19,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter @@ -32,7 +32,7 @@ class RolesAndPermissionsNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: RolesAndPermissionsPresenter, - private val room: MatrixRoom, + private val room: BaseRoom, ) : Node(buildContext, plugins = plugins), RolesAndPermissionsNavigator { interface Callback : Plugin, RolesAndPermissionsNavigator { override fun openAdminList() @@ -59,7 +59,7 @@ class RolesAndPermissionsNode @AssistedInject constructor( lifecycleScope.launch { room.roomInfoFlow .filter { info -> - info.userPowerLevels[room.sessionId] != RoomMember.Role.ADMIN.powerLevel + info.roomPowerLevels?.users?.get(room.sessionId) != RoomMember.Role.ADMIN.powerLevel } .take(1) .onEach { navigateUp() } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt index 5cd4bd17376..f2de1d2817f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt @@ -21,8 +21,8 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.activeRoomMembers import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange @@ -32,14 +32,14 @@ import kotlinx.coroutines.launch import javax.inject.Inject class RolesAndPermissionsPresenter @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, ) : Presenter { @Composable override fun present(): RolesAndPermissionsState { val coroutineScope = rememberCoroutineScope() - val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val roomInfo by room.roomInfoFlow.collectAsState() val roomMembers by room.membersStateFlow.collectAsState() // Get the list of active room members (joined or invited), in order to filter members present in the power // level state Event. @@ -105,13 +105,13 @@ class RolesAndPermissionsPresenter @Inject constructor( ) = launch(dispatchers.io) { runUpdatingState(resetPermissionsAction) { analyticsService.capture(RoomModeration(RoomModeration.Action.ResetPermissions)) - room.resetPowerLevels().map {} + room.resetPowerLevels() } } - private fun MatrixRoomInfo?.userCountWithRole(userIds: List, role: RoomMember.Role): Int { - return this?.userPowerLevels.orEmpty().count { (userId, level) -> + private fun RoomInfo.userCountWithRole(userIds: List, role: RoomMember.Role): Int { + return this.roomPowerLevels?.users?.count { (userId, level) -> RoomMember.Role.forPowerLevel(level) == role && userId in userIds - } + } ?: 0 } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt index 392f9da0961..e3a173f2f24 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt @@ -30,7 +30,7 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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.MatrixRoom +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.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole @@ -50,7 +50,7 @@ import kotlinx.coroutines.launch class ChangeRolesPresenter @AssistedInject constructor( @Assisted private val role: RoomMember.Role, - private val room: MatrixRoom, + private val room: JoinedRoom, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, ) : Presenter { @@ -106,10 +106,10 @@ class ChangeRolesPresenter @AssistedInject constructor( val hasPendingChanges = usersWithRole.value != selectedUsers.value - val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val roomInfo by room.roomInfoFlow.collectAsState() fun canChangeMemberRole(userId: UserId): Boolean { // An admin can't remove or demote another admin - val powerLevel = roomInfo?.userPowerLevels?.get(userId) ?: 0L + val powerLevel = roomInfo.roomPowerLevels?.users?.get(userId) ?: 0L return RoomMember.Role.forPowerLevel(powerLevel) != RoomMember.Role.ADMIN } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt index 4c5a5e9b6b4..a9e321a11f9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt @@ -11,7 +11,6 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -29,7 +28,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -52,12 +50,12 @@ import io.element.android.libraries.designsystem.components.async.rememberAsyncI import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog 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.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Checkbox import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.SearchBar @@ -65,6 +63,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul 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.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -96,16 +95,10 @@ fun ChangeRolesView( topBar = { AnimatedVisibility(visible = !state.isSearchActive) { TopAppBar( - title = { - val title = when (state.role) { - RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_change_role_administrators_title) - RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_change_role_moderators_title) - RoomMember.Role.USER -> error("This should never be reached") - } - Text( - text = title, - style = ElementTheme.typography.aliasScreenTitle, - ) + titleStr = when (state.role) { + RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_change_role_administrators_title) + RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_change_role_moderators_title) + RoomMember.Role.USER -> error("This should never be reached") }, navigationIcon = { BackButton(onClick = { state.eventSink(ChangeRolesEvent.Exit) }) @@ -225,7 +218,6 @@ fun ChangeRolesView( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun SearchResultsList( currentRole: RoomMember.Role, @@ -343,7 +335,10 @@ private fun MemberRow( .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically ) { - Avatar(avatarData) + Avatar( + avatarData = avatarData, + avatarType = AvatarType.User, + ) Column( modifier = Modifier .padding(start = 12.dp) @@ -356,7 +351,7 @@ private fun MemberRow( text = name, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontBodyLgRegular, ) // Invitation pending marker @@ -365,7 +360,7 @@ private fun MemberRow( modifier = Modifier.padding(start = 8.dp), text = stringResource(id = R.string.screen_room_member_list_pending_header_title), style = ElementTheme.typography.fontBodySmRegular.copy(fontStyle = FontStyle.Italic), - color = MaterialTheme.colorScheme.secondary + color = ElementTheme.colors.textSecondary ) } } @@ -373,7 +368,7 @@ private fun MemberRow( userId?.let { Text( text = userId, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodySmRegular, @@ -398,7 +393,9 @@ internal fun ChangeRolesViewPreview(@PreviewParameter(ChangeRolesStateProvider:: @PreviewsDayNight @Composable internal fun PendingMemberRowWithLongNamePreview() { - ElementPreview { + ElementPreview( + drawableFallbackForImages = CommonDrawables.sample_avatar, + ) { MemberRow( avatarData = AvatarData("userId", "A very long name that should be truncated", "https://example.com/avatar.png", AvatarSize.UserListItem), name = "A very long name that should be truncated", diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt index 0a059b26389..8b6f7efc96c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt @@ -21,8 +21,8 @@ import dagger.assisted.AssistedInject import io.element.android.features.roomdetails.impl.analytics.trackPermissionChangeAnalytics import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -31,7 +31,7 @@ import kotlinx.coroutines.launch class ChangeRoomPermissionsPresenter @AssistedInject constructor( @Assisted private val section: ChangeRoomPermissionsSection, - private val room: MatrixRoom, + private val room: JoinedRoom, private val analyticsService: AnalyticsService, ) : Presenter { companion object { @@ -59,8 +59,8 @@ class ChangeRoomPermissionsPresenter @AssistedInject constructor( private val items: ImmutableList = itemsForSection(section) - private var initialPermissions by mutableStateOf(null) - private var currentPermissions by mutableStateOf(null) + private var initialPermissions by mutableStateOf(null) + private var currentPermissions by mutableStateOf(null) private var saveAction by mutableStateOf>(AsyncAction.Uninitialized) private var confirmExitAction by mutableStateOf>(AsyncAction.Uninitialized) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsState.kt index 69f62e17d94..5e7b77560a9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsState.kt @@ -8,12 +8,12 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import kotlinx.collections.immutable.ImmutableList data class ChangeRoomPermissionsState( val section: ChangeRoomPermissionsSection, - val currentPermissions: MatrixRoomPowerLevels?, + val currentPermissions: RoomPowerLevelsValues?, val items: ImmutableList, val hasChanges: Boolean, val saveAction: AsyncAction, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStateProvider.kt index 54f07bd637b..4210d117c7f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsStateProvider.kt @@ -10,7 +10,7 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions.permiss import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import kotlinx.collections.immutable.toPersistentList class ChangeRoomPermissionsStateProvider : PreviewParameterProvider { @@ -36,7 +36,7 @@ class ChangeRoomPermissionsStateProvider : PreviewParameterProvider = ChangeRoomPermissionsPresenter.itemsForSection(section), hasChanges: Boolean = false, saveAction: AsyncAction = AsyncAction.Uninitialized, @@ -52,8 +52,8 @@ internal fun aChangeRoomPermissionsState( eventSink = eventSink, ) -private fun previewPermissions(): MatrixRoomPowerLevels { - return MatrixRoomPowerLevels( +private fun previewPermissions(): RoomPowerLevelsValues { + return RoomPowerLevelsValues( // MembershipModeration section invite = RoomMember.Role.ADMIN.powerLevel, kick = RoomMember.Role.MODERATOR.powerLevel, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt index d2e9c3f2902..ba8cc7f978b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.core.bool.orFalse @@ -26,7 +25,6 @@ import io.element.android.libraries.designsystem.components.dialogs.Confirmation import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.aliasScreenTitle 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 @@ -36,7 +34,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.ui.strings.CommonStrings @OptIn(ExperimentalMaterial3Api::class) @@ -58,7 +56,7 @@ fun ChangeRoomPermissionsView( ChangeRoomPermissionsSection.MembershipModeration -> stringResource(R.string.screen_room_change_permissions_member_moderation) } TopAppBar( - title = { Text(text = title, style = ElementTheme.typography.aliasScreenTitle) }, + titleStr = title, navigationIcon = { BackButton(onClick = { state.eventSink(ChangeRoomPermissionsEvent.Exit) }) }, @@ -133,7 +131,7 @@ fun ChangeRoomPermissionsView( private fun SelectRoleItem( permissionsItem: RoomPermissionType, role: RoomMember.Role, - currentPermissions: MatrixRoomPowerLevels?, + currentPermissions: RoomPowerLevelsValues?, onClick: (RoomPermissionType, RoomMember.Role) -> Unit ) { val title = when (role) { @@ -153,7 +151,7 @@ private fun SelectRoleItem( ) } -private fun MatrixRoomPowerLevels.isSelected(item: RoomPermissionType, role: RoomMember.Role): Boolean { +private fun RoomPowerLevelsValues.isSelected(item: RoomPermissionType, role: RoomMember.Role): Boolean { return when (item) { RoomPermissionType.BAN -> RoomMember.Role.forPowerLevel(ban) == role RoomPermissionType.INVITE -> RoomMember.Role.forPowerLevel(invite) == role diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt index 769ff87baee..35ecbc0d516 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenter.kt @@ -29,8 +29,8 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.RoomInfo 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.roomdirectory.RoomVisibility @@ -43,7 +43,7 @@ import kotlinx.coroutines.launch class SecurityAndPrivacyPresenter @AssistedInject constructor( @Assisted private val navigator: SecurityAndPrivacyNavigator, private val matrixClient: MatrixClient, - private val room: MatrixRoom, + private val room: JoinedRoom, ) : Presenter { @AssistedFactory interface Factory { @@ -57,7 +57,7 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val homeserverName = remember { matrixClient.userIdServerName() } val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val roomInfo = room.roomInfoFlow.collectAsState(null) + val roomInfo by room.roomInfoFlow.collectAsState() val savedIsVisibleInRoomDirectory = remember { mutableStateOf>(AsyncData.Uninitialized) } LaunchedEffect(Unit) { @@ -66,12 +66,13 @@ class SecurityAndPrivacyPresenter @AssistedInject constructor( val savedSettings by remember { derivedStateOf { + val historyVisibility = roomInfo.historyVisibility.map() SecurityAndPrivacySettings( - roomAccess = roomInfo.value?.joinRule.map(), - isEncrypted = room.isEncrypted, + roomAccess = roomInfo.joinRule.map(), + isEncrypted = roomInfo.isEncrypted == true, isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory.value, - historyVisibility = roomInfo.value?.historyVisibility.map(), - address = roomInfo.value?.firstDisplayableAlias(homeserverName)?.value, + historyVisibility = historyVisibility, + address = roomInfo.firstDisplayableAlias(homeserverName)?.value, ) } } @@ -278,6 +279,6 @@ private fun SecurityAndPrivacyHistoryVisibility.map(): RoomHistoryVisibility { } } -private fun MatrixRoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { +private fun RoomInfo.firstDisplayableAlias(serverName: String): RoomAlias? { return aliases.firstOrNull { it.matchesServer(serverName) } ?: aliases.firstOrNull() } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt index 59dfeeaca01..c46f8a7a6f4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyView.kt @@ -40,7 +40,6 @@ import io.element.android.libraries.designsystem.components.list.ListItemContent 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.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.ListItem @@ -142,12 +141,7 @@ private fun SecurityAndPrivacyToolbar( ) { TopAppBar( modifier = modifier, - title = { - Text( - text = stringResource(R.string.screen_room_details_security_and_privacy_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(R.string.screen_room_details_security_and_privacy_title), navigationIcon = { BackButton(onClick = onBackClick) }, actions = { TextButton( @@ -267,8 +261,7 @@ private fun RoomAddressSection( Text(text = stringResource(R.string.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName)) }, onClick = if (isVisibleInRoomDirectory.isSuccess()) onVisibilityChange else null, - trailingContent = - when (isVisibleInRoomDirectory) { + trailingContent = when (isVisibleInRoomDirectory) { is AsyncData.Uninitialized, is AsyncData.Loading -> { ListItemContent.Custom { CircularProgressIndicator( @@ -288,7 +281,6 @@ private fun RoomAddressSection( is AsyncData.Success -> { ListItemContent.Switch( checked = isVisibleInRoomDirectory.data, - onChange = { onVisibilityChange() }, ) } } @@ -316,7 +308,6 @@ private fun EncryptionSection( trailingContent = ListItemContent.Switch( checked = isRoomEncrypted, enabled = canToggleEncryption, - onChange = { onToggleEncryption() }, ), onClick = if (canToggleEncryption) onToggleEncryption else null ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index 153b751d080..32af99dc1dc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -9,6 +9,8 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroom import androidx.compose.runtime.Composable 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 import androidx.compose.runtime.remember @@ -23,7 +25,8 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.alias.RoomAliasHelper import io.element.android.libraries.matrix.api.roomAliasFromName import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity @@ -34,7 +37,7 @@ import kotlinx.coroutines.launch class EditRoomAddressPresenter @AssistedInject constructor( @Assisted private val navigator: SecurityAndPrivacyNavigator, private val client: MatrixClient, - private val room: MatrixRoom, + private val room: JoinedRoom, private val roomAliasHelper: RoomAliasHelper, ) : Presenter { @AssistedFactory @@ -45,15 +48,16 @@ class EditRoomAddressPresenter @AssistedInject constructor( @Composable override fun present(): EditRoomAddressState { val coroutineScope = rememberCoroutineScope() + val roomInfo by room.roomInfoFlow.collectAsState() val homeserverName = remember { client.userIdServerName() } val roomAddressValidity = remember { mutableStateOf(RoomAddressValidity.Unknown) } - val savedRoomAddress = remember { room.firstAliasMatching(homeserverName)?.addressName() } + val savedRoomAddress by remember { derivedStateOf { roomInfo.firstAliasMatching(homeserverName)?.addressName() } } val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } var newRoomAddress by remember { mutableStateOf( - savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(room.displayName) + savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(roomInfo.name.orEmpty()) ) } @@ -97,9 +101,10 @@ class EditRoomAddressPresenter @AssistedInject constructor( newRoomAddress: String, ) = launch { suspend { - val savedCanonicalAlias = room.canonicalAlias - val savedAliasFromHomeserver = room.firstAliasMatching(serverName) - val newRoomAlias = client.roomAliasFromName(newRoomAddress).getOrThrow() + val roomInfo = room.info() + val savedCanonicalAlias = roomInfo.canonicalAlias + val savedAliasFromHomeserver = roomInfo.firstAliasMatching(serverName) + val newRoomAlias = client.roomAliasFromName(newRoomAddress) ?: throw IllegalArgumentException("Invalid room address") // First publish the new alias in the room directory room.publishRoomAliasInRoomDirectory(newRoomAlias).getOrThrow() @@ -112,7 +117,7 @@ class EditRoomAddressPresenter @AssistedInject constructor( when { // Allow to update the canonical alias only if the saved canonical alias matches the homeserver or if there is no canonical alias savedCanonicalAlias == null || savedCanonicalAlias.matchesServer(serverName) -> { - val newAlternativeAliases = room.alternativeAliases.filter { it != savedAliasFromHomeserver } + val newAlternativeAliases = roomInfo.alternativeAliases.filter { it != savedAliasFromHomeserver } room.updateCanonicalAlias(newRoomAlias, newAlternativeAliases).getOrThrow() } // Otherwise, only update the alternative aliases and keep the current canonical alias @@ -121,7 +126,7 @@ class EditRoomAddressPresenter @AssistedInject constructor( // New alias is added first, so we make sure we pick it first add(newRoomAlias) // Add all other aliases, except the one we just removed from the room directory - addAll(room.alternativeAliases.filter { it != savedAliasFromHomeserver }) + addAll(roomInfo.alternativeAliases.filter { it != savedAliasFromHomeserver }) } room.updateCanonicalAlias(savedCanonicalAlias, newAlternativeAliases).getOrThrow() } @@ -134,7 +139,7 @@ class EditRoomAddressPresenter @AssistedInject constructor( /** * Returns the first alias that matches the given server name, or null if none match. */ -private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? { +private fun RoomInfo.firstAliasMatching(serverName: String): RoomAlias? { // Check if the canonical alias matches the homeserver if (canonicalAlias?.matchesServer(serverName) == true) { return canonicalAlias diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt index d39208ee955..93c2523f656 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt @@ -20,16 +20,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults 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.aliasScreenTitle 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.matrix.ui.room.address.RoomAddressField @@ -99,12 +96,7 @@ private fun EditRoomAddressTopBar( ) { TopAppBar( modifier = modifier, - title = { - Text( - text = stringResource(R.string.screen_edit_room_address_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = stringResource(R.string.screen_edit_room_address_title), navigationIcon = { BackButton(onClick = onBackClick) }, actions = { TextButton( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt index 50a676da1e4..cc052b19b1b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/permissions/SecurityAndPrivacyPermissions.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.produceState import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.SecurityAndPrivacyPermissions.Companion.DEFAULT -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.powerlevels.canSendState @@ -37,7 +37,7 @@ data class SecurityAndPrivacyPermissions( } @Composable -fun MatrixRoom.securityAndPrivacyPermissionsAsState(updateKey: Long): State { +fun BaseRoom.securityAndPrivacyPermissionsAsState(updateKey: Long): State { return produceState(DEFAULT, key1 = updateKey) { value = SecurityAndPrivacyPermissions( canChangeRoomAccess = canSendState(type = StateEventType.ROOM_JOIN_RULES).getOrElse { false }, 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 6ba7e59a2ed..d50b3c5f6e6 100644 --- a/features/roomdetails/impl/src/main/res/values-be/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-be/translations.xml @@ -7,11 +7,11 @@ "Заблакіраваць людзей" "Выдаліць паведамленні" "Усе" - "Запрасіць людзей" + "Запрашайце людзей і прымайце запыты на далучэнне" "Мадэрацыя ўдзельнікаў" "Паведамленні і змест" "Адміністратары і мадэратары" - "Выдаліць людзей" + "Выдаляйце людзей і адхіляйце запыты на далучэнне" "Змяніць аватар пакоя" "Дэталі пакоя" "Змяніць назву пакоя" @@ -60,29 +60,20 @@ "Інфармацыя аб пакоі" "Тэма" "Ідзе абнаўленне пакоя…" - "Заблакіраваць" - "Яны не змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." - "Вы ўпэўнены, што хочаце заблакіраваць гэтага карыстальніка?" "У гэтым пакоі няма заблакіраваных удзельнікаў." - "Блакіроўка %1$s" "%1$d удзельнік" "%1$d удзельнікі" "%1$d удзельнікаў" - "Выдаліць і заблакіраваць удзельніка" - "Выдаліць удзельніка з пакоя" "Выдаліць і заблакіраваць удзельніка" "Толькі выдаліць удзельніка" - "Выдаліць удзельніка і забараніць далучацца ў будучыні?" "Разблакіраваць" "Яны змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." "Разблакіраваць удзельніка" - "Прагляд профілю" "Заблакіраваныя" "Удзельнікі" "У чаканні" - "Выдаленне %1$s …" "Адміністратар" "Мадэратар" "Удзельнікі пакоя" @@ -115,4 +106,7 @@ "Ролі" "Дэталі пакоя" "Ролі і дазволы" + "Папрасіце далучыцца" + "Хто заўгодна" + "Хто заўгодна" diff --git a/features/roomdetails/impl/src/main/res/values-bg/translations.xml b/features/roomdetails/impl/src/main/res/values-bg/translations.xml index e764baee3ca..6f9c2567425 100644 --- a/features/roomdetails/impl/src/main/res/values-bg/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-bg/translations.xml @@ -1,10 +1,32 @@ + "Вашият сървър не поддържа тази опция в шифровани стаи, може да не получавате известия в някои стаи." "Анкети" + "Само администратори" + "Премахване на съобщения" + "Всеки" + "Поканване на хора и приемане на заявки за присъединяване" + "Модериране на членове" + "Съобщения и съдържание" + "Администратори и модератори" + "Премахване на хора и отхвърляне на заявки за присъединяване" + "Подробности за стаята" + "Промяна на името на стаята" + "Промяна на темата на стаята" + "Изпращане на съобщения" + "Редактиране на администраторите" + "Добавяне на администратор?" + "Администраторите автоматично имат модераторски права" + "Редактиране на модераторите" + "Администратори" + "Модератори" "Членове" "Добавяне на тема" "Вече е член" "Вече е бил поканен" + "С шифроване" + "Без шифроване" + "Общодостъпна стая" "Редактиране на стаята" "Не може да се обнови стаята" "Съобщенията са защитени с ключове. Само вие и получателите имате уникалните ключове, за да ги отключите." @@ -12,12 +34,17 @@ "Поканване на хора" "Напускане на разговора" "Напускане на стаята" + "Медия и файлове" "Персонализирани" "По подразбиране" "Известия" + "Закачени съобщения" + "Профил" + "Роли и разрешения" "Име на стаята" "Защита" "Споделяне на стаята" + "Информация за стаята" "Тема" "Обновяване на стаята…" @@ -32,4 +59,19 @@ "Всички съобщения" "Само споменавания и ключови думи" "В тази стая, да бъда известяван за" + "Администратори" + "Промяна на моята роля" + "Модериране на членове" + "Съобщения и съдържание" + "Модератори" + "Разрешения" + "Нулиране на разрешенията" + "Роли" + "Подробности за стаята" + "Роли и разрешения" + "Шифроване" + "Всеки" + "Видима в директорията на обществените стаи" + "Всеки" + "Видимост на стаята" 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 fc5c82860c7..73e51ad0452 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -66,29 +66,20 @@ "Informace o místnosti" "Téma" "Aktualizace místnosti…" - "Vykázat" - "Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni." - "Jste si jisti, že chcete vykázat tohoto člena?" "V této místnosti nejsou žádní vykázaní uživatelé." - "Vykazování %1$s" "%1$d osoba" "%1$d osoby" "%1$d osob" - "Odebrat a vykázat člena" - "Odebrat z místnosti" "Odebrat a vykázat člena" "Pouze odebrat člena" - "Odebrat člena a zakázat mu připojení v budoucnu?" "Zrušit vykázání" "Pokud budou pozváni, budou se moci do této místnosti znovu připojit." "Zrušit vykázání uživatele" - "Zobrazit profil" "Vykázaní" "Členové" "Nevyřízeno" - "Odstraňování %1$s…" "Správce" "Moderátor" "Členové místnosti" @@ -123,7 +114,7 @@ "Role a oprávnění" "Přidat adresu místnosti" "Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout." - "Požádat o vstup" + "Požádat o připojení" "Ano, povolit šifrování" "Po aktivaci nelze šifrování místnosti deaktivovat. Historie zpráv bude viditelná pouze pro členy místnosti od doby, kdy byli pozváni nebo od té doby, co do místnosti vstoupili. Nikdo kromě členů místnosti nebude moci číst zprávy. To může bránit správnému fungování robotů a propojení. diff --git a/features/roomdetails/impl/src/main/res/values-cy/translations.xml b/features/roomdetails/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..ab0a8fa95d6 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,151 @@ + + + "Bydd angen cyfeiriad ystafell arnoch i\'w wneud yn weladwy yn y cyfeiriadur." + "Cyfeiriad yr ystafell" + "Digwyddodd gwall wrth ddiweddaru\'r gosodiad hysbysu." + "Nid yw eich gweinydd cartref yn cefnogi\'r dewis hwn mewn ystafelloedd sydd wedi\'u hamgryptio, efallai fyddwch chi ddim yn cael gwybod mewn rhai ystafelloedd." + "Pleidleisiau" + "Gweinyddwyr yn unig" + "Gwahardd pobl" + "Dileu negeseuon" + "Pawb" + "Gwahodd pobl a derbyn ceisiadau i ymuno" + "Cymedroli aelodau" + "Negeseuon a chynnwys" + "Gweinyddwyr a chymedrolwyr" + "Dileu pobl a gwrthod ceisiadau i ymuno" + "Newid afatar ystafell" + "Manylion yr ystafell" + "Newid enw\'r ystafell" + "Newid pwnc yr ystafell" + "Anfon negeseuon" + "Golygu Gweinyddwyr" + "Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych chi\'n hyrwyddo\'r defnyddiwr i gael yr un lefel pŵer â chi." + "Ychwanegu Gweinyddwr?" + "Gostwng" + "Fyddwch chi ddim yn gallu dadwneud y newid hwn gan eich bod yn israddio eich hun, os mai chi yw\'r defnyddiwr breintiedig olaf yn yr ystafell bydd yn amhosibl adennill breintiau." + "Israddio eich hun?" + "%1$s (Yn aros)" + "Yn aros" + "Mae gan weinyddwyr freintiau cymedrolwr yn awtomatig" + "Golygu Cymedrolwyr" + "Gweinyddwyr" + "Cymedrolwyr" + "Aelodau" + "Mae gennych newidiadau heb eu cadw." + "Cadw\'r newidiadau?" + "Ychwanegu pwnc" + "Eisoes yn aelod" + "Wedi gwahodd yn barod" + "Wedi\'i amgryptio" + "Heb ei amgryptio" + "Ystafell gyhoeddus" + "Ystafell Golygu" + "Roedd gwall anhysbys ac nid oedd modd newid y manylion." + "Methu diweddaru\'r ystafell" + "Negeseuon yn cael eu diogelu gyda chloeon. Dim ond chi a\'r derbynwyr sydd â\'r allweddi unigryw i\'w datgloi." + "Galluogwyd amgryptio neges" + "Digwyddodd gwall wrth lwytho gosodiadau hysbysu." + "Wedi methu tewi\'r ystafell hon, ceisiwch eto." + "Wedi methu dad-dewi\'r ystafell hon, ceisiwch eto." + "Gwahodd pobl" + "Gadael y sgwrs" + "Gadael yr ystafell" + "Cyfryngau a ffeiliau" + "Cyfaddas" + "Rhagosodiad" + "Hysbysiadau" + "Negeseuon wedi\'u pinio" + "Proffil" + "Ceisiadau i ymuno" + "Rolau a chaniatâd" + "Enw\'r ystafell" + "Diogelwch a phreifatrwydd" + "Diogelwch" + "Rhannu ystafell" + "Manylion ystafell" + "Pwnc" + "Wrthi\'n diweddaru ystafell…" + "Nid oes unrhyw ddefnyddwyr gwaharddedig yn yr ystafell hon." + + "%1$d personau" + "%1$d person" + "%1$d berson" + "%1$d person" + "%1$d pherson" + "%1$d person" + + "Gwahardd o ystafell" + "Dileu aelod yn unig" + "Adfer" + "Fyddan nhw yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad." + "Gwahardd defnyddiwr" + "Wedi\'i wahardd" + "Aelodau" + "Dan ystyriaeth" + "Gweinyddwr" + "Cymedrolwr" + "Aelodau\'r ystafell" + "Dad-wahardd %1$s" + "Caniatáu gosodiad personol" + "Bydd troi hwn ymlaen yn diystyru eich gosodiad rhagosodedig" + "Rhowch wybod i mi yn y sgwrs hon am" + "Gallwch ei newid yn eich %1$s." + "gosodiadau cyffredinol" + "Gosodiad rhagosodedig" + "Dileu gosodiad personol" + "Digwyddodd gwall wrth lwytho gosodiadau hysbysu." + "Wedi methu ag adfer y modd rhagosodedig, ceisiwch eto." + "Wedi methu gosod y modd, ceisiwch eto." + "Nid yw eich gweinydd cartref yn cefnogi\'r dewis hwn mewn ystafelloedd sydd wedi\'u hamgryptio, chewch chi ddim eich hysbysu yn yr ystafell hon." + "Pob neges" + "Crybwylliadau ac Allweddeiriau\'n unig" + "Yn yr ystafell hon, rhowch wybod i mi am" + "Gweinyddwyr" + "Newid fy rôl" + "Israddio aelod" + "Israddio cymedrolwr" + "Cymedroli aelodau" + "Negeseuon a chynnwys" + "Cymedrolwyr" + "Caniatâd" + "Ailosod caniatâd" + "Ar ôl i chi ailosod caniatâd, byddwch yn colli\'r gosodiadau cyfredol." + "Ailosod caniatâd?" + "Rolau" + "Manylion yr ystafell" + "Rolau a chaniatâd" + "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" + "Iawn, galluogi amgryptio" + "Unwaith y bydd wedi\'i alluogi, does dim modd analluogi amgryptio ar gyfer ystafell, dim ond ar gyfer aelodau\'r ystafell y bydd hanes neges yn weladwy ers iddyn nhw gael eu gwahodd neu ers iddyn nhw ymuno â\'r ystafell. +Fydd neb ar wahân i aelodau\'r ystafell yn gallu darllen negeseuon. Gall hyn atal botiau a phontydd i weithio\'n gywir. +Nid ydym yn argymell galluogi amgryptio ar gyfer ystafelloedd y gall unrhyw un ddod o hyd iddynt ac ymuno â nhw." + "Galluogi amgryptio?" + "Unwaith y bydd wedi\'i alluogi, does dim modd analluogi amgryptio." + "Amgryptiad" + "Galluogi amgryptio o\'r dechrau i\'r diwedd" + "Gall unrhyw un ddod o hyd iddo ac ymuno" + "Unrhyw un" + "Dim ond os cawn nhw wahoddiad gall pobl ymuno" + "Gwahoddiad yn unig" + "Mynediad ystafell" + "Nid yw gofodau\'n cael eu cefnogi ar hyn o bryd" + "Aelodau gofod" + "Bydd angen cyfeiriad ystafell arnoch er mwyn ei wneud yn weladwy yn y cyfeiriadur ystafelloedd." + "Cyfeiriad yr ystafell" + "Caniatáu i\'r ystafell hon gael ei chanfod trwy chwilio cyfeiriadur ystafelloedd cyhoeddus %1$s" + "Gweladwy yn y cyfeiriadur ystafelloedd cyhoeddus" + "Unrhyw un" + "Pwy all ddarllen hanes" + "Yn aelodau ond dim ond ers cael eu gwahodd" + "Yn aelodau dim ond ers dewis y dewis hwn" + "Mae cyfeiriadau ystafelloedd yn ffyrdd o ddod o hyd i ystafelloedd a chael mynediad iddyn nhw. Mae hyn hefyd yn sicrhau y gallwch chi rannu\'ch ystafell yn hawdd ag eraill. +Gallwch ddewis cyhoeddi eich ystafell yng ngweinydd cartref eich cyfeiriadur ystafelloedd cyhoeddus." + "Cyhoeddi ystafell" + "Mae cyfeiriadau ystafelloedd yn ffyrdd o ddod o hyd i ystafelloedd a chael mynediad iddyn nhw. Mae hyn hefyd yn sicrhau y gallwch chi rannu\'ch ystafell yn hawdd ag eraill. +Mae angen y cyfeiriad hefyd i wneud yr ystafell yn weladwy yng nghyfeiriadur ystafell gyhoeddus %1$s." + "Gwelededd yr ystafell" + "Diogelwch a phreifatrwydd" + diff --git a/features/roomdetails/impl/src/main/res/values-da/translations.xml b/features/roomdetails/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..4ee3a8439a0 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,147 @@ + + + "Du skal bruge en rum-adresse for at gøre den synlig i kataloget." + "Rummets adresse" + "Der opstod en fejl under opdatering af notifikationsindstillingen." + "Din hjemmeserver understøtter ikke denne mulighed i krypterede rum, og derfor er det muligt at du ikke får besked i alle rum." + "Afstemninger" + "Kun admins" + "Spær personer" + "Fjern beskeder" + "Alle" + "Invitér personer og acceptér anmodninger om at deltage" + "Moderation af medlemmer" + "Beskeder og indhold" + "Admins og moderatorer" + "Fjern personer og afvis anmodninger om at deltage" + "Skift rummets avatar" + "Detaljer om rummet" + "Skift rummets navn" + "Skift emne for rummet" + "Send beskeder" + "Redigér admins" + "Du kan ikke fortryde denne handling. Du forfremmer brugeren til at have samme magtniveau som dig." + "Tilføj Admin?" + "Nedgradering" + "Du vil ikke være i stand til at fortryde denne ændring, da du degraderer dig selv. Hvis du er den sidste privilegerede bruger i rummet, vil det være umuligt at genvinde privilegier." + "Nedgrader dig selv?" + "%1$s (Afventer)" + "(Afventer)" + "Administratorer har automatisk moderatorrettigheder" + "Redigér moderatorer" + "Administratorer" + "Moderatorer" + "Medlemmer" + "Du har ændringer, der ikke er gemt." + "Gem ændringer?" + "Tilføj emne" + "Allerede medlem" + "Allerede inviteret" + "Krypteret" + "Ikke krypteret" + "Offentligt rum" + "Rediger rum" + "Der opstod en ukendt fejl, og oplysningerne kunne ikke ændres." + "Rummet kunne ikke opdateres" + "Beskeder er sikret med låse. Kun du og modtagerne har de unikke nøgler til at låse dem op." + "Beskedkryptering aktiveret" + "Der opstod en fejl under indlæsning af notifikationsindstillinger." + "Det lykkedes ikke at slå lyden fra for dette rum. Prøv igen." + "Det lykkedes ikke at slå lyden til igen i dette rum. Prøv igen." + "Invitér folk" + "Forlad samtalen" + "Forlad rum" + "Medier og filer" + "Brugerdefineret" + "Standard" + "Notifikationer" + "Fastgjorte beskeder" + "Profil" + "Anmodninger om at deltage" + "Roller og tilladelser" + "Navn på rum" + "Sikkerhed og privatliv" + "Sikkerhed" + "Del rum" + "Informationer om rummet" + "Emne" + "Opdaterer rum…" + "Der er ingen spærrede brugere i dette rum." + + "%1$d person" + "%1$d personer" + + "Spær fra rum" + "Fjern kun medlem" + "Fjern spærring af" + "De vil være i stand til at deltage i dette rum igen, hvis de inviteres." + "Ophæv blokering af bruger fra rum" + "Spærret" + "Medlemmer" + "Afventer" + "Admin" + "Moderator" + "Medlemmer af rummet" + "Ophæver spærring af %1$s" + "Tillad brugerdefineret indstilling" + "Hvis du aktiverer dette, tilsidesættes din standardindstilling" + "Giv mig besked i denne samtale for" + "Du kan ændre det i din %1$s." + "globale indstillinger" + "Standardindstilling" + "Fjern brugerdefineret indstilling" + "Der opstod en fejl under indlæsning af meddelelsesindstillinger." + "Gendannelse af standardtilstanden mislykkedes. Prøv igen." + "Kunne ikke indstille tilstanden. Prøv igen." + "Din hjemmeserver understøtter ikke denne indstilling i krypterede rum, du får ikke besked i dette rum." + "Alle beskeder" + "Kun omtaler og nøgleord" + "Giv mig besked i dette rum for" + "Administratorer" + "Skift min rolle" + "Nedgrader til medlem" + "Nedgradering til moderator" + "Moderation af medlemmer" + "Beskeder og indhold" + "Moderatorer" + "Tilladelser" + "Nulstil tilladelser" + "Når du nulstiller tilladelserne, mister du de nuværende indstillinger." + "Nulstil tilladelser?" + "Roller" + "Detaljer om rummet" + "Roller og tilladelser" + "Tilføj adresse på rum" + "Alle kan bede om at deltage i lokalet, men en administrator eller moderator skal acceptere anmodningen." + "Spørg om at deltage" + "Ja, aktivér kryptering" + "Når det først er aktiveret, kan kryptering for et rum ikke deaktiveres igen. Beskedhistorik vil kun være synlig for rummedlemmer, siden de blev inviteret, eller siden de blev medlem af rummet. +Ingen udover medlemmer af rummet vil være i stand til at læse beskeder. Dette kan forhindre bots og broer i at fungere korrekt. +Vi anbefaler ikke at aktivere kryptering for rum, som alle kan finde og deltage i." + "Aktivér kryptering?" + "Når kryptering først er aktiveret, kan den ikke deaktiveres igen." + "Kryptering" + "Aktivér end-to-end-kryptering" + "Alle kan finde og deltage" + "Enhver" + "Folk kan kun deltage, hvis de bliver inviteret" + "Kun med invitation" + "Adgang til rummet" + "Klynger understøttes ikke i øjeblikket" + "Medlemmer af klyngen" + "Du skal bruge en adresse til rummet, for at gøre den synlig i rum-registeret." + "Rummets adresse" + "Tillad, at dette rum kan findes ved at søge i %1$s fortegnelse over offentlige rum" + "Synlig i det offentlige register over rum" + "Enhver" + "Hvem kan læse historikken?" + "Kun medlemmer, efter de blev inviteret" + "Kun medlemmer siden valg af denne mulighed" + "Rum-adresser er en måde at finde og få adgang til værelser på. Dette sikrer også, at du nemt kan dele dit rum med andre. +Du kan vælge at offentliggøre dit rum i din hjemmeservers offentlige katalog over rum." + "Udgivelse af rum" + "Rum-adresser er måder at finde og få adgang til rum på. Dette sikrer også, at du nemt kan dele dit rum med andre. +Adressen er også påkrævet for at gøre rummet synligt i det %1$s offentlige register." + "Rummets synlighed" + "Sikkerhed og privatliv" + 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 310d955a60f..16ef6b7e050 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -3,7 +3,7 @@ "Sie brauchen eine Chatroomadresse, so dass sie im Verzeichnis sichtbar gemacht werden kann." "Chatroomadresse" "Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." - "Dein Homeserver unterstützt diese Option in verschlüsselten Chat nicht. In einigen Chats wirst du möglicherweise nicht benachrichtigt." + "Ihr Homeserver unterstützt diese Option in verschlüsselten Räumen nicht. In einigen Räumen werden Sie möglicherweise nicht benachrichtigt." "Umfragen" "Nur Administratoren" "Mitglieder sperren" @@ -20,11 +20,11 @@ "Raumthema ändern" "Nachrichten senden" "Admins bearbeiten" - "Du kannst diese Aktion nicht mehr rückgängig machen. Du vergibst dieselbe Rolle, die du auch hast." + "Sie können diese Aktion nicht mehr rückgängig machen. Sie vergeben die gleiche Rolle, die Sie auch haben." "Als Administrator hinzufügen?" "Zurückstufen" - "Du stufst dich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn du der letzte Benutzer mit dieser Rolle bist, ist es nicht möglich, diese Rolle wiederzuerlangen." - "Möchtest du dich selbst herabstufen?" + "Sie stufen sich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn Sie der letzte Nutzer mit dieser Rolle sind, ist es nicht möglich, diese Rolle wiederzuerlangen." + "Möchten Sie sich selbst herabstufen?" "%1$s (Ausstehend)" "(Ausstehend)" "Administratoren haben automatisch Moderatorenrechte" @@ -32,7 +32,7 @@ "Administratoren" "Moderatoren" "Mitglieder" - "Du hast nicht gespeicherte Änderungen." + "Sie haben ungespeicherte Änderungen." "Änderungen speichern?" "Thema hinzufügen" "Bereits Mitglied" @@ -43,12 +43,12 @@ "Raum bearbeiten" "Es ist ein unbekannter Fehler aufgetreten und die Informationen konnten nicht geändert werden." "Raum kann nicht aktualisiert werden" - "Nachrichten und Anrufe sind Ende-zu-Ende verschlüsselt. Nur du und die Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren." + "Nachrichten und Anrufe sind verschlüsselt. Nur Sie und die Empfänger haben die Schlüssel, um sie zu entsperren." "Nachrichtenverschlüsselung aktiviert" "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." "Die Stummschaltung ist fehlgeschlagen, bitte versuche es erneut." "Die Deaktivierung der Stummschaltung ist fehlgeschlagen, bitte versuche es erneut." - "Personen einladen" + "Nutzer einladen" "Unterhaltung verlassen" "Verlassen" "Medien und Dateien" @@ -66,43 +66,34 @@ "Informationen" "Thema" "Raum wird aktualisiert…" - "Sperren" - "Sie können dem Raum nicht mehr beitreten, selbst wenn sie eingeladen werden." - "Bist du sicher, dass du dieses Mitglied sperren möchtest?" - "In diesem Raum gibt es keine gesperrten Benutzer." - "%1$s wird gesperrt." + "In diesem Chatroom gibt es keine gesperrten Nutzer." "%1$d Person" "%1$d Personen" - "Mitglied entfernen und sperren" - "Mitglied entfernen" "Mitglied entfernen und sperren" "Mitglied nur entfernen" - "Mitglied entfernen und den erneuten Beitritt sperren?" "Sperre aufheben" "Die Nutzer können den Raum wieder beitreten, wenn sie dazu eingeladen werden." - "Benutzer entsperren" - "Benutzerinformationen anzeigen" + "Nutzer entsperren" "Gesperrt" "Mitglieder" "Ausstehend" - "%1$s wird entfernt." "Administrator" "Moderator" "Mitglieder" "%1$s wird entsperrt." "Benutzerdefinierte Einstellungen verwenden" - "Wenn du diese Option aktivierst, wird deine Standardeinstellung außer Kraft gesetzt." + "Dies wird ihre Standardeinstellungen außer Kraft setzen." "Benachrichtige mich in diesem Chat bei" "Zum Anpassen der Standardeinstellungen gehe zu: %1$s" "Globale Einstellungen" "Standardeinstellung" "Benutzerdefinierte Einstellung entfernen" "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." - "Fehler beim Wiederherstellen des Standardmodus. Bitte versuche es erneut." - "Fehler beim Einstellen des Modus. Bitte versuche es erneut." - "Dein Homeserver unterstützt diese Option in verschlüsselten Chats nicht. Du wirst in diesem Chat nicht benachrichtigt." + "Fehler beim Wiederherstellen des Standardmodus. Bitte erneut versuchen." + "Fehler beim Einstellen des Modus. Bitte erneut versuchen." + "Ihr Homeserver unterstützt diese Option in verschlüsselten Chatrooms nicht. Sie erhalten in diesem Chatroom keine Benachrichtigungen." "Alle Nachrichten" "Nur Erwähnungen und Schlüsselwörter" "Benachrichtige mich bei" @@ -121,35 +112,35 @@ "Raum-Details anpassen" "Rollen und Berechtigungen" "Chatroomadresse hinzufügen" - "Jeder kann den Beitritt zum Chatroom beantragen, aber ein Administrator oder Moderator muss die Anfrage annehmen." - "Bitte um Beitritt" + "Jeder kann den Zutritt zum Raum beantragen, aber ein Administrator oder ein Moderator müssen die Anfrage akzeptieren." + "Beitritt beantragen" "Ja, Verschlüsselung aktivieren" - "Nach der Aktivierung kann die Verschlüsselung für einen Raum nicht mehr deaktiviert werden. Der Nachrichtenverlauf ist nur für Raummitglieder sichtbar, weil sie eingeladen wurden oder dem Chatroom beigetreten sind. -Niemand außer den Raummitgliedern kann Nachrichten lesen. Dies kann verhindern, dass Bots und Bridges richtig funktionieren. -Wir empfehlen nicht, die Verschlüsselung für Räume zu aktivieren, die jeder finden und denen jeder beitreten kann." + "Einmal angeschaltet kann die Verschlüsselung für einen Chatroom nicht mehr deaktiviert werden. Der Nachrichtenverlauf ist nur für Chatroommitglieder sichtbar, seit sie eingeladen wurden oder dem Chatroom beigetreten sind. +Niemand außer Chatroommitgliedern kann Nachrichten lesen. Dies kann verhindern, dass Bots und Bridges richtig funktionieren. +Wir empfehlen nicht, die Verschlüsselung für Chatrooms die jeder finden und betreten darf, zu aktivieren." "Verschlüsselung aktivieren?" - "Nach der Aktivierung kann die Verschlüsselung nicht deaktiviert werden." + "Einmal angeschaltet kann die Verschlüsselung nicht mehr deaktiviert werden." "Verschlüsselung" "Ende-zu-Ende-Verschlüsselung aktivieren" - "Jeder kann es finden und beitreten" - "Alle" + "Jeder kann diesen Raum finden und betreten" + "Jeder" "Personen können nur beitreten, wenn sie eingeladen werden." "Nur auf Einladung" "Chatroomzugang" "Räume werden zur Zeit nicht unterstützt." - "Raum Mitglieder" + "Spacemitglieder" "Um den Chatroom im Chatroomverzeichnis sichtbar zu machen, benötigen Sie eine Chatroomadresse." "Chatroomadresse" - "Erlauben Sie, dass dieser Chatroom gefunden werden kann, indem Sie das %1$s öffentliche Chatroomverzeichnis durchsuchen" + "Erlauben Sie, dass dieser Chatroom im öffentlichen Chatroomverzeichnis von %1$s gefunden werden kann." "Sichtbar im öffentlichen Chatroomverzeichnis" "Jeder" - "Wer kann die Verlaufsgeschichte lesen" - "Nur Mitglieder, da sie eingeladen wurden" - "Nur Mitglieder seit Auswahl dieser Option" - "Chatroomadressen machen es möglich, Chatrooms zu finden und auf sie zuzugreifen. Dadurch wird auch sichergestellt, dass Sie Ihren Chatroom problemlos mit anderen teilen können. -Lassen Sie die Möglichkeit zu, Ihren Chatroom in Ihrem öffentlichen Chatroomverzeichnis auf Ihrem Homeserver zu veröffentlichen." + "Wer hat Zugriff auf den Nachrichtenverlauf des Chatrooms" + "Nur Mitglieder, aber erst seit ihrer Einladung" + "Nur Mitglieder seit diese Chatroomoption ausgewählt wurde." + "Chatroomadressen machen es möglich, Chatrooms zu finden und auf sie zuzugreifen. Dies erleichtert es, Chatrooms mit anderen zu teilen. +Falls erlaubt, können Sie Ihren Chatroom im öffentlichen Raumverzeichnis Ihres Homeservers aufführen." "Veröffentlichung von Räumen" "Chatroomadressen sind Möglichkeiten, Chatrooms zu finden und auf sie zuzugreifen. So können Sie Ihren Chatroom auch problemlos mit anderen teilen. Die Adresse ist auch erforderlich, um den Chatroom in einem %1$s öffentlichen Chatroomverzeichnis sichtbar zu machen." - "Chatroomsichtbarkeit." + " Sichtbarkeit des Chatrooms" "Sicherheit & Datenschutz" 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 3f21e7cf528..aed10ce24f1 100644 --- a/features/roomdetails/impl/src/main/res/values-el/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-el/translations.xml @@ -1,27 +1,29 @@ + "Θα χρειαστείτε μια διεύθυνση αίθουσας για να την κάνετε ορατή στον κατάλογο." + "Διεύθυνση αίθουσας" "Παρουσιάστηκε σφάλμα κατά την ενημέρωση της ρύθμισης ειδοποίησης." - "Ο οικιακός διακομιστής σου δεν υποστηρίζει αυτήν την επιλογή σε κρυπτογραφημένα δωμάτια, ενδέχεται να μην λάβεις ειδοποίηση σε ορισμένα δωμάτια." + "Ο αρχικός διακομιστής σας δεν υποστηρίζει αυτή την επιλογή σε κρυπτογραφημένες αίθουσες, ενδέχεται να μην λαμβάνετε ειδοποιήσεις σε ορισμένες αίθουσες." "Δημοσκοπήσεις" "Μόνο διαχειριστές" "Αποκλεισμός ατόμων" "Αφαίρεση μηνυμάτων" "Όλοι" - "Πρόσκληση ατόμων" + "Προσκάλεσε άτομα και αποδέξου αιτήματα συμμετοχής" "Συντονισμός μελών" "Μηνύματα και περιεχόμενο" "Διαχειριστές και συντονιστές" - "Αφαίρεση ατόμων" - "Αλλαγή avatar δωματίου" - "Λεπτομέρειες δωματίου" - "Αλλαγή ονόματος δωματίου" - "Αλλαγή θέματος δωματίου" + "Αφαίρεση ατόμων και απόρριψη αιτημάτων συμμετοχής" + "Αλλαγή εικόνας προφίλ αίθουσας" + "Λεπτομέρειες αίθουσας" + "Αλλαγή ονόματος αίθουσας" + "Αλλαγή θέματος αίθουσας" "Αποστολή μηνυμάτων" "Επεξεργασία Διαχειριστών" "Δεν θα μπορείς να αναιρέσεις αυτήν την ενέργεια. Προβιβάζεις τον χρήστη να έχει το ίδιο επίπεδο ισχύος με σένα." "Προσθήκη Διαχειριστή;" "Υποβιβασμός" - "Δεν θα μπορείς να αναιρέσεις αυτήν την αλλαγή αφού, υποβιβάζεσαι. Εάν είσαι ο τελευταίος προνομιούχος χρήστης στο δωμάτιο θα είναι αδύνατο να ανακτηθούν προνόμια." + "Δεν θα μπορέσετε να αναιρέσετε αυτή την αλλαγή καθώς υποβιβάζετε τον εαυτό σας, αν είστε ο τελευταίος χρήστης με δικαιώματα στην αίθουσα θα είναι αδύνατο να ανακτήσετε δικαιώματα." "Υποβιβασμός του εαυτού σου;" "%1$s (Σε αναμονή)" "(Σε αναμονή)" @@ -37,57 +39,49 @@ "Ήδη προσκεκλημένος" "Κρυπτογραφημένο" "Μη κρυπτογραφημένο" - "Δημόσιο δωμάτιο" - "Επεξεργασία Δωματίου" + "Δημόσια αίθουσα" + "Επεξεργασία Αίθουσας" "Υπήρξε ένα άγνωστο σφάλμα και οι πληροφορίες δεν μπορούσαν να αλλάξουν." - "Δεν είναι δυνατή η ενημέρωση του δωματίου" + "Αδυναμία ενημέρωσης αίθουσας" "Τα μηνύματα ασφαλίζονται με κλειδαριές. Μόνο εσύ και οι παραλήπτες έχετε τα μοναδικά κλειδιά για να τα ξεκλειδώσετε." "Ενεργοποιημένη κρυπτογράφηση μηνυμάτων" "Παρουσιάστηκε σφάλμα κατά τη φόρτωση των ρυθμίσεων ειδοποίησης." - "Αποτυχία σίγασης αυτού του δωματίου, δοκίμασε ξανά." - "Αποτυχία κατάργησης σίγασης αυτού του δωματίου, δοκίμασε ξανά." + "Η σίγαση αυτής της αίθουσας απέτυχε, δοκιμάστε ξανά." + "Η κατάργηση σίγασης αυτής της αίθουσας απέτυχε, δοκιμάστε ξανά." "Πρόσκληση ατόμων" "Αποχώρηση από τη συζήτηση" - "Αποχώρηση από το δωμάτιο" + "Αποχώρηση από την αίθουσα" "Πολυμέσα και αρχεία" "Προσαρμοσμένο" "Προεπιλογή" "Ειδοποιήσεις" "Καρφιτσωμένα μηνύματα" + "Προφίλ" "Αιτήματα συμμετοχής" "Ρόλοι και δικαιώματα" - "Όνομα δωματίου" + "Όνομα αίθουσας" "Ασφάλεια & απόρρητο" "Ασφάλεια" - "Κοινή χρήση δωματίου" - "Πληροφορίες δωματίου" + "Κοινή χρήση αίθουσας" + "Πληροφορίες αίθουσας" "Θέμα" - "Ενημέρωση δωματίου…" - "Αποκλεισμός" - "Δεν θα μπορεί να συμμετέχει ξανά σε αυτό το δωμάτιο εάν προσκληθεί." - "Θες σίγουρα να αποκλείσεις αυτό το μέλος;" - "Δεν υπάρχουν αποκλεισμένοι χρήστες σε αυτό το δωμάτιο." - "Αποκλεισμός %1$s" + "Ενημέρωση αίθουσας…" + "Δεν υπάρχουν αποκλεισμένοι χρήστες σε αυτή την αίθουσα." "%1$d άτομο" "%1$d άτομα" - "Αφαίρεση και αποκλεισμός μέλους" - "Αφαίρεση από το δωμάτιο" "Αφαίρεση και αποκλεισμός μέλους" "Μόνο αφαίρεση μέλους" - "Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;" "Αναίρεση αποκλεισμού" - "Θα μπορεί να συμμετάσχει ξανά στο δωμάτιο εάν προσκληθεί." + "Θα μπορούν να συμμετάσχουν ξανά σε αυτή την αίθουσα, εάν προσκληθούν." "Άρση αποκλεισμού χρήστη" - "Προβολή προφίλ" "Αποκλεισμένοι" "Μέλη" "Σε αναμονή" - "Αφαίρεση %1$s…" "Διαχειριστής" "Συντονιστής" - "Μέλη δωματίου" + "Μέλη της αίθουσας" "Άρση αποκλεισμού %1$s" "Να επιτρέπεται η προσαρμοσμένη ρύθμιση" "Η ενεργοποίηση αυτής της ρύθμισης θα παρακάμψει την προεπιλεγμένη ρύθμιση" @@ -99,10 +93,10 @@ "Παρουσιάστηκε σφάλμα κατά τη φόρτωση των ρυθμίσεων ειδοποίησης." "Αποτυχία επαναφοράς της προεπιλεγμένης λειτουργίας, δοκίμασε ξανά." "Αποτυχία ρύθμισης της λειτουργίας, δοκίμασε ξανά." - "Ο οικιακός σου διακομιστής δεν υποστηρίζει αυτήν την επιλογή σε κρυπτογραφημένα δωμάτια, δεν θα λαμβάνεις ειδοποίηση σε αυτό το δωμάτιο." + "Ο αρχικός διακομιστής σας δεν υποστηρίζει αυτή την επιλογή σε κρυπτογραφημένες αίθουσες, δεν θα λάβετε ειδοποιήσεις σε αυτή την αίθουσα." "Όλα τα μηνύματα" "Μόνο αναφορές και λέξεις-κλειδιά" - "Σε αυτό το δωμάτιο, ειδοποίησέ με για" + "Σε αυτήν την αίθουσα, ειδοποιήστε με για" "Διαχειριστές" "Άλλαξε τον ρόλο μου" "Υποβιβασμός σε μέλος" @@ -115,14 +109,15 @@ "Μόλις επαναφέρεις τα δικαιώματα, θα χάσεις τις τρέχουσες ρυθμίσεις." "Επαναφορά δικαιωμάτων;" "Ρόλοι" - "Λεπτομέρειες δωματίου" + "Λεπτομέρειες αίθουσας" "Ρόλοι και δικαιώματα" - "Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά κάποιος διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα." + "Προσθήκη διεύθυνσης αίθουσας" + "Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στην αίθουσα, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχτεί το αίτημα." "Αίτημα συμμετοχής" "Ναι, ενεργοποιήστε την κρυπτογράφηση" - "Μόλις ενεργοποιηθεί, η κρυπτογράφηση για ένα δωμάτιο δεν μπορεί να απενεργοποιηθεί. Το ιστορικό μηνυμάτων θα είναι ορατό μόνο για τα μέλη του δωματίου από τότε που προσκλήθηκαν ή από τότε που εντάχθηκαν στην αίθουσα. -Κανείς εκτός από τα μέλη του δωματίου δεν θα μπορεί να διαβάσει μηνύματα. Αυτό μπορεί να αποτρέψει τη σωστή λειτουργία των bots και των γεφυρών. -Δεν συνιστούμε να ενεργοποιήσεις την κρυπτογράφηση για δωμάτια στα οποία μπορεί κανείς να βρει και να συμμετάσχει." + "Μόλις ενεργοποιηθεί, η κρυπτογράφηση για μια αίθουσα δεν μπορεί να απενεργοποιηθεί, το ιστορικό μηνυμάτων θα είναι ορατό μόνο για τα μέλη της αίθουσας από τότε που προσκλήθηκαν ή από τότε που συμμετείχαν στην αίθουσα. +Κανείς άλλος εκτός από τα μέλη της αίθουσας δεν θα μπορεί να διαβάσει τα μηνύματα. Αυτό μπορεί να εμποδίσει τη σωστή λειτουργία των bots και των γεφυρών. +Δεν συνιστούμε την ενεργοποίηση της κρυπτογράφησης για αίθουσες που μπορεί να βρει και να συμμετάσχει ο καθένας." "Ενεργοποίηση κρυπτογράφησης;" "Μόλις ενεργοποιηθεί, η κρυπτογράφηση δεν μπορεί να απενεργοποιηθεί." "Κρυπτογράφηση" @@ -131,6 +126,22 @@ "Οποιοσδήποτε" "Τα άτομα μπορούν να συμμετάσχουν μόνο εάν έχουν προσκληθεί" "Μόνο πρόσκληση" - "Πρόσβαση δωματίου" + "Πρόσβαση στην αίθουσα" + "Οι χώροι δεν υποστηρίζονται προς το παρόν" + "Μέλη χώρου" + "Θα χρειαστείτε μια διεύθυνση αίθουσας για να την κάνετε ορατή στον κατάλογο αιθουσών." + "Διεύθυνση αίθουσας" + "Επιστρέψτε την εύρεση αυτής της αίθουσας με αναζήτηση στον κατάλογο %1$s δημοσίων αιθουσών" + "Ορατή στον κατάλογο δημόσιων αιθουσών" + "Οποιοσδήποτε" + "Ποιος μπορεί να διαβάσει το ιστορικό" + "Μόνο μέλη από τη στιγμή που προσκλήθηκαν" + "Μόνο για μέλη μετά από αυτήν την επιλογή" + "Οι διευθύνσεις αιθουσών είναι τρόποι εύρεσης και πρόσβασης σε αίθουσες. Αυτό διασφαλίζει επίσης ότι μπορείτε εύκολα να μοιραστείτε την αίθουσα με άλλους. +Μπορείτε να επιλέξετε να δημοσιεύσετε την αίθουσά σας στον δημόσιο κατάλογο αιθουσών του αρχικού διακομιστή σας." + "Δημοσίευση αίθουσας" + "Οι διευθύνσεις αιθουσών είναι τρόποι εύρεσης και πρόσβασης σε αίθουσες. Αυτό εξασφαλίζει επίσης ότι μπορείτε εύκολα να μοιραστείτε την αίθουσα με άλλους. +Η διεύθυνση απαιτείται επίσης για να γίνει η αίθουσα ορατή στον δημόσιο κατάλογο αιθουσών %1$s." + "Ορατότητα αίθουσας" "Ασφάλεια & απόρρητο" 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 de509e7e81b..ffce0a6b768 100644 --- a/features/roomdetails/impl/src/main/res/values-es/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -1,17 +1,19 @@ + "Necesitarás una dirección de sala para que sea visible en el directorio." + "Dirección de la sala" "Se ha producido un error al actualizar la configuración de notificaciones." - "Tu servidor principal no admite esta opción en salas cifradas, puede que no recibas notificaciones en algunas salas." + "Tu servidor base no admite esta opción en salas cifradas, puede que no recibas notificaciones de algunas salas." "Encuestas" "Solo administradores" - "Prohibir personas" + "Vetar personas" "Eliminar mensajes" "Todos" - "Invitar a personas" + "Invitar personas y aceptar solicitudes de unión" "Moderación de miembros" "Mensajes y contenido" "Administradores y moderadores" - "Eliminar personas" + "Eliminar personas y rechazar solicitudes de unión" "Cambiar el avatar de la sala" "Detalles de la sala" "Cambiar el nombre de la sala" @@ -23,6 +25,9 @@ "Degradar" "No podrás deshacer este cambio ya que te estás degradando. Si eres el último usuario privilegiado en la sala será imposible recuperar los privilegios." "¿Degradarte?" + "%1$s (Pendiente)" + "(Pendiente)" + "Los administradores tienen privilegios de moderador de forma automática" "Editar moderadores" "Administradores" "Moderadores" @@ -32,6 +37,9 @@ "Añadir tema" "Ya eres miembro" "Ya estás invitado" + "Cifrada" + "No cifrada" + "Sala pública" "Editar sala" "Se ha producido un error desconocido y no se ha podido cambiar la información." "No se puede actualizar la sala" @@ -43,41 +51,38 @@ "Invitar personas" "Salir de la conversación" "Salir de la sala" + "Medios y archivos" "Personalizado" "Por defecto" "Notificaciones" + "Mensajes fijados" + "Perfil" + "Solicitudes de unión" "Roles y permisos" "Nombre de la sala" + "Seguridad y privacidad" "Seguridad" "Compartir sala" + "Información de la sala" "Tema" "Actualizando la sala…" - "Prohibir" - "No podrán volver a unirse a esta sala si son invitados." - "¿Estás seguro de que quieres prohibir a este miembro?" - "No hay usuarios prohibidos en esta sala." - "Prohibiendo %1$s" + "No hay usuarios vetados en esta sala." "Una persona" "%1$d personas" - "Eliminar y prohibir a un miembro" - "Remover de la sala" - "Eliminar y prohibir a un miembro" + "Sacar y vetar a un miembro" "Solo eliminar miembro" - "¿Eliminar al miembro y prohibirle unirse en el futuro?" - "Anular la prohibición" - "Podrán volver a unirse a esta sala si son invitados de nuevo." - "Desprohibir al usuario" - "Ver perfil" - "Prohibidos" + "Quitar veto" + "Podrá volver a unirse a esta sala si se le invita." + "Quitar veto al usuario" + "Vetados" "Miembros" "Pendiente" - "Eliminando %1$s…" "Admin" "Moderador" "Miembros de la sala" - "Dejando de prohibir %1$s" + "Levantando veto a %1$s" "Permitir configuración personalizada" "Si activas esta opción, anularás tu configuración por defecto" "Notificarme en este chat para" @@ -88,7 +93,7 @@ "Se ha producido un error al cargar la configuración de las notificaciones." "No se pudo restaurar el modo predeterminado, por favor vuelve a intentarlo de nuevo." "No se pudo cambiar el modo, por favor inténtalo de nuevo." - "Tu servidor principal no admite esta opción en salas cifradas, no recibirás notificaciones en esta sala." + "Tu servidor base no admite esta opción en salas cifradas, no recibirás notificaciones de esta sala." "Todos los mensajes" "Únicamente Menciones y Palabras clave" "En esta sala, notificarme por" @@ -106,4 +111,37 @@ "Roles" "Detalles de la sala" "Roles y permisos" + "Agregar dirección de sala" + "Cualquiera puede solicitar unirse a la sala, pero un administrador o moderador tendrá que aceptar la solicitud." + "Solicitud para unirse" + "Sí, activar cifrado" + "Una vez activado, el cifrado de una sala no se puede desactivar. El historial de mensajes solo será visible para los miembros de la sala desde que fueron invitados o desde que se unieron a la sala. +Nadie más que los miembros de la sala podrán leer los mensajes. Esto puede impedir que los bots y los puentes funcionen correctamente. +No recomendamos habilitar el cifrado para las salas que cualquiera pueda encontrar y unirse." + "¿Activar cifrado?" + "Una vez activado, el cifrado no se puede desactivar." + "Cifrado" + "Activar el cifrado de extremo a extremo" + "Cualquiera puede encontrarla y unirse" + "Cualquiera" + "Las personas solo pueden unirse si están invitadas" + "Solo por invitación" + "Acceso a la sala" + "No se admiten los espacios por el momento." + "Miembros del espacio" + "Necesitarás una dirección de sala para que esta sea visible en el directorio de salas." + "Dirección de la sala" + "Permite encontrar esta sala buscando en el directorio de salas públicas de %1$s" + "Visible en el directorio de salas públicas" + "Cualquiera" + "Quién puede leer el historial" + "Solo participantes desde que fueron invitados" + "Solo participantes desde que se selecciona esta opción" + "Las direcciones de sala son formas de buscar salas y acceder a ellas. Esto también garantiza que puedas compartir fácilmente tu sala con otras personas. +Puedes optar por publicar tu sala en el directorio de salas públicas de tu servidor base." + "Publicación de la sala" + "Las direcciones de sala son un medio para buscar y acceder a salas. También te permiten compartir fácilmente tu sala con otras personas. +La dirección también es necesaria para hacer visible la sala en el directorio de salas públicas de %1$s." + "Visibilidad de la sala" + "Seguridad y privacidad" 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 698f9dffe93..9fb14d85ecf 100644 --- a/features/roomdetails/impl/src/main/res/values-et/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-et/translations.xml @@ -66,28 +66,19 @@ "Jututoa teave" "Teema" "Uuendame jututuba…" - "Sea suhtluskeeld" - "Ta ei saa selle jututoaga liituda isegi kutse olemasolul." - "Kas sa oled kindel, et soovid sellele kasutajale seada suhtluskeelu?" "Jututoas pole suhtluskeeluga kasutajaid" - "Seame kasutajale %1$s suhtluskeelu" "%1$d osaleja" "%1$d osalejat" - "Eemalda ja sea suhtluskeeld" - "Eemalda kasutaja jututoast" "Eemalda ja sea suhtluskeeld" "Ainult eemalda kasutaja" - "Kas eemaldama kasutaja ja seame talle tulevikuks suhtluskeelu?" "Eemalda suhtluskeeld" "Kutse olemasolul saab ta nüüd jututoaga uuesti liituda" "Eemalda kasutaja suhtluskeeld" - "Vaata profiili" "Suhtluskeeluga kasutajad" "Liikmed" "Ootel" - "Eemaldame kasutajat %1$s…" "Peakasutaja" "Moderaator" "Jututoas osalejad" @@ -103,7 +94,7 @@ "Vaikimisi seadistuste taastamine ei õnnestunud. Palun proovi uuesti." "Seadistuste muutmine ei õnnestunud. Palun proovi uuesti." "Sinu koduserver ei toeta seda võimalust krüptitud jututubades, seega sa ei saa selle jututoa kohta teavitusi." - "Kõik sõnumid" + "Kõikide sõnumite korral" "Mainimiste ja võtmesõnade alusel" "Selles jututoas teavita mind" "Peakasutajad" @@ -122,7 +113,7 @@ "Rollid ja õigused" "Lisa jututoa aadress" "Kõik võivad paluda jututoaga liitumist, kuid peakasutaja või moderaator peavad sellega nõustuma." - "Palu võimalust liituda" + "Küsi võimalust liitumiseks" "Jah, lülita krüptimine sisse" "Kui jututoa krüptimine on kord sisse lülitatud, siis seda välja lülitada ei saa. Sõnumite ajalugu on nähtav vaid jututoa liikmetele alates kutse saamise või liitumise hetkest. Keegi teine peale jututoa liikmete ei saa sõnumeid lugeda. See võib takistada suhtlusrobotite ja/või võrgusildade toimimist. diff --git a/features/roomdetails/impl/src/main/res/values-eu/translations.xml b/features/roomdetails/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..bd2a3709428 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,124 @@ + + + "Gelaren helbidea" + "Errorea gertatu da jakinarazpen-ezarpena eguneratzean." + "Zure zerbitzaria ez da bateragarria enkriptatutako gelen aukerarekin; litekeena da gela batzuetako jakinarazpenak ez jasotzea." + "Inkestak" + "Administratzaileak soilik" + "Jarri debekua jendeari" + "Kendu mezuak" + "Guztiak" + "Gonbidatu jendea" + "Kideen moderazioa" + "Mezuak eta edukiak" + "Administratzaileak eta moderatzaileak" + "Kendu jendea" + "Aldatu gelaren abatarra" + "Gelaren xehetasunak" + "Aldatu gelaren izena" + "Aldatu gelako mintzagaia" + "Bidali mezuak" + "Editatu administratzaileak" + "Administratzailea gehitu?" + "Jaitsi mailaz" + "Ezin izango duzu hau aldatu zure burua mailaz jaisten ari zarelako, zu bazara gelan baimenak dituen azken erabiltzailea ezin izango dira baimenak berreskuratu." + "Zure burua mailaz jaitsi?" + "%1$s (zain)" + "(Egiteke)" + "Administratzaileek automatikoki dute moderatzaile-pribilegioak" + "Editatu moderatzaileak" + "Administratzaileak" + "Moderatzaileak" + "Kideak" + "Gorde gabeko aldaketak dituzu." + "Aldaketak gorde?" + "Gehitu hizketagaia" + "Kidea da dagoeneko" + "Lehendik ere gonbidatuta" + "Zifratuta" + "Zifratu gabe" + "Gela publikoa" + "Editatu gela" + "Errore ezezaguna gertatu da eta ezin izan da informazioa aldatu." + "Ezin da gela eguneratu" + "Mezuen enkriptazioa gaituta dago" + "Errorea gertatu da jakinarazpen-ezarpenak kargatzean." + "Ezin izan da gela mututu; saiatu berriro." + "Ezin izan da gela mututzeari utzi; saiatu berriro." + "Gonbidatu jendea" + "Utzi elkarrizketa" + "Atera gelatik" + "Multimedia eta fitxategiak" + "Lehenetsia" + "Jakinarazpenak" + "Finkatutako mezuak" + "Profila" + "Sartzeko eskaerak" + "Rolak eta baimenak" + "Gelaren izena" + "Segurtasuna eta pribatutasuna" + "Segurtasuna" + "Partekatu gela" + "Gelaren informazioa" + "Gaia" + "Gela eguneratzen…" + "Gela honetan ez dago debekua ezarri zaion erabiltzailerik." + + "Pertsona %1$d" + "%1$d pertsona" + + "Kendu kidea eta ezarri debekua" + "Kendu kidea soilik" + "Kendu debekua" + "Kendu debekua erabiltzaileari" + "Debekatuta" + "Kideak" + "Zain" + "Kudeatzailea" + "Moderatzailea" + "Gelako kideak" + "%1$s(r)i debekua kentzen" + "Aktibatuz gero, defektuzko ezarpena gainidatziko du" + "Jakinarazi txat honetan" + "%1$s alda dezakezu." + "Ezarpen orokorretan" + "Defektuzko ezarpena" + "Errorea gertatu da jakinarazpen-ezarpenak kargatzean." + "Zure zerbitzaria ez da bateragarria enkriptatutako gelen aukerarekin; litekeena da gela batzuetako jakinarazpenak ez jasotzea." + "Mezu guztiak" + "Aipamenak eta hitz gakoak soilik" + "Gela honetan, jakinarazi" + "Administratzaileak" + "Aldatu nire rola" + "Jaitsi maila, kidera" + "Jaitsi maila, moderatzailera" + "Kideen moderazioa" + "Mezuak eta edukiak" + "Moderatzaileak" + "Baimenak" + "Berrezarri baimenak" + "Baimenak berrezarritakoan, uneko ezarpenak galduko dituzu." + "Baimenak berrezarri?" + "Rolak" + "Gelaren xehetasunak" + "Rolak eta baimenak" + "Gehitu gelaren helbidea" + "Bai, gaitu zifratzea" + "Zifratzea" + "Edonork aurkitu eta bat egin dezake" + "Edonork" + "Gonbidatutako pertsonak bakarrik sartu ahal izango dira" + "Gonbidapen bidez" + "Gelarako sarbidea" + "Gaur-gaurkoz ez da guneekin bateragarria" + "Guneko kideak" + "Gelarako helbideren bat beharko duzu gelen direktorioan ikusgai egon dadila nahi baduzu." + "Gelaren helbidea" + "Gela publikoen direktorioan ikusgai" + "Edonork" + "Nork irakur dezake historia" + "Kideek bakarrik, gonbidatu zituztenetik" + "Kideek bakarrik, aukera hau hautatu zenetik" + "Gelaren ikusgarritasuna" + "Segurtasuna eta pribatutasuna" + 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 491435d52de..279e636c5b7 100644 --- a/features/roomdetails/impl/src/main/res/values-fa/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fa/translations.xml @@ -1,5 +1,6 @@ + "نشانی اتاق" "هنگام به‌روز کردن تنظیمات آگاهی خطایی رخ داد." "کارساز خانگیتان از این گزینه در اتاق‌های رمز شده پشتیبانی نمی‌کند. ممکن است در برخی اتاق‌ها آگاه نشوید." "نظرسنجی‌ها" @@ -7,11 +8,11 @@ "تحریم افراد" "برداشتن پیام‌ها" "هرکسی" - "دعوت افراد" + "دعوت افراد و پذیرش درخواست‌های پیوستن" "نظارت اعضا" "پیام‌ها و محتوا" "مدیرن و ناظران" - "برداشتن افراد" + "برداشتن افراد و رد درخواست‌های پیوستن" "تغییر چهرک اتاق" "جزییات اتاق" "تغییر نام اتاق" @@ -41,7 +42,11 @@ "ویرایش اتاق" "خطایی ناشناخته رخ داد و اطّلاعات قابل تغییر نبودند." "ناتوان در به‌روز رسانی اتاق" + "پیام‌ها با قفل محافظت می‌شوند. فقط شما و گیرندگان، کلیدهای منحصر به فرد برای باز کردن قفل آنها را دارید." "رمزنگاری پیام به کار افتاد" + "هنگام بارگیری تنظیمات اعلان خطایی رخ داد." + "بی صدا کردن این اتاق ناموفق بود، لطفا دوباره امتحان کنید." + "بی صدا کردن این اتاق ناموفق بود، لطفا دوباره امتحان کنید." "دعوت افراد" "ترک گفت‌وگو" "ترک اتاق" @@ -49,36 +54,42 @@ "پیش‌گزیده" "آگاهی‌ها" "پیام‌های سنجاق شده" + "نمایه" + "درخواست‌های پیوستن" "نقش‌ها و اجازه‌ها" "نام اتاق" + "امنیت و محرمانگی" "امنیت" "هم‌رسانی اتاق" "اطّلاعات اتاق" "موضوع" "به‌روز کردن اتاق…" - "تحریم" - "تحریم کردن %1$s" - "برداشت و تحریم عضو" - "برداشتن از اتاق" + + "%1$d نفر" + "%1$d نفر" + "برداشت و تحریم عضو" "تنها برداشتن عضو" "رفع انسداد" + "در صورت دعوت می‌تواند دوباره به اتاق بپیوندد." "تحریم نکردن کاربر" - "دیدن نمایه" "محروم" "اعضا" "منتظر" - "برداشتن %1$s…" "مدیر" "ناظمر" "اعضای اتاق" "رفع تحریم %1$s" "اجازه به تنظیمت شخصی" + "روشن کردن این گزینه تنظیمات پیش فرض شما را لغو می کند" "آگاهی من در این گپ برای" "می‌توانید در %1$sتان تغییرش دهید." "تنظیمات جهانی" "تنظیمات پیش‌گزیده" "برداشتن تنظیمات سفارشی" + "هنگام بارگیری تنظیمات اعلان خطایی رخ داد." + "بازیابی حالت پیش فرض ناموفق بود، لطفا دوباره امتحان کنید." + "تنظیم حالت ناموفق است، لطفا دوباره امتحان کنید." "همهٔ پیام‌ها" "فقط اشاره‌ها و کلیدواژگان" "آگاهی من در این اتاق برای" @@ -95,4 +106,22 @@ "نقش‌ها" "جزییات اتاق" "نقش‌ها و اجازه‌ها" + "افزودن نشانی اتاق" + "درخواست دعوت" + "بله. به کار انداختن رمزنگاری" + "رمزگذاری فعال شود؟" + "پس از به کار افتادن، رمزنگاری قابل از کار انداختن نیست." + "رمزنگاری" + "هرکسی می تواند یافته و بپیوندد" + "هرکسی" + "افراد فقط در صورت دعوت می‌توانند بپیوندند" + "فقط دعوتی" + "دسترسی اتاق" + "در حال حاضر فضاها پشتیبانی نمی‌شوند" + "اعضای فضا" + "نشانی اتاق" + "هرکسی" + "انتشار اتاق" + "نمایانی اتاق" + "امنیت و محرمانگی" 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 a1e0d29e3a0..82314e3b68c 100644 --- a/features/roomdetails/impl/src/main/res/values-fi/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fi/translations.xml @@ -66,28 +66,19 @@ "Huoneen tiedot" "Aihe" "Muokataan huonetta…" - "Anna porttikielto" - "He eivät voi enää liittyä tähän huoneeseen, jos heidät kutsutaan." - "Haluatko varmasti antaa tälle jäsenelle porttikiellon?" "Tässä huoneessa ei ole porttikieltoja" - "Annetaan porttikieltoa käyttäjälle %1$s" "%1$d henkilö" "%1$d henkilöä" - "Poista jäsen huoneesta ja anna porttikielto" - "Poista huoneesta" "Poista jäsen huoneesta ja anna porttikielto" "Poista vain jäsen huoneesta" - "Poistetaanko jäsen huoneesta ja kielletäänkö heitä liittymästä tulevaisuudessa?" "Poista porttikielto" "He voivat liittyä tähän huoneeseen uudelleen, jos heidät kutsutaan." "Poista käyttäjän porttikielto" - "Näytä profiili" "Porttikiellot" "Jäsenet" "Kutsuttu" - "Poistetaan käyttäjää %1$s huoneesta…" "Ylläpitäjä" "Valvoja" "Huoneen jäsenet" 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 e407265c4e5..559c020986d 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -66,28 +66,19 @@ "Informations du salon" "Sujet" "Mise à jour du salon…" - "Bannir" - "L‘utilisateur ne pourra pas rejoindre le salon à nouveau, même si il est invité." - "Êtes-vous certain de vouloir bannir ce membre ?" "Il n’y a pas d’utilisateur banni dans ce salon." - "Bannissement de %1$s" "%1$d personne" "%1$d personnes" - "Retirer et bannir ce membre" - "Retirer le membre du salon" - "Retirer et bannir ce membre" + "Bannir du salon" "Retirer le membre uniquement" - "Retirer le membre et interdire l’adhésion à l’avenir ?" "Débannir" - "Cet utilisateur pourra rejoindre le salon à nouveau si il est invité." + "Il pourra rejoindre le salon à nouveau si il est invité." "Débannir l’utilisateur" - "Voir le profil" "Bannis" "Membres" "En attente" - "Enlever %1$s…" "Administrateur" "Modérateur" "Membres du salon" 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 1dfc0f50e80..775379ad328 100644 --- a/features/roomdetails/impl/src/main/res/values-hu/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-hu/translations.xml @@ -66,28 +66,19 @@ "Szobainformációk" "Téma" "Szoba frissítése…" - "Kitiltás" - "Többé nem csatlakozhat ehhez a szobához, akkor sem, ha meghívják." - "Biztos, hogy kitiltja ezt a tagot?" "Ebben a szobában nincsenek kitiltott felhasználók." - "%1$s kitiltása" "%1$d személy" "%1$d személy" - "Eltávolítás és a tag kitiltása" - "Eltávolítás a szobából" "Eltávolítás és a tag kitiltása" "Csak a tag eltávolítása" - "Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?" "Tiltás feloldása" "Ehhez a szobához is csatlakozhat, ha meghívják." "Felhasználó tiltásának feloldása" - "Profil megtekintése" "Kitiltva" "Tagok" "Függőben" - "%1$s eltávolítása…" "Adminisztrátor" "Moderátor" "Szoba tagjai" @@ -139,7 +130,7 @@ Nem javasoljuk a titkosítás engedélyezését az olyan szobákban, amelyeket b "A terek jelenleg nem támogatottak" "A tér tagjai" "Szüksége lesz egy szobacímre, hogy láthatóvá váljon a szobakatalógusban." - "Szoba címe" + "A szoba címe" "A szoba megtalálhatóvá tétele a(z) %1$s nyilvános szobakatalógusában való kereséssel." "Látható a nyilvános szobakatalógusban" "Bárki" 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 24c6fd1f0a0..2143418fb0d 100644 --- a/features/roomdetails/impl/src/main/res/values-in/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-in/translations.xml @@ -1,5 +1,7 @@ + "Anda akan memerlukan alamat ruangan untuk membuatnya terlihat dalam direktori." + "Alamat ruangan" "Terjadi kesalahan saat memperbarui pengaturan pemberitahuan." "Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda mungkin tidak diberi tahu dalam beberapa ruangan." "Pemungutan suara" @@ -7,11 +9,11 @@ "Cekal orang-orang" "Hapus pesan" "Semua orang" - "Undang orang-orang" + "Undang orang-orang dan terima permintaan untuk bergabung" "Moderasi anggota" "Pesan dan konten" "Admin dan moderator" - "Keluarkan orang-orang" + "Keluarkan orang-orang dan tolak permintaan untuk bergabung" "Ubah avatar ruangan" "Detail ruangan" "Ubah nama ruangan" @@ -54,35 +56,28 @@ "Bawaan" "Notifikasi" "Pesan yang disematkan" + "Profil" "Permintaan untuk bergabung" "Peran dan perizinan" "Nama ruangan" + "Keamanan & privasi" "Keamanan" "Bagikan ruangan" "Info ruangan" "Topik" "Memperbarui ruangan…" - "Cekal" - "Mereka tidak akan dapat bergabung ke ruangan ini lagi jika diundang." - "Apakah Anda yakin ingin mencekal anggota ini?" "Tidak ada pengguna yang dicekal dalam ruangan ini." - "Mencekal %1$s" "%1$d orang" - "Keluarkan dan cekal anggota" - "Keluarkan dari ruangan" "Keluarkan dan cekal anggota" "Hanya keluarkan anggota" - "Keluarkan pengguna dan cekal pengguna bergabung lagi di masa mendatang?" "Batalkan pencekalan" "Pengguna dapat bergabung ke ruangan ini lagi jika diundang." "Batalkan pencekalan pengguna" - "Tampilkan profil" "Tercekal" "Anggota" "Tertunda" - "Mengeluarkan %1$s…" "Admin" "Moderator" "Anggota ruangan" @@ -115,4 +110,36 @@ "Peran" "Detail ruangan" "Peran dan perizinan" + "Tambahkan alamat ruangan" + "Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut." + "Minta untuk bergabung" + "Ya, aktifkan enkripsi" + "Setelah diaktifkan, encryption untuk sebuah ruangan tidak dapat dinonaktifkan, Riwayat pesan hanya akan terlihat oleh anggota ruangan sejak mereka diundang atau sejak mereka bergabung dengan ruangan tersebut. +Tidak ada orang lain selain anggota ruangan yang dapat membaca pesan. Hal ini dapat mencegah bot dan jembatan bekerja dengan benar. +Kami tidak menyarankan untuk mengaktifkan enkripsi untuk ruangan yang dapat ditemukan dan diikuti oleh siapa pun." + "Aktifkan enkripsi?" + "Setelah diaktifkan, enkripsi tidak dapat dinonaktifkan." + "Enkripsi" + "Aktifkan enkripsi ujung ke ujung" + "Siapa pun dapat menemukan dan bergabung" + "Siapa pun" + "Orang hanya dapat bergabung jika mereka diundang" + "Hanya undangan" + "Akses ruangan" + "Space saat ini tidak didukung" + "Anggota space" + "Anda memerlukan alamat ruangan agar dapat membuatnya terlihat di direktori ruangan." + "Alamat ruangan" + "Izinkan ruangan ini ditemukan dengan mencari direktori ruangan %1$s publik" + "Terlihat di direktori ruangan publik" + "Siapa pun" + "Siapa yang bisa membaca riwayat" + "Hanya anggota sejak mereka diundang" + "Hanya anggota sejak memilih opsi ini" + "Alamat ruangan adalah cara untuk menemukan dan mengakses ruangan. Ini juga memastikan Anda dapat dengan mudah berbagi ruangan dengan orang lain. Anda dapat memilih untuk menerbitkan ruangan Anda di direktori ruangan publik homeserver Anda." + "Penerbitan ruangan" + "Alamat ruangan adalah cara untuk menemukan dan mengakses ruangan. Hal ini juga memastikan Anda dapat dengan mudah berbagi ruangan dengan orang lain. +Alamat ini juga diperlukan untuk membuat ruangan terlihat di %1$s direktori ruangan publik." + "Keterlihatan ruangan" + "Keamanan & privasi" 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 5ba1ecea095..54de5f5407c 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -66,28 +66,19 @@ "Informazioni sulla stanza" "Argomento" "Aggiornamento della stanza…" - "Escludi" - "Non potrà entrare nuovamente in questa stanza se invitato." - "Vuoi davvero escludere questo membro?" "Non ci sono utenti esclusi in questa stanza." - "Esclusione di %1$s" "1 persona" "%1$d persone" - "Rimuovi ed escludi" - "Rimuovi dalla stanza" "Rimuovi ed escludi" "Rimuovi soltanto" - "Rimuovere e vietare l\'accesso in futuro?" "Riammetti" "Potrà entrare nuovamente in questa stanza se invitato." "Riammetti utente" - "Visualizza profilo" "Esclusi" "Membri" "In attesa" - "Rimozione di %1$s…" "Amministratore" "Moderatore" "Membri della stanza" 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 65ce26797e5..6a68985d875 100644 --- a/features/roomdetails/impl/src/main/res/values-ka/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ka/translations.xml @@ -3,7 +3,34 @@ "შეტყობინებების პარამეტრის განახლებისას მოხდა შეცდომა." "თქვენი სახლის სერვერი არ უჭერს მხარს ამ პარამეტრს დაშიფრულ ოთახებში, ზოგიერთ ოთახში შეიძლება არ მიიღოთ შეტყობინება." "გამოკითხვები" + "მხოლოდ ადმინისტრატორები" + "მომხმარებლების დაბლოკვა" + "შეტყობინებების წაშლა" "ყველა" + "მომხმარებლების მოწვევა და გაწევრიანების მოთხოვნების დადასტურება" + "წევრების მოდერირება" + "შეტყობინებები და შინაარსი" + "ადმინისტრატორები და მოდერატორები" + "მომხმარებლების გაგდება და გაწევრიანების მოთხოვნების უარყოფა" + "ოთახის სურათის შეცვლა" + "ოთახის დეტალები" + "ოთახის სახელის შეცვლა" + "ოთახის თემის შეცვლა" + "შეტყობინებების გაგზავნა" + "ადმინისტრატორების რედაქტირება" + "ამ მოქმედების გაუქმებას ვერ შეძლებთ. თქვენ ნიშნავთ ამ მომხმარებელს იმავე ძალაუფლების დონეზე, რომელიც გაქვთ თქვენ." + "ადმინისტრატორის დამატება?" + "დაქვეითება" + "იმის გამო, რომ აქვეითებთ თქვენ თავს, ამ მოქმედებას ვერ გააუქმებთ. პრივილეგიების აღდგენა შეუძლებელია თუ თქვენ ბოლო პრივილეგირებული მომხმარებელი ხართ ამ ოთახში." + "გსურთ საკუთარი თავის დაქვეითება?" + "%1$s (მოლოდინი)" + "(მოლოდინში)" + "მოდერატორების რედაქტირება" + "ადმინისტრატორები" + "მოდერატორები" + "წევრები" + "თქვენ გაქვთ შეუნახავი ცვლილებები" + "შენახვა?" "თემის დამატება" "უკვე წევრია" "უკვე მოწვეულია" @@ -16,25 +43,39 @@ "ამ ოთახის დადუმება ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა." "ამ ოთახის დადუმების მოხსნა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა." "ხალხის მოწვევა" + "საუბრის დატოვება" "ოთახის დატოვება" "მორგებული" "ნაგულისხმევი" "შეტყობინებები" + "როლები და ნებართვები" "ოთახის სახელი" "უსაფრთხოება" "ოთახის გაზიარება" + "ოთახის ინფორმაცია" "თემა" "ოთახის განახლება…" + "ამ ოთახში არაა დაბლოკილი მომხმარებლები." "%1$d ადამიანი" "%1$d ადამიანი" + "წევრის წაშლა და დაბლოკვა" + "მხოლოდ წევრის წაშლა" + "განბლოკვა" + "მოწვევის შემთხვევაში განბლოკილი მომხმარებელი ისევ შეძლებს ოთახს შეუერთდეს." + "მომხმარებლის განბლოკვა" + "დაბლოკილები" + "წევრები" "მომლოდინე" + "ადმინისტრატორი" + "მოდერატორი" "ოთახის წევრები" + "%1$s-ს განბლოკვა" "მორგებული პარამეტრის დაშვება" "ამის ჩართვა უგულებელყოფს თქვენს ნაგულისხმევ პარამეტრს" "ამ ჩატში ჩემი შეტყობინება:" - "თქვენ შეგიძლიათ შეცვალოთ იგი თქვენს %1$s ." + "თქვენ შეგიძლიათ შეცვალოთ იგი თქვენს %1$s." "გლობალური პარამეტრები" "Სტანდარტული პარამეტრები" "მორგებული პარამეტრის წაშლა" @@ -45,4 +86,18 @@ "ყველა შეტყობინება" "მხოლოდ ხსენებები და საკვანძო სიტყვები" "ამ ოთახში, შემატყობინეთ:" + "ადმინისტრატორები" + "ჩემი როლის შეცვლა" + "დაქვეითება წევრამდე" + "დაქვეითება მოდერატორამდე" + "წევრების მოდერირება" + "შეტყობინებები და შინაარსი" + "მოდერატორები" + "ნებართვები" + "ნებართვების გადაყენება" + "ნებართვების გადაყენების შემთხვევაში მიმდინარე პარამეტრებს დაკარგავთ." + "გადავაყენოთ ცვლილებები?" + "როლები" + "ოთახის დეტალები" + "როლები და ნებართვები" diff --git a/features/roomdetails/impl/src/main/res/values-lt/translations.xml b/features/roomdetails/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..792ce9cf825 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,25 @@ + + + "Pridėti temą" + "Jau narys" + "Jau pakviestas" + "Redaguoti kambarį" + "Įvyko nežinoma klaida ir informacijos pakeisti nepavyko." + "Nepavyko atnaujinti kambario" + "Žinutės yra užrakintos. Tik Jūs ir gavėjai turite unikalius raktus joms atrakinti." + "Įjungtas žinučių šifravimas" + "Pakviesti žmonių" + "Palikti kambarį" + "Kambario pavadinimas" + "Saugumas" + "Bendrinti kambarį" + "Tema" + "Atnaujinamas kambarys…" + + "%1$d asmuo" + "%1$d asmenys" + "%1$d asmenų" + + "Laukiama" + "Kambario nariai" + 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 573622f3b62..a8f5d8d1230 100644 --- a/features/roomdetails/impl/src/main/res/values-nb/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-nb/translations.xml @@ -1,6 +1,36 @@ + "Du trenger en adresse til rommet for å gjøre det synlig i katalogen." + "Romadresse" + "Det oppstod en feil under oppdatering av varslingsinnstillingen." + "Hjemmeserveren din støtter ikke dette alternativet i krypterte rom, og det kan hende at du ikke blir varslet i enkelte rom." + "Avstemninger" + "Kun for administratorer" + "Forby folk" + "Fjern meldinger" + "Alle" + "Inviter folk og godta forespørsler om å bli med" + "Moderering av medlemmer" + "Meldinger og innhold" + "Administratorer og moderatorer" + "Fjern folk og avslå forespørsler om å bli med" + "Endre romavatar" + "Romdetaljer" + "Endre romnavn" + "Endre temaet til rommet" + "Send meldinger" + "Rediger administratorer" + "Du vil ikke kunne angre denne handlingen. Du forfremmer brukeren til å ha samme rettighetsnivå som deg." + "Legg til administrator?" + "Degradere" + "Du vil ikke kunne angre denne endringen ettersom du degraderer deg selv, og hvis du er den siste privilegerte brukeren i rommet, vil det være umulig å få tilbake privilegiene." + "Degradere deg selv?" + "%1$s (Venter)" + "(Venter)" + "Administratorer har automatisk moderatorrettigheter" + "Rediger moderatorer" "Administratorer" + "Moderatorer" "Medlemmer" "Du har endringer som ikke er lagret." "Lagre endringer?" @@ -15,40 +45,40 @@ "Kan ikke oppdatere rommet" "Meldingene er krypterte. Det er bare du og mottakerne som har de unike nøklene til å låse dem opp." "Meldingskryptering aktivert" + "Det oppstod en feil ved lasting av varslingsinnstillinger." + "Mislyktes i å dempe dette rommet, prøv igjen." + "Mislyktes i å oppheve dempingen av dette rommet, prøv igjen." "Inviter folk" "Forlat samtalen" "Forlat rommet" "Medier og filer" "Tilpasset" "Standard" + "Varslinger" + "Festede meldinger" + "Profil" + "Forespørsler om å bli med" + "Roller og tillatelser" "Romnavn" + "Sikkerhet og personvern" "Sikkerhet" "Del rom" "Informasjon om rommet" "Emne" "Oppdaterer rommet …" - "Utesteng" - "De vil ikke kunne bli med i dette rommet igjen hvis de blir invitert." - "Er du sikker på at du vil utestenge dette medlemmet?" "Det er ingen utestengte brukere i dette rommet." - "Utestenger %1$s" "%1$d person" "%1$d personer" - "Fjern og utesteng medlem" - "Fjern fra rommet" "Fjern og utesteng medlem" "Bare fjern medlem" - "Fjerne medlem og utestenge fra å bli med i fremtiden?" "Opphev utestengelse" "De vil kunne bli med i dette rommet igjen hvis de blir invitert." "Opphev utestengelse av bruker" - "Vis profil" "Utestengt" "Medlemmer" "Venter" - "Fjerner %1$s…" "Administrator" "Moderator" "Medlemmer av rommet" @@ -56,14 +86,62 @@ "Tillat egendefinert innstilling" "Hvis du slår på dette, overstyrer du standardinnstillingen" "Varsle meg i denne chatten om" - "Du kan endre det i din %1$s ." + "Du kan endre det i din %1$s." "globale innstillinger" "Standard innstilling" "Fjern egendefinert innstilling" + "Det oppstod en feil ved innlasting av varslingsinnstillinger." + "Gjenoppretting av standardmodus mislyktes, prøv igjen." + "Innstilling av modus mislyktes, prøv igjen." + "Hjemmeserveren din støtter ikke dette alternativet i krypterte rom, og du vil ikke bli varslet i dette rommet." "Alle meldinger" + "Bare omtaler og nøkkelord" "I dette rommet, varsle meg om" "Administratorer" "Endre rollen min" + "Nedgradere til medlem" + "Nedgradere til moderator" + "Moderering av medlemmer" + "Meldinger og innhold" + "Moderatorer" + "Tillatelser" + "Tilbakestill tillatelser" + "Når du har tilbakestilt tillatelsene, mister du gjeldende innstillinger." + "Vil du tilbakestille tillatelsene?" + "Roller" + "Romdetaljer" + "Roller og tillatelser" + "Legg til romadresse" + "Alle kan be om å bli med i rommet, men en administrator eller moderator må godta forespørselen." + "Be om å bli med" + "Ja, aktiver kryptering" + "Når kryptering for et rom er aktivert, kan den ikke deaktiveres. Meldingshistorikken vil bare være synlig for rommedlemmer siden de ble invitert eller siden de ble med i rommet. +Ingen andre enn rommedlemmene vil kunne lese meldingene. Dette kan føre til at bots og broer ikke fungerer som de skal. +Vi anbefaler ikke å aktivere kryptering for rom som hvem som helst kan finne og bli med i." + "Vil du aktivere kryptering?" + "Når kryptering er aktivert, kan det ikke deaktiveres." + "Kryptering" + "Aktiver ende-til-ende-kryptering" + "Alle kan finne og bli med" + "Alle" + "Folk kan bare bli med hvis de er invitert" + "Kun for inviterte" + "Tilgang til rom" "Områder støttes ikke for øyeblikket" "Medlemmer av område" + "Du trenger en adresse til rommet for å gjøre rommet synlig i romkatalogen." + "Romadresse" + "Tillat at dette rommet blir funnet ved å søke %1$s offentlig romkatalog" + "Synlig i offentlig romkatalog" + "Alle" + "Hvem kan lese historikk" + "Medlemmer bare siden de ble invitert" + "Kun medlemmer siden du valgte dette alternativet" + "Romadresser er måter å finne og få tilgang til rom på. Dette sikrer også at du enkelt kan dele rommet ditt med andre. +Du kan velge å publisere rommet ditt i hjemeserverens offentlige romkatalog." + "Publisering av rom" + "Romadresser er en måte å finne og få tilgang til rommene på. Dette sikrer også at du enkelt kan dele rommet ditt med andre. +Adressen er også nødvendig for å gjøre rommet synlig i den offentlige romkatalogen på %1$s." + "Romsynlighet" + "Sikkerhet og personvern" 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 6c677ea68ee..2b190fc4db7 100644 --- a/features/roomdetails/impl/src/main/res/values-nl/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-nl/translations.xml @@ -7,11 +7,11 @@ "Personen verbannen" "Berichten verwijderen" "Iedereen" - "Personen uitnodigen" + "Nodig personen uit en accepteer verzoeken om deel te nemen" "Moderatie van leden" "Berichten en inhoud" "Beheerders en moderators" - "Personen verwijderen" + "Verwijder personen en weiger verzoeken om deel te nemen" "Kamerafbeelding wijzigen" "Kamergegevens" "Kamernaam wijzigen" @@ -48,7 +48,7 @@ "Het dempen opheffen voor deze kamer is mislukt. Probeer het opnieuw." "Mensen uitnodigen" "Gesprek verlaten" - "Ruimte verlaten" + "Kamer verlaten" "Aangepast" "Standaard" "Meldingen" @@ -60,28 +60,19 @@ "Kamer info" "Onderwerp" "Kamer bijwerken…" - "Verbannen" - "Ze kunnen niet meer toetreden tot deze kamer als ze worden uitgenodigd." - "Weet je zeker dat je dit lid wilt verbannen?" "Er zijn geen verbannen gebruikers in deze kamer." - "%1$s verbannen" "%1$d persoon" "%1$d personen" - "Lid verwijderen en verbannen" - "Verwijderen uit kamer" "Lid verwijderen en verbannen" "Alleen lid verwijderen" - "Lid verwijderen en toekomstige deelname verbieden?" "Ontbannen" "Ze kunnen opnieuw tot de kamer toetreden als ze worden uitgenodigd." "Ontban gebruiker" - "Profiel bekijken" "Verbannen" "Leden" "In behandeling" - "%1$s wordt verwijderd…" "Beheerder" "Moderator" "Kamerleden" @@ -114,4 +105,7 @@ "Rollen" "Kamergegevens" "Rollen en rechten" + "Vraag om toe te treden" + "Iedereen" + "Iedereen" 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 e2dec9da494..d13337096de 100644 --- a/features/roomdetails/impl/src/main/res/values-pl/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-pl/translations.xml @@ -1,5 +1,7 @@ + "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" @@ -7,11 +9,11 @@ "Banowanie osób" "Usuwanie wiadomości" "Wszyscy" - "Zapraszanie osób" + "Zapraszanie osób i akceptowanie próśb o dołączenie" "Moderacja członków" "Wiadomości i zawartość" "Administratorzy i moderatorzy" - "Usuwanie osób" + "Usuwanie osób i odrzucanie próśb o dołączenie" "Zmień awatar pokoju" "Szczegóły pokoju" "Zmień nazwę pokoju" @@ -50,10 +52,11 @@ "Opuść rozmowę" "Opuść pokój" "Media i pliki" - "Niestandardowy" + "Niestandardowe" "Domyślny" "Powiadomienia" "Przypięte wiadomości" + "Profil" "Prośby o dołączenie" "Role i uprawnienia" "Nazwa pokoju" @@ -63,29 +66,20 @@ "Informacje pokoju" "Temat" "Aktualizuję pokój…" - "Zbanuj" - "Nie będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni." - "Czy na pewno chcesz zbanować tego członka?" "W tym pokoju nie ma zbanowanych użytkowników." - "Banowanie %1$s" "%1$d osoba" "%1$d osoby" "%1$d osób" - "Usuń i zbanuj członka" - "Usuń z pokoju" "Usuń i zbanuj członka" "Tylko usuń członka" - "Usunąć członka i zablokować możliwość dołączenia w przyszłości?" "Odbanuj" "Będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni." "Odbanuj użytkownika" - "Wyświetl profil" "Zbanowanych" "Członków" "Oczekujące" - "Usuwanie %1$s…" "Administrator" "Moderator" "Członkowie pokoju" @@ -118,6 +112,7 @@ "Role" "Szczegóły pokoju" "Role i uprawnienia" + "Dodaj adres pokoju" "Każdy może poprosić o dołączenie do pokoju, ale administrator lub moderator będzie musiał zatwierdzić żądanie." "Poproś o dołączenie" "Tak, włącz szyfrowanie" @@ -129,9 +124,25 @@ Odradzamy włączanie szyfrowania dla pokoi, które każdy może znaleźć i do "Szyfrowanie" "Włącz szyfrowanie end-to-end" "Każdy może znaleźć i dołączyć" - "Każdy" + "Wszyscy" "Tylko osoby z zaproszeniem mogą dołączyć" "Tylko zaproszenie" "Dostęp do pokoju" + "Przestrzenie nie są obecnie wspierane" + "Członkowie przestrzeni" + "Aby pokój był widoczny w katalogu pokoi, potrzebny jest adres pokoju." + "Adres pokoju" + "Zezwól na znalezienie tego pokoju wyszukując %1$s w katalogu pokoi publicznych" + "Widoczny w katalogu pokoi publicznych" + "Wszyscy" + "Kto może czytać historię" + "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 pokoju umożliwiają łatwe znalezienie i dołączenie do pokojów. +Dodatkowo adres pokoju jest wymagany, aby pomieszczenie było widoczne w katalogu pokoi publicznych %1$s." + "Widoczność pomieszczenia" "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 f7e4d5f9e0e..5561005e9bb 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 @@ -1,23 +1,33 @@ + "Você precisará de um endereço de sala para torná-lo visível no diretório." + "Endereço da sala" "Ocorreu um erro ao atualizar a configuração de notificação." + "Seu servidor doméstico não suporta esta opção em salas criptografadas. Você pode não ser notificado em algumas salas." "Enquetes" "Somente administradores" "Banir pessoas" "Remover mensagens" "Todos" - "Convidar pessoas" + "Convide pessoas e aceite solicitações de entrada" "Moderação de membros" "Mensagens e conteúdo" "Administradores e moderadores" - "Remover pessoas" + "Remover pessoas e recusar solicitações de entrada" "Alterar avatar da sala" "Detalhes da sala" "Alterar nome da sala" "Alterar tópico da sala" "Enviar mensagens" "Editar administradores" + "Você não poderá desfazer essa ação. Você está promovendo o usuário a ter o mesmo nível de poder que você." "Adicionar administrador?" + "Reduzir privilégio" + "Você não poderá desfazer essa alteração, pois estará se rebaixando. Se você for o último usuário privilegiado na sala, será impossível recuperar os privilégios." + "Reduzir seu próprio privilégio?" + "%1$s (Pendente)" + "(Pendente)" + "Os administradores têm privilégios de moderador automaticamente" "Editar moderadores" "Administradores" "Moderadores" @@ -27,6 +37,9 @@ "Adicionar tópico" "Já é membro" "Já foi convidado" + "Criptografado" + "Não criptografado" + "Sala pública" "Editar sala" "Ocorreu um erro desconhecido e as informações não puderam ser alteradas." "Não foi possível atualizar a sala" @@ -38,34 +51,34 @@ "Convidar pessoas" "Sair da conversa" "Sair da sala" + "Mídia e arquivos" "Personalizado" "Padrão" "Notificações" + "Mensagens fixadas" + "Perfil" + "Solicitações para entrar" "Cargos e permissões" "Nome da sala" + "Segurança e privacidade" "Segurança" "Compartilhar sala" + "Informação da sala" "Tópico" "Atualizando a sala…" - "Banir" - "Tem certeza de que quer banir este membro?" - "Banindo %1$s" + "Não há usuários banidos nesta sala." "%1$d pessoa" "%1$d pessoas" - "Remover e banir membro" - "Remover da sala" "Remover e banir membro" "Somente remover membro" - "Remover membro e banir de entrar novamente no futuro?" "Desbanir" + "Eles poderão entrar nesta sala novamente se forem convidados." "Desbanir usuário" - "Ver perfil" "Banidos" "Membros" "Pendente" - "Removendo %1$s…" "Administrador" "Moderador" "Membros da sala" @@ -80,18 +93,55 @@ "Ocorreu um erro ao carregar as configurações de notificação." "Falha ao restaurar o modo padrão, tente novamente." "Falha ao definir o modo, tente novamente." + "Seu servidor doméstico não suporta esta opção em salas criptografadas, você não será notificado nesta sala." "Todas as mensagens" "Somente menções e palavras-chave" "Nesta sala, notifique-me para" "Administradores" "Alterar meu cargo" + "Rebaixar para membro" + "Rebaixar para moderador" "Moderação de membros" "Mensagens e conteúdo" "Moderadores" "Permissões" "Redefinir permissões" + "Depois de redefinir as permissões, você perderá as configurações atuais." "Redefinir permissões?" "Cargos" "Detalhes da sala" "Cargos e permissões" + "Adicionar endereço da sala" + "Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou moderador terá que aceitar a solicitação." + "Pedir para entrar" + "Sim, habilite a criptografia" + "Uma vez ativada, a criptografia de uma sala não pode ser desativada. O histórico de mensagens só será visível para os membros da sala desde que foram convidados ou desde que entraram na sala. +Ninguém além dos membros da sala poderá ler as mensagens. Isso pode impedir que os bots e as pontes funcionem corretamente. +Não recomendamos que você ative a criptografia para salas que qualquer pessoa possa encontrar e participar." + "Ativar criptografia?" + "Uma vez ativada, a criptografia não poderá ser desativada." + "Criptografia" + "Ativar a criptografia de ponta a ponta" + "Qualquer um pode encontrar e aderir" + "Qualquer pessoa" + "As pessoas só podem participar se forem convidadas" + "Somente para convidados" + "Acesso à sala" + "No momento, não há compatibilidade com espaços" + "Membros do espaço" + "Você precisará de um endereço de sala para torná-lo visível no diretório de salas." + "Endereço da sala" + "Permitir que esta sala seja encontrada pesquisando diretório de salas públicas de %1$s" + "Visível no diretório de salas públicas" + "Qualquer pessoa" + "Quem pode ler o histórico" + "Somente membros, desde que foram convidados" + "Somente para membros após selecionar esta opção" + "Os endereços das salas são formas de encontrar e acessar as salas. Isso também garante que você possa compartilhar facilmente sua sala com outras pessoas. +Você pode optar por publicar sua sala no diretório público de salas do seu servidor doméstico." + "Publicação em sala" + "Os endereços das salas são formas de encontrar e acessar as salas. Isso também garante que você possa compartilhar facilmente sua sala com outras pessoas. +O endereço também é necessário para que você possa ver a sala no diretório público de salas do site %1$s." + "Visibilidade da sala" + "Segurança e privacidade" 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 d4dcb4f7f4d..8eadbeed09c 100644 --- a/features/roomdetails/impl/src/main/res/values-pt/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-pt/translations.xml @@ -1,5 +1,7 @@ + "É necessário um endereço para tornar a sala visível no diretório." + "Endereço da sala" "Erro ao atualizar a configuração de notificação." "O teu servidor não suporta esta opção em salas cifradas, pelo que poderás não ser notificado em algumas salas." "Sondagens" @@ -7,11 +9,11 @@ "Banir pessoas" "Remover mensagens" "Toda a gente" - "Convida pessoas" + "Convidar pessoas e aceitar pedidos de entrada" "Moderação de participantes" "Mensagens e conteúdo" "Administradores e moderadores" - "Remover pessoas" + "Remover pessoas e rejeitar pedidos de entrada" "Alterar o ícone da sala" "Detalhes da sala" "Altera o nome da sala" @@ -54,36 +56,29 @@ "Predefinição" "Notificações" "Mensagens afixadas" + "Perfil" "Pedidos de entrada" "Cargos e permissões" "Nome da sala" + "Segurança e privacidade" "Segurança" "Partilhar sala" "Informação da sala" "Descrição" "A atualizar sala…" - "Banir" - "Não poderão voltar a entrar nesta sala, mesmo se forem convidados." - "Tens a certeza que queres banir este participante?" "Não há nenhum utilizador banido desta sala." - "A banir %1$s" "%1$d pessoa" "%1$d pessoas" - "Remover e banir participante" - "Remover da sala" "Remover e banir participante" "Remover apenas" - "Remover participante e proibir que entre no futuro?" "Anular banimento" "Poderão juntar-se novamente a esta sala se forem convidados." "Anular banimento do utilizador" - "Ver perfil" "Banidos" "Participantes" "Pendente" - "A remover %1$s…" "Administrador" "Moderador" "Participantes" @@ -99,7 +94,7 @@ "Falha ao restaurar o modo predefinido, tenta novamente." "Falha ao definir o modo, tenta novamente." "O teu servidor não suporta esta opção em salas cifradas, pelo que não serás notificado nesta sala." - "Todas as mensagens" + "Qualquer mensagem" "Menções ou palavras-chave" "Nesta sala, notifica-me se" "Administradores" @@ -116,4 +111,37 @@ "Cargos" "Detalhes da sala" "Cargos e permissões" + "Adicionar endereço de sala" + "Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou moderador tem que aceitar o pedido." + "Pedir para participar" + "Sim, ativar cifragem" + "Uma vez ativada, a cifragem não pode ser desativada. O histórico de mensagens só será visível a membros a partir do momento em que foram convidados ou que entraram na sala. +Ninguém além dos membros poderão ler quaisquer mensagens. Isto pode impedir que robôs (\"bots\") e pontes (\"bridges\") funcionem devidamente. +Não recomendamos ativar a cifragem em salas que qualquer pessoa possa encontrar e entrar." + "Ativar cifragem?" + "Uma vez ativada, a cifragem não pode ser desativada." + "Cifragem" + "Ativar cifragem ponta-a-ponta" + "Qualquer pessoa pode encontrar a sala e entrar" + "Qualquer pessoa" + "Só é possível entrar tendo um convite" + "Apenas por convite" + "Acesso à sala" + "Os espaços ainda não estão implementados" + "Membros do espaço" + "É necessário um endereço para tornar a sala visível no diretório." + "Endereço da sala" + "Permite que esta sala seja encontrada através do diretório público do %1$s." + "Visível no diretório público de salas" + "Qualquer pessoa" + "Quem pode ler o histórico de mensagens" + "Apenas membros, desde o momento em que forem convidados" + "Apenas membros, desde o memento em que esta opção for selecionada" + "Estes endereços permitem encontrar e aceder a sala, bem como a sua fácil partilha com outros. +Podes escolher publicar a sala no diretório público do teu servidor." + "Publicar sala" + "Estes endereços permitem encontrar e aceder a sala, bem como a sua fácil partilha com outros. +Além disso, é necessário ter endereço para publicar a sala no diretório público do %1$s." + "Visibilidade da sala" + "Segurança e privacidade" 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 e4ad084733b..622278dc648 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -60,28 +60,19 @@ "Informatii camera" "Subiect" "Se actualizează camera…" - "Interzicere" - "Nu se vor putea alătura din nou acestei camere dacă sunt invitați." - "Sunteți sigur că doriți să interziceți acest membru?" "Nu există utilizatori interziși în această cameră." - "Se interzice %1$s" "o persoană" "%1$d persoane" - "Eliminați și interziceți membrul" - "Înlăturați membrul" "Eliminați și interziceți membrul" "Doar înlăturare" - "Înlăturați membrul și interziceți-i să se alăture în viitor?" "Anulare excludere" "Se vor putea alătura din nou acestei săli dacă sunt invitați." "Anulați interzicerea utilizatorului" - "Vizualizare profil" "Excluși" "Membri" "În așteptare" - "Se elimină %1$s" "Administrator" "Moderator" "Membrii camerei" @@ -114,4 +105,8 @@ "Roluri" "Detaliile camerei" "Roluri și permisiuni" + "Cereți să vă alăturați" + "Criptare" + "Oricine" + "Oricine" 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 611b89a45d8..77d0b94eb39 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -66,29 +66,20 @@ "Информация о комнате" "Тема" "Обновление комнаты…" - "Заблокировать" - "Они не смогут снова присоединиться к этой комнате, если их пригласят." - "Вы уверены, что хотите заблокировать этого участника?" "В этой комнате нет заблокированных пользователей." - "Блокировка %1$s" "%1$d пользователь" "%1$d пользователя" "%1$d пользователей" - "Удалить и заблокировать участника" - "Удалить участника из комнаты" "Удалить и заблокировать участника" "Только удалить участника" - "Удалить участника и запретить присоединяться в будущем?" "Разблокировать" "Они снова смогут присоединиться в эту комнату если их пригласят." "Разбанить пользователя?" - "Посмотреть профиль" "Заблокированные" "Участники" "В ожидании" - "Удаление %1$s…" "Администратор" "Модератор" "Участники комнаты" @@ -123,7 +114,7 @@ "Роли и разрешения" "Добавить адрес комнаты" "Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос." - "Присоединиться" + "Попросить присоединиться" "Да, включить шифрование" "Шифрование комнаты нельзя будет отключить, история сообщений будет видна только участникам комнаты с момента их приглашения или с момента присоединения к комнате. Никто, кроме членов комнаты, не сможет читать сообщения. Это может помешать ботам и мостам работать корректно. 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 4c470cd1a79..a78ccb98851 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -66,29 +66,20 @@ "Informácie o miestnosti" "Téma" "Aktualizácia miestnosti…" - "Zakázať" - "Nebudú sa môcť pripojiť k tejto miestnosti znova ani ak budú pozvaní." - "Ste si istý, že chcete zakázať tohto člena?" "V tejto miestnosti nie sú žiadni zakázaní používatelia." - "Zakazuje sa %1$s" "%1$d osoba" "%1$d osoby" "%1$d osôb" - "Odstrániť a zakázať člena" - "Odstrániť z miestnosti" "Odstrániť a zakázať člena" "Iba odstrániť člena" - "Odstrániť člena a zakázať vstup v budúcnosti?" "Zrušiť zákaz" "V prípade pozvania sa budú môcť znova pripojiť k tejto miestnosti." "Zrušiť zákaz používateľa" - "Zobraziť profil" "Zakázaní" "Členovia" "Čaká sa" - "Odstraňuje sa %1$s…" "Administrátor" "Moderátor" "Členovia miestnosti" 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 829ab87f863..12cec6d6ee2 100644 --- a/features/roomdetails/impl/src/main/res/values-sv/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sv/translations.xml @@ -1,5 +1,7 @@ + "Du behöver en rumsadress för att göra den synlig i katalogen." + "Rumsadress" "Ett fel uppstod vid uppdatering av aviseringsinställningen." "Din hemserver stöder inte det här alternativet i krypterade rum, du kanske inte aviseras i vissa rum." "Omröstningar" @@ -49,39 +51,34 @@ "Bjud in personer" "Lämna konversation" "Lämna rum" + "Media och filer" "Anpassad" "Förval" "Aviseringar" "Fästa meddelanden" + "Profil" + "Begäran om att gå med" "Roller och behörigheter" "Rumsnamn" + "Säkerhet och sekretess" "Säkerhet" "Dela rum" "Rumsinfo" "Ämne" "Uppdaterar rummet …" - "Banna" - "Denne kommer inte att kunna gå med i det här rummet igen om denne bjuds in." - "Är du säker på att du vill banna den här medlemmen?" "Det finns inga bannade användare i det här rummet." - "Bannar %1$s" "%1$d person" "%1$d personer" - "Ta bort och banna medlem" - "Ta bort från rummet" "Ta bort och banna medlem" "Ta bara bort medlem" - "Ta bort medlem och banna från att gå med i framtiden?" "Avbanna" "Denne kommer kunna gå med i rummet igen om denne bjuds in" "Avbanna användare" - "Visa profil" "Bannade" "Medlemmar" "Väntar" - "Tar bort %1$s …" "Admin" "Moderator" "Rumsmedlemmar" @@ -89,7 +86,7 @@ "Tillåt anpassad inställning" "Om du aktiverar detta åsidosätts din standardinställning" "Meddela mig i den här chatten för" - "Du kan ändra det i dina %1$s ." + "Du kan ändra det i dina %1$s." "globala inställningar" "Standardinställning" "Ta bort anpassad inställning" @@ -114,4 +111,37 @@ "Roller" "Rumsdetaljer" "Roller och behörigheter" + "Lägg till rumsadress" + "Vem som helst kan be om att gå med i rummet men en administratör eller moderator måste acceptera begäran." + "Be om att gå med" + "Ja, aktivera kryptering" + "När det är aktiverat kan kryptering för ett rum inte inaktiveras, meddelandehistoriken visas bara för rumsmedlemmar sedan de blev inbjudna eller sedan de gick med i rummet. +Ingen förutom rumsmedlemmarna kommer att kunna läsa meddelanden. Detta kan förhindra att bots och bridges fungerar korrekt. +Vi rekommenderar inte att aktivera kryptering för rum som vem som helst kan hitta och gå med i." + "Aktivera kryptering?" + "Efter aktivering kan kryptering inte inaktiveras." + "Kryptering" + "Aktivera totalsträckskryptering" + "Vem som helst kan hitta och gå med" + "Vem som helst" + "Användare kan bara gå med om de är inbjudna" + "Endast inbjudan" + "Tillgång till rum" + "Utrymmen stöds för närvarande inte" + "Utrymmesmedlemmar" + "Du behöver en rumsadress för att göra den synlig i rumskatalogen." + "Rumsadress" + "Tillåt att detta rum hittas genom att söka i den offentliga rumskatalogen på %1$s" + "Synlig i katalogen för offentliga rum" + "Vem som helst" + "Vem kan läsa historik" + "Endast medlemmar sedan de bjöds in" + "Endast medlemmar sedan det här alternativet har valts" + "Rumsadresser är sätt att hitta och komma åt rum. Detta säkerställer också att du enkelt kan dela ditt rum med andra. +Du kan välja att publicera ditt rum i din hemservers offentliga rumskatalog." + "Rumspublicering" + "Rumsadresser är sätt att hitta och komma åt rum. Detta säkerställer också att du enkelt kan dela ditt rum med andra. +Adressen krävs också för att göra rummet synligt i%1$s katalog för offentliga rum." + "Rumssynlighet" + "Säkerhet och sekretess" diff --git a/features/roomdetails/impl/src/main/res/values-tr/translations.xml b/features/roomdetails/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..67a88458217 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,147 @@ + + + "Dizinde görünür hale getirmek için bir oda adresine ihtiyacınız olacak." + "Oda adresi" + "Bildirim ayarı güncellenirken bir hata oluştu." + "Ana sunucunuz şifreli odalarda bu seçeneği desteklemiyor, bazı odalarda bildirim almayabilirsiniz." + "Anketler" + "Yalnızca yöneticiler" + "İnsanları yasakla" + "Mesajları kaldır" + "Herkes" + "Kişileri davet etme ve katılma isteklerini kabul etme" + "Üye moderasyonu" + "Mesajlar ve içerik" + "Yöneticiler ve moderatörler" + "Kişileri kaldırma ve katılma isteklerini reddetme" + "Oda resmini değiştir" + "Oda bilgileri" + "Oda adını değiştir" + "Oda konusunu değiştir" + "Mesaj gönder" + "Yöneticileri Düzenle" + "Bu eylemi geri alamazsınız. Kullanıcıyı sizinle aynı güç seviyesine sahip olacak şekilde terfi ettiriyorsunuz." + "Yönetici Ekle?" + "Rütbe Düşür" + "Rütbenizi düşürdüğünüz için bu değişikliği geri alamazsınız, eğer odadaki son ayrıcalıklı kullanıcı sizseniz ayrıcalıkları yeniden kazanmanız mümkün olmayacaktır." + "Rütbeni düşür?" + "%1$s (Beklemede)" + "(Beklemede)" + "Yöneticiler otomatik olarak moderatör ayrıcalıklarına sahiptir" + "Moderatörleri Düzenle" + "Yöneticiler" + "Moderatörler" + "Üyeler" + "Kaydedilmemiş değişiklikleriniz var." + "Değişiklikleri Kaydet?" + "Konu ekle" + "Zaten üye" + "Zaten davet edildi" + "Şifrelenmiş" + "Şifrelenmemiş" + "Herkese açık oda" + "Odayı Düzenle" + "Bilinmeyen bir hata oluştu ve bilgiler değiştirilemedi." + "Oda güncellenemiyor" + "Mesajlar kilitlerle güvence altına alınır. Yalnızca siz ve alıcılar, bunların kilidini açmak için benzersiz anahtarlara sahipsiniz." + "Mesaj şifrelemesi etkinleştirildi" + "Bildirim ayarları yüklenirken bir hata oluştu." + "Bu odayı sessize alma başarısız oldu, lütfen tekrar deneyin." + "Bu odanın sesi açılamadı, lütfen tekrar deneyin." + "İnsanları davet et" + "Sohbeti bırak" + "Odadan ayrıl" + "Medya ve dosyalar" + "Özel" + "Varsayılan" + "Bildirimler" + "Sabitlenmiş mesajlar" + "Profil" + "Katılma istekleri" + "Roller ve izinler" + "Oda adı" + "Güvenlik ve gizlilik" + "Güvenlik" + "Oda paylaş" + "Oda bilgisi" + "Konu" + "Oda güncelleniyor…" + "Bu odada yasaklı kullanıcı yok." + + "%1$d kişi" + "%1$d kişi" + + "Üyeyi çıkar ve yasakla" + "Yalnızca üyeyi kaldır" + "Yasağı Kaldır" + "Davet edildikleri takdirde bu odaya tekrar katılabileceklerdir." + "Kullanıcının yasağını kaldır" + "Yasaklandı" + "Üyeler" + "Beklemede" + "Yönetici" + "Moderatör" + "Oda üyeleri" + "Yasak kaldırılıyor %1$s" + "Özel ayarlara izin ver" + "Bunu açmak varsayılan ayarlarınızı geçersiz kılacaktır" + "Bu sohbette bana bildir" + "Bunu %1$s içinde değiştirebilirsiniz." + "genel ayarlar" + "Varsayılan ayar" + "Özel ayarı kaldır" + "Bildirim ayarları yüklenirken bir hata oluştu." + "Varsayılan ayarlar geri yüklenemedi, lütfen tekrar deneyin." + "Ayarlanamadı, lütfen tekrar deneyin." + "Ana sunucunuz şifreli odalarda bu seçeneği desteklemiyor, bu odada bildirim almayacaksınız." + "Tüm mesajlar" + "Yalnızca Bahsetmeler ve Anahtar Kelimeler" + "Bu odada, bana bildir" + "Yöneticiler" + "Rolümü değiştir" + "Üyeliğe düşür" + "Moderatörlüğe düşür" + "Üye moderasyonu" + "Mesajlar ve içerik" + "Moderatörler" + "İzinler" + "İzinleri sıfırla" + "İzinleri sıfırladığınızda, mevcut ayarları kaybedersiniz." + "İzinleri sıfırla?" + "Roller" + "Oda bilgileri" + "Roller ve izinler" + "Oda adresi ekle" + "Herkes odaya katılma isteğinde bulunabilir ancak bir yönetici veya moderatörün isteği kabul etmesi gerekir." + "Katılmak için sor" + "Evet, şifrelemeyi etkinleştir" + "Etkinleştirildikten sonra, bir oda için şifreleme devre dışı bırakılamaz, Mesaj geçmişi yalnızca davet edildiklerinden veya odaya katıldıklarından beri oda üyeleri için görünür olacaktır. +Oda üyeleri dışında hiç kimse mesajları okuyamayacaktır. Bu, botların ve köprülerin düzgün çalışmasını engelleyebilir. +Herkesin bulabileceği ve katılabileceği odalar için şifrelemenin etkinleştirilmesini önermiyoruz." + "Şifrelemeyi etkinleştir?" + "Açıldıktan donra şifreleme kapatılamaz." + "Şifreleme" + "Uçtan uca şifrelemeyi etkinleştir" + "Herkes bulabilir ve katılabilir" + "Herkes" + "İnsanlar yalnızca davet edildiklerinde katılabilirler" + "Yalnızca davet" + "Oda Erişimi" + "Alanlar şu anda desteklenmiyor" + "Alan üyeleri" + "Oda dizininde görünür kılmak için bir oda adresine ihtiyacınız olacaktır." + "Oda adresi" + "Bu odanın %1$s genel oda dizininde arama yapılarak bulunmasına izin verin" + "Genel oda dizininde görünür" + "Herkes" + "Geçmişi kimler okuyabilir ?" + "Sadece üyeler (davet edildiklerinden beri)" + "Bu seçeneği seçtiğinden beri yalnızca üyeler" + "Oda adresleri, odaları bulmanın ve odalara erişmenin yoludur. Bu aynı zamanda odanızı başkalarıyla kolayca paylaşabilmenizi sağlar. +Odanızı ana sunucunuzun genel oda dizininde yayınlamayı seçebilirsiniz." + "Oda yayınlama" + "Oda adresleri, odaları bulmanın ve odalara erişmenin yollarıdır. Bu aynı zamanda odanızı başkalarıyla kolayca paylaşabilmenizi sağlar. +Adres, odayı %1$s genel oda dizininde görünür kılmak için de gereklidir." + "Oda görünürlüğü" + "Güvenlik ve gizlilik" + 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 1bbbcd48174..3f8d33e7208 100644 --- a/features/roomdetails/impl/src/main/res/values-uk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-uk/translations.xml @@ -9,11 +9,11 @@ "Заблоковувати людей" "Вилучати повідомлення" "Усі" - "Запрошувати людей" + "Запрошувати людей і приймати запити на приєднання" "Модерація учасників" "Повідомлення та зміст" "Адміністратори та модератори" - "Вилучати людей" + "Вилучати людей і відхиляти запити на приєднання" "Змінювати аватар кімнати" "Деталі кімнати" "Змінювати назву кімнати" @@ -66,29 +66,20 @@ "Інформація про кімнату" "Тема" "Оновлення кімнати…" - "Заблокувати" - "Він не зможе приєднатися до цієї кімнати знову, якщо його запросять." - "Ви точно хочете заблокувати цього користувача?" "У цій кімнаті немає заблокованих користувачів." - "Блокування %1$s" "%1$d особа" "%1$d особи" "%1$d осіб" - "Вилучити й заблокувати учасника" - "Вилучити з кімнати" "Вилучити й заблокувати учасника" "Лише вилучити учасника" - "Вилучити учасника та заборонити приєднання в майбутньому?" "Розблокувати" "Вони зможуть знову приєднатися до цієї кімнати, якщо їх запросять." "Розблокувати користувача" - "Переглянути профіль" "Заблоковані" "Учасники" "На розгляді" - "Вилучення %1$s…" "Адміністратор" "Модератор" "Учасники кімнати" @@ -123,7 +114,7 @@ "Ролі та дозволи" "Додати адресу кімнати" "Будь-хто може надіслати запит приєднатися до кімнати, але адміністратор або модератор повинні прийняти запит." - "Запит на приєднання" + "Запросити приєднатися" "Так, увімкнути шифрування" "Після ввімкнення шифрування кімнати, його неможливо вимкнути, історію повідомлень бачитимуть лише учасники кімнати, яких було запрошено або які приєдналися до кімнати. Ніхто, крім учасників кімнати, не зможе прочитати повідомлення. Це може перешкоджати коректній роботі ботів і мостів. @@ -133,7 +124,7 @@ "Шифрування" "Увімкнути наскрізне шифрування" "Будь-хто може знайти та приєднатися." - "Будь-хто" + "Кожний" "Люди можуть приєднатися, лише якщо їх запросили" "Лише запрошені" "Доступ до кімнати" @@ -143,7 +134,7 @@ "Адреса кімнати" "Дозвольте, щоб цю кімнату можна було знайти за допомогою пошуку в каталозі загальнодоступних кімнат %1$s " "Видима в каталозі загальнодоступних кімнат" - "Будь-хто" + "Кожний" "Хто може читати історію" "Лише учасники з моменту запрошення" "Лише учасники після вибору цього параметра" diff --git a/features/roomdetails/impl/src/main/res/values-ur/translations.xml b/features/roomdetails/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..00c90107ac8 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,108 @@ + + + "اطلاع کی ترتیب کی تجدید کرتے ہوئے ایک نقص واقع ہوا۔" + "آپ کا منزلی خادم مرموز کردہ کمروں میں اس اختیار کی حمایت نہیں کرت، ہوسکتا ہے کچھ کمروں میں آپ کو مطلع نہ کیا جائے۔" + "رائے شماری ہا" + "صرف منتظمین" + "لوگوں کو محظور کریں" + "پیغامات ہٹائیں" + "ہر کوئی" + "لوگوں کو مدعو کریں اور شمولیت کی درخواستیں قبول کریں" + "ارکان کا اعتدال" + "پیغامات اور مواد" + "منتظمین اور ناظمین" + "لوگوں کو ہٹا دیں اور شمولیت کی درخواستیں مسترد کریں" + "کمرے کا اوتار بدلیں" + "کمرے کی تفصیلات" + "کمرے کا نام بدلیں" + "کمرے کا موضوع بدلیں" + "پیغامات بھیجیں" + "منتظمین میں ترمیم کریں" + "آپ اس کارروائی کو کالعدم نہیں کرسکیں گے۔ آپ صارف کو اپنی جیسی طاقت کی سطح رکھنے کے لئے فروغ دے رہے ہیں۔" + "منتظم شمال کریں؟" + "تنزل کریں" + "آپ اس تبدیلی کو کالعدم نہیں کرسکیں گے کیونکہ آپ اپنے آپ کو تنزل کر رہے ہیں، اگر آپ کمرے میں آخری مراعات یافتہ صارف ہیں تو مراعات پھر حاصل کرنا ناممکن ہو جائے گا۔" + "اپنے آپ کو تنزل کریں؟" + "%1$s (زیر التواء)" + "(زیر التواء)" + "منتظمین کے پاس خودکاراً ناظمین مراعات ہوتی ہیں" + "ناظمین میں ترمیم کریں" + "منتظمین" + "ناظمین" + "اراکین" + "آپکے پاس غیر محفوظ تبدیلیاں ہیں" + "تبدیلیاں محفوظ کریں؟" + "موضوع شامل کریں" + "پہلے سے ہی رکن" + "پہلے سے مدعو شدہ" + "مرموز کردہ" + "رموز کردہ نہیں" + "عوامی کمرہ" + "کمرے میں ترمیم کریں" + "ایک نامعلوم خلل تھا اور معلومات تبدیل نہیں ہوسکی۔" + "کمرے کی تجدید کرنے سے قاصر" + "پیغامات قفلوں کے ساتھ محفوظ ہیں۔ ان کو غیر مقفل کرنے کے لیے صرف آپ اور وصول کنندگان کے پاس منفرد چابیاں ہیں۔" + "پیغام کی مرموزکاری فعال ہے۔" + "اطلاع کی ترتیبات لادتے ہوئے ایک نقص واقع ہوا۔" + "اس کمرے کو خاموش کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" + "اس کمرے کو غیر خاموش کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" + "لوگوں کو مدعو کریں" + "گفتگو چھوڑیں" + "کمرہ چھوڑ دیں" + "حسب ضرورت" + "طے شدہ" + "اطلاعات" + "مثبوتہ پیغامات" + "کردارہا اور اجازتیں" + "کمرے کا نام" + "حفاظت" + "کمرے کا اشتراک کریں" + "کمرے کی معلومات" + "موضوع" + "کمرے کی تجدید کر رہا ہے…" + "اس کمرے میں کوئی محظور صارفین نہیں ہیں۔" + + "%1$d شخص" + "%1$d اشخاص" + + "کمرے سے محظور کریں" + "رکن کو صرف ہٹائیں" + "غیر محظور کریں" + "اگر وہ مدعو کیا جائیں تو وہ دوبارہ اس کمرے میں شامل ہوسکیں گے۔" + "صارف کو غیر محظور کریں" + "محظور" + "اراکین" + "زیر التواء" + "منتظم" + "ناظم" + "کمرے کے ارکان" + "%1$s کو غیر محظور کر رہا ہے" + "حسب ضرورت ترتیب کی اجازت دیں" + "اسے چالو کرنے سے آپکی متعینہ ترتیبات تجاوز گی جائیں گی" + "اس گفتگو میں مجھے مطلع کریں برائے" + "آپ اسے اپنے %1$s میں بدل سکتے ہیں۔" + "عالمی ترتیبات" + "متعین ترتیب" + "حسب ضرورت ترتیب کو ہٹائیں" + "اطلاع کی ترتیبات لادتے ہوئے ایک نقص واقع ہوا۔" + "متعین وضع کو بحال کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" + "وضع ترتیب دینے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" + "آپ کا منزلی خادم مرموز کردہ کمروں میں اس اختیار کی حمایت نہیں کرتا ہے، آپ کو اس کمرے میں مطلع کیا جائے گا۔" + "تمام پیغامات" + "صرف تذکرے اور کلیدی الفاظ" + "اس کمرے میں، مجھے مطلع کریں برائے" + "منتظمین" + "میرا کردار تبدیل کریں" + "تا رکن تنزلی کریں" + "تا ناظم تنزلی کریں" + "ارکان کا اعتدال" + "پیغامات اور مواد" + "ناظمین" + "اجازتیں" + "اجازتیں بحال کریں" + "ایک بار جب آپ اجازتیں بحال کردیں گے، آپ موجودہ ترتیبات کھو دیں گے۔" + "اجازتیں بحال کریں؟" + "کردارہا" + "کمرے کی تفصیلات" + "کردارہا اور اجازتیں" + 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 aa7dae4539a..ac32c3597bc 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 @@ -9,11 +9,11 @@ "管理黑名單" "移除訊息" "所有人" - "邀請夥伴" + "邀請夥伴並接受加入請求" "成員管理" "訊息與內容" "管理員和版主" - "踢出成員" + "移除夥伴並拒絕加入請求" "變更聊天室大頭照" "聊天室資訊" "變更聊天室名稱" @@ -56,6 +56,7 @@ "預設" "通知" "釘選訊息" + "設定檔" "請求加入" "身份與權限" "聊天室名稱" @@ -65,27 +66,18 @@ "聊天室資訊" "主題" "正在更新聊天室…" - "加入黑名單" - "即使收到邀請,他們仍然無法加入聊天室。" - "您確定要將此成員加入黑名單?" "此聊天室沒有黑名單。" - "正在將 %1$s 加入黑名單" "%1$d 位夥伴" - "踢出並加入黑名單" - "踢出聊天室" "踢出並加入黑名單" "僅移除成員" - "移除成員並禁止未來再度加入?" "解除黑名單" "如果收到邀請,他們能再次加入聊天室。" "解除黑名單" - "查看個人檔案" "黑名單" "成員" "待定" - "正在踢出 %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 6446625c260..367741e4ef8 100644 --- a/features/roomdetails/impl/src/main/res/values-zh/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh/translations.xml @@ -66,27 +66,18 @@ "聊天室信息" "主题" "正在更新聊天室……" - "封禁" - "即使受到邀请,他们也无法再次加入聊天室。" - "您确定要封禁该成员吗?" "此聊天室里没有被封禁的用户。" - "封禁 %1$s" "%1$d 人" - "移除并封禁成员" - "从聊天室移除" "移除并封禁成员" "仅移除成员" - "删除成员并禁止重新加入?" "取消封禁" "如果受到邀请,他们可以重新加入聊天室。" "解封用户" - "查看个人资料" "已封禁用户" "成员" "待处理" - "正在移除 %1$s……" "管理员" "协管员" "聊天室成员" @@ -102,7 +93,7 @@ "恢复默认模式失败,请重试。" "设置模式失败,请重试。" "服务器在加密聊天室中不支持此选项,无法在此聊天室收到通知。" - "所有消息" + "全部消息" "仅限提及和关键词" "在这个聊天室,通知我:" "管理员" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 62a7e482954..883f300bff5 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -66,28 +66,19 @@ "Room info" "Topic" "Updating room…" - "Ban" - "They won’t be able to join this room again if invited." - "Are you sure you want to ban this member?" "There are no banned users in this room." - "Banning %1$s" "%1$d person" "%1$d people" - "Remove and ban member" - "Remove from room" - "Remove and ban member" + "Ban from room" "Only remove member" - "Remove member and ban from joining in the future?" "Unban" "They will be able to join this room again if invited." "Unban user" - "View profile" "Banned" "Members" "Pending" - "Removing %1$s…" "Admin" "Moderator" "Room members" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt deleted file mode 100644 index 2bc200a0d8f..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails - -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.RoomMember -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.test.AN_AVATAR_URL -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_ROOM_TOPIC -import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.room.aRoomInfo -import io.element.android.tests.testutils.lambda.lambdaError - -fun aMatrixRoom( - roomId: RoomId = A_ROOM_ID, - displayName: String = A_ROOM_NAME, - rawName: String? = displayName, - topic: String? = A_ROOM_TOPIC, - avatarUrl: String? = AN_AVATAR_URL, - isEncrypted: Boolean = true, - isPublic: Boolean = true, - isDirect: Boolean = false, - joinRule: JoinRule? = null, - notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), - emitRoomInfo: Boolean = false, - canInviteResult: (UserId) -> Result = { lambdaError() }, - canBanResult: (UserId) -> Result = { lambdaError() }, - canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, - userDisplayNameResult: (UserId) -> Result = { lambdaError() }, - userAvatarUrlResult: () -> Result = { lambdaError() }, - setNameResult: (String) -> Result = { lambdaError() }, - setTopicResult: (String) -> Result = { lambdaError() }, - updateAvatarResult: (String, ByteArray) -> Result = { _, _ -> lambdaError() }, - removeAvatarResult: () -> Result = { lambdaError() }, - canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, - getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, -) = FakeMatrixRoom( - roomId = roomId, - displayName = displayName, - topic = topic, - avatarUrl = avatarUrl, - isEncrypted = isEncrypted, - isPublic = isPublic, - isDirect = isDirect, - notificationSettingsService = notificationSettingsService, - canInviteResult = canInviteResult, - canBanResult = canBanResult, - canSendStateResult = canSendStateResult, - userDisplayNameResult = userDisplayNameResult, - userAvatarUrlResult = userAvatarUrlResult, - setNameResult = setNameResult, - setTopicResult = setTopicResult, - updateAvatarResult = updateAvatarResult, - removeAvatarResult = removeAvatarResult, - canUserJoinCallResult = canUserJoinCallResult, - getUpdatedMemberResult = getUpdatedMemberResult, -).apply { - if (emitRoomInfo) { - givenRoomInfo( - aRoomInfo( - name = displayName, - rawName = rawName, - topic = topic, - avatarUrl = avatarUrl, - isDirect = isDirect, - isPublic = isPublic, - joinRule = joinRule, - ) - ) - } -} 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 new file mode 100644 index 00000000000..1de627fd517 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt @@ -0,0 +1,157 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.roomdetails.impl + +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.SessionId +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.StateEventType +import io.element.android.libraries.matrix.api.room.join.JoinRule +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 +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.A_ROOM_TOPIC +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.lambda.lambdaError + +fun aRoom( + sessionId: SessionId = A_SESSION_ID, + roomId: RoomId = A_ROOM_ID, + displayName: String = A_ROOM_NAME, + rawName: String? = displayName, + topic: String? = A_ROOM_TOPIC, + avatarUrl: String? = AN_AVATAR_URL, + canonicalAlias: RoomAlias? = A_ROOM_ALIAS, + isEncrypted: Boolean = true, + isPublic: Boolean = true, + isDirect: Boolean = false, + joinRule: JoinRule? = null, + activeMemberCount: Long = 1, + joinedMemberCount: Long = 1, + invitedMemberCount: Long = 0, + canInviteResult: (UserId) -> Result = { lambdaError() }, + canBanResult: (UserId) -> Result = { lambdaError() }, + canKickResult: (UserId) -> Result = { lambdaError() }, + canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, + userDisplayNameResult: (UserId) -> Result = { lambdaError() }, + userAvatarUrlResult: () -> Result = { lambdaError() }, + canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, + getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, + userRoleResult: () -> Result = { lambdaError() }, + setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, +) = FakeBaseRoom( + sessionId = sessionId, + roomId = roomId, + canInviteResult = canInviteResult, + canBanResult = canBanResult, + canKickResult = canKickResult, + canSendStateResult = canSendStateResult, + userDisplayNameResult = userDisplayNameResult, + userAvatarUrlResult = userAvatarUrlResult, + canUserJoinCallResult = canUserJoinCallResult, + getUpdatedMemberResult = getUpdatedMemberResult, + userRoleResult = userRoleResult, + setIsFavoriteResult = setIsFavoriteResult, + initialRoomInfo = aRoomInfo( + name = displayName, + rawName = rawName, + topic = topic, + avatarUrl = avatarUrl, + canonicalAlias = canonicalAlias, + isDirect = isDirect, + isPublic = isPublic, + isEncrypted = isEncrypted, + joinRule = joinRule, + joinedMembersCount = joinedMemberCount, + activeMembersCount = activeMemberCount, + invitedMembersCount = invitedMemberCount, + ) +) + +fun aJoinedRoom( + sessionId: SessionId = A_SESSION_ID, + roomId: RoomId = A_ROOM_ID, + displayName: String = A_ROOM_NAME, + rawName: String? = displayName, + topic: String? = A_ROOM_TOPIC, + avatarUrl: String? = AN_AVATAR_URL, + canonicalAlias: RoomAlias? = A_ROOM_ALIAS, + isEncrypted: Boolean = true, + isPublic: Boolean = true, + isDirect: Boolean = false, + joinRule: JoinRule? = null, + activeMemberCount: Long = 1, + joinedMemberCount: Long = 1, + invitedMemberCount: Long = 0, + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), + canInviteResult: (UserId) -> Result = { lambdaError() }, + canBanResult: (UserId) -> Result = { lambdaError() }, + canKickResult: (UserId) -> Result = { lambdaError() }, + canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, + userDisplayNameResult: (UserId) -> Result = { lambdaError() }, + userAvatarUrlResult: () -> Result = { lambdaError() }, + setNameResult: (String) -> Result = { lambdaError() }, + setTopicResult: (String) -> Result = { lambdaError() }, + updateAvatarResult: (String, ByteArray) -> Result = { _, _ -> lambdaError() }, + removeAvatarResult: () -> Result = { lambdaError() }, + canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, + getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, + userRoleResult: () -> Result = { lambdaError() }, + kickUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, + banUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, + unBanUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, + updateCanonicalAliasResult: (RoomAlias?, List) -> Result = { _, _ -> lambdaError() }, + publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, + removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, + setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, +) = FakeJoinedRoom( + roomNotificationSettingsService = notificationSettingsService, + setNameResult = setNameResult, + setTopicResult = setTopicResult, + updateAvatarResult = updateAvatarResult, + removeAvatarResult = removeAvatarResult, + kickUserResult = kickUserResult, + banUserResult = banUserResult, + unBanUserResult = unBanUserResult, + updateCanonicalAliasResult = updateCanonicalAliasResult, + publishRoomAliasInRoomDirectoryResult = publishRoomAliasInRoomDirectoryResult, + removeRoomAliasFromRoomDirectoryResult = removeRoomAliasFromRoomDirectoryResult, + baseRoom = aRoom( + sessionId = sessionId, + roomId = roomId, + canInviteResult = canInviteResult, + canBanResult = canBanResult, + canKickResult = canKickResult, + canSendStateResult = canSendStateResult, + userDisplayNameResult = userDisplayNameResult, + userAvatarUrlResult = userAvatarUrlResult, + canUserJoinCallResult = canUserJoinCallResult, + getUpdatedMemberResult = getUpdatedMemberResult, + userRoleResult = userRoleResult, + setIsFavoriteResult = setIsFavoriteResult, + displayName = displayName, + rawName = rawName, + topic = topic, + avatarUrl = avatarUrl, + canonicalAlias = canonicalAlias, + isDirect = isDirect, + isPublic = isPublic, + isEncrypted = isEncrypted, + joinRule = joinRule, + joinedMemberCount = joinedMemberCount, + activeMemberCount = activeMemberCount, + invitedMemberCount = invitedMemberCount, + ) +) 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 81e95cc051e..6c8f29d873b 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 @@ -8,28 +8,25 @@ package io.element.android.features.roomdetails.impl import androidx.lifecycle.Lifecycle -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.TurbineTestContext -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomcall.api.aStandByCallState -import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.userprofile.shared.aUserProfileState +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.RoomMembersState 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 @@ -39,10 +36,13 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_TOPIC import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.EventsRecorder @@ -53,7 +53,7 @@ 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 io.element.android.tests.testutils.withFakeLifecycleOwner +import io.element.android.tests.testutils.testWithLifecycleOwner import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -62,6 +62,7 @@ import org.junit.Rule import org.junit.Test import kotlin.time.Duration.Companion.milliseconds +@Suppress("LargeClass") @ExperimentalCoroutinesApi class RoomDetailsPresenterTest { @get:Rule @@ -72,7 +73,7 @@ class RoomDetailsPresenterTest { } private fun TestScope.createRoomDetailsPresenter( - room: MatrixRoom = aMatrixRoom(), + room: JoinedRoom = aJoinedRoom(), leaveRoomState: LeaveRoomState = aLeaveRoomState(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), @@ -84,6 +85,9 @@ class RoomDetailsPresenterTest { ) ), isPinnedMessagesFeatureEnabled: Boolean = true, + encryptionService: FakeEncryptionService = FakeEncryptionService(), + clipboardHelper: ClipboardHelper = FakeClipboardHelper(), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore() ): RoomDetailsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { @@ -94,6 +98,8 @@ class RoomDetailsPresenterTest { userProfilePresenterFactory = { Presenter { aUserProfileState() } }, + encryptionService = encryptionService, + clipboardHelper = clipboardHelper, ) } } @@ -108,50 +114,44 @@ class RoomDetailsPresenterTest { dispatchers = dispatchers, isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled }, analyticsService = analyticsService, + clipboardHelper = clipboardHelper, + appPreferencesStore = appPreferencesStore, ) } - private suspend fun RoomDetailsPresenter.test(validate: suspend TurbineTestContext.() -> Unit) { - moleculeFlow(RecompositionMode.Immediate) { - withFakeLifecycleOwner(fakeLifecycleOwner) { - present() - } - }.test(validate = validate) - } - @Test - fun `present - initial state is created from room if roomInfo is null`() = runTest { - val room = aMatrixRoom( + fun `present - initial state is created from initial room info`() = runTest { + val room = aJoinedRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room) - presenter.test { - skipItems(1) + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { val initialState = awaitItem() assertThat(initialState.roomId).isEqualTo(room.roomId) - assertThat(initialState.roomName).isEqualTo(room.displayName) - assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl) - assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!)) - assertThat(initialState.memberCount).isEqualTo(room.joinedMemberCount) - assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted) + assertThat(initialState.roomName).isEqualTo(room.info().name) + assertThat(initialState.roomAvatarUrl).isEqualTo(room.info().avatarUrl) + assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.info().topic!!)) + assertThat(initialState.memberCount).isEqualTo(room.info().joinedMembersCount) assertThat(initialState.canShowPinnedMessages).isTrue() - assertThat(initialState.pinnedMessagesCount).isNull() + assertThat(initialState.pinnedMessagesCount).isEqualTo(0) assertThat(initialState.canShowSecurityAndPrivacy).isFalse() + assertThat(initialState.showDebugInfo).isFalse() + + cancelAndIgnoreRemainingEvents() } } @Test - fun `present - initial state is updated with roomInfo if it exists`() = runTest { + fun `present - initial state is updated with a new roomInfo`() = runTest { val roomInfo = aRoomInfo( name = A_ROOM_NAME, - isPublic = true, topic = A_ROOM_TOPIC, avatarUrl = AN_AVATAR_URL, pinnedEventIds = listOf(AN_EVENT_ID), ) - val room = aMatrixRoom( + val room = aJoinedRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, @@ -159,7 +159,7 @@ class RoomDetailsPresenterTest { givenRoomInfo(roomInfo) } val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { skipItems(1) val updatedState = awaitItem() assertThat(updatedState.roomName).isEqualTo(roomInfo.name) @@ -172,16 +172,16 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state with no room name`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( displayName = "", canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { val initialState = awaitItem() - assertThat(initialState.roomName).isEqualTo(room.displayName) + assertThat(initialState.roomName).isEqualTo(room.info().name) cancelAndIgnoreRemainingEvents() } @@ -191,9 +191,7 @@ class RoomDetailsPresenterTest { fun `present - initial state with DM member sets custom DM roomType`() = runTest { val myRoomMember = aRoomMember(A_SESSION_ID) val otherRoomMember = aRoomMember(A_USER_ID_2) - val room = aMatrixRoom( - isEncrypted = true, - isDirect = true, + val room = aJoinedRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, @@ -206,10 +204,17 @@ class RoomDetailsPresenterTest { }, ).apply { val roomMembers = persistentListOf(myRoomMember, otherRoomMember) - givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) + givenRoomMembersState(RoomMembersState.Ready(roomMembers)) + + givenRoomInfo( + aRoomInfo( + isEncrypted = true, + isDirect = true, + ) + ) } val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { val initialState = awaitItem() assertThat(initialState.roomType).isEqualTo( RoomDetailsType.Dm( @@ -223,13 +228,13 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can invite others to room`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { // Initially false assertThat(awaitItem().canInvite).isFalse() // Then the asynchronous check completes and it becomes true @@ -241,13 +246,15 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can not invite others to room`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( canInviteResult = { Result.success(false) }, + canKickResult = { Result.success(false) }, + canBanResult = { Result.success(false) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { assertThat(awaitItem().canInvite).isFalse() cancelAndIgnoreRemainingEvents() @@ -256,13 +263,13 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when canInvite errors`() = runTest { - val room = aMatrixRoom( - canInviteResult = { Result.failure(Throwable("Whoops")) }, + val room = aJoinedRoom( + canInviteResult = { Result.failure(RuntimeException("Whoops")) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { assertThat(awaitItem().canInvite).isFalse() cancelAndIgnoreRemainingEvents() @@ -271,19 +278,21 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can edit one attribute`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_TOPIC -> Result.success(true) StateEventType.ROOM_NAME -> Result.success(false) - else -> Result.failure(Throwable("Whelp")) + else -> Result.failure(RuntimeException("Whelp")) } }, + canBanResult = { Result.success(false) }, + canKickResult = { Result.success(false) }, canInviteResult = { Result.success(false) }, canUserJoinCallResult = { Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { // Initially false assertThat(awaitItem().canEdit).isFalse() // Then the asynchronous check completes and it becomes true @@ -297,17 +306,17 @@ class RoomDetailsPresenterTest { fun `present - initial state when user can edit attributes in a DM`() = runTest { val myRoomMember = aRoomMember(A_SESSION_ID) val otherRoomMember = aRoomMember(A_USER_ID_2) - val room = aMatrixRoom( - isEncrypted = true, - isDirect = true, + val room = aJoinedRoom( canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_TOPIC, StateEventType.ROOM_NAME, StateEventType.ROOM_AVATAR -> Result.success(true) - else -> Result.failure(Throwable("Whelp")) + else -> Result.failure(RuntimeException("Whelp")) } }, + canKickResult = { Result.success(false) }, + canBanResult = { Result.success(false) }, canInviteResult = { Result.success(false) }, canUserJoinCallResult = { Result.success(true) }, getUpdatedMemberResult = { userId -> @@ -319,17 +328,24 @@ class RoomDetailsPresenterTest { }, ).apply { val roomMembers = persistentListOf(myRoomMember, otherRoomMember) - givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) + givenRoomMembersState(RoomMembersState.Ready(roomMembers)) + + givenRoomInfo( + aRoomInfo( + isEncrypted = true, + isDirect = true, + ) + ) } val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { // Initially false assertThat(awaitItem().canEdit).isFalse() // Then the asynchronous check completes, but editing is still disallowed because it's a DM val settledState = awaitItem() assertThat(settledState.canEdit).isFalse() // If there is a topic, it's visible - assertThat(settledState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!)) + assertThat(settledState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.info().topic!!)) cancelAndIgnoreRemainingEvents() } @@ -339,8 +355,7 @@ class RoomDetailsPresenterTest { fun `present - initial state when in a DM with no topic`() = runTest { val myRoomMember = aRoomMember(A_SESSION_ID) val otherRoomMember = aRoomMember(A_USER_ID_2) - val room = aMatrixRoom( - isEncrypted = true, + val room = aJoinedRoom( isDirect = true, topic = null, canSendStateResult = { _, stateEventType -> @@ -348,9 +363,11 @@ class RoomDetailsPresenterTest { StateEventType.ROOM_AVATAR, StateEventType.ROOM_TOPIC, StateEventType.ROOM_NAME -> Result.success(true) - else -> Result.failure(Throwable("Whelp")) + else -> Result.failure(RuntimeException("Whelp")) } }, + userDisplayNameResult = { Result.success(A_USER_NAME) }, + userAvatarUrlResult = { Result.success(AN_AVATAR_URL) }, canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, getUpdatedMemberResult = { userId -> @@ -362,12 +379,20 @@ class RoomDetailsPresenterTest { }, ).apply { val roomMembers = persistentListOf(myRoomMember, otherRoomMember) - givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) + givenRoomMembersState(RoomMembersState.Ready(roomMembers)) + + givenRoomInfo( + aRoomInfo( + isDirect = true, + activeMembersCount = 2, + topic = null, + ) + ) } val presenter = createRoomDetailsPresenter(room) - presenter.test { - skipItems(1) + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { + skipItems(2) // There's no topic, so we hide the entire UI for DMs assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden) @@ -378,22 +403,28 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can edit all attributes`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_TOPIC, StateEventType.ROOM_NAME, StateEventType.ROOM_AVATAR -> Result.success(true) - else -> Result.failure(Throwable("Whelp")) + else -> Result.failure(RuntimeException("Whelp")) } }, + canKickResult = { + Result.success(false) + }, + canBanResult = { + Result.success(false) + }, canInviteResult = { Result.success(false) }, canUserJoinCallResult = { Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { // Initially false assertThat(awaitItem().canEdit).isFalse() // Then the asynchronous check completes and it becomes true @@ -405,22 +436,28 @@ class RoomDetailsPresenterTest { @Test fun `present - initial state when user can edit no attributes`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_TOPIC, StateEventType.ROOM_NAME, StateEventType.ROOM_AVATAR -> Result.success(false) - else -> Result.failure(Throwable("Whelp")) + else -> Result.failure(RuntimeException("Whelp")) } }, + canBanResult = { + Result.success(false) + }, + canKickResult = { + Result.success(false) + }, canInviteResult = { Result.success(false) }, canUserJoinCallResult = { Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { // Initially false, and no further events assertThat(awaitItem().canEdit).isFalse() @@ -430,23 +467,29 @@ class RoomDetailsPresenterTest { @Test fun `present - topic state is hidden when no topic and user has no permission`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = null, canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_AVATAR, StateEventType.ROOM_NAME -> Result.success(true) StateEventType.ROOM_TOPIC -> Result.success(false) - else -> Result.failure(Throwable("Whelp")) + else -> Result.failure(RuntimeException("Whelp")) } }, + canKickResult = { + Result.success(false) + }, + canBanResult = { + Result.success(false) + }, canInviteResult = { Result.success(false) }, canUserJoinCallResult = { Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { // The initial state is "hidden" and no further state changes happen assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden) @@ -456,16 +499,22 @@ class RoomDetailsPresenterTest { @Test fun `present - topic state is 'can add topic' when no topic and user has permission`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = null, canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_AVATAR, StateEventType.ROOM_TOPIC, StateEventType.ROOM_NAME -> Result.success(true) - else -> Result.failure(Throwable("Whelp")) + else -> Result.failure(RuntimeException("Whelp")) } }, + canKickResult = { + Result.success(false) + }, + canBanResult = { + Result.success(false) + }, canInviteResult = { Result.success(false) }, @@ -474,7 +523,7 @@ class RoomDetailsPresenterTest { givenRoomInfo(aRoomInfo(topic = null)) } val presenter = createRoomDetailsPresenter(room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { // Ignore the initial state skipItems(1) @@ -488,7 +537,7 @@ class RoomDetailsPresenterTest { @Test fun `present - leave room event is passed on to leave room presenter`() = runTest { val leaveRoomEventRecorder = EventsRecorder() - val room = aMatrixRoom( + val room = aJoinedRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, @@ -498,7 +547,7 @@ class RoomDetailsPresenterTest { leaveRoomState = aLeaveRoomState(eventSink = leaveRoomEventRecorder), dispatchers = testCoroutineDispatchers() ) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { awaitItem().eventSink(RoomDetailsEvent.LeaveRoom) leaveRoomEventRecorder.assertSingle(LeaveRoomEvent.ShowConfirmation(room.roomId)) cancelAndIgnoreRemainingEvents() @@ -508,7 +557,7 @@ class RoomDetailsPresenterTest { @Test fun `present - notification mode changes`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() - val room = aMatrixRoom( + val room = aJoinedRoom( notificationSettingsService = notificationSettingsService, canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, @@ -518,7 +567,7 @@ class RoomDetailsPresenterTest { room = room, notificationSettingsService = notificationSettingsService, ) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { notificationSettingsService.setRoomNotificationMode( room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY @@ -537,7 +586,7 @@ class RoomDetailsPresenterTest { fun `present - mute room notifications`() = runTest { val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) - val room = aMatrixRoom( + val room = aJoinedRoom( notificationSettingsService = notificationSettingsService, canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, @@ -547,7 +596,7 @@ class RoomDetailsPresenterTest { room = room, notificationSettingsService = notificationSettingsService ) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { awaitItem().eventSink(RoomDetailsEvent.MuteNotification) val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE @@ -565,7 +614,7 @@ class RoomDetailsPresenterTest { initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, initialEncryptedGroupDefaultMode = RoomNotificationMode.ALL_MESSAGES ) - val room = aMatrixRoom( + val room = aJoinedRoom( notificationSettingsService = notificationSettingsService, canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, @@ -575,7 +624,7 @@ class RoomDetailsPresenterTest { room = room, notificationSettingsService = notificationSettingsService ) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification) val updatedState = consumeItemsUntilPredicate { it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES @@ -590,7 +639,7 @@ class RoomDetailsPresenterTest { @Test fun `present - when set is favorite event is emitted, then the action is called`() = runTest { val setIsFavoriteResult = lambdaRecorder> { _ -> Result.success(Unit) } - val room = FakeMatrixRoom( + val room = aJoinedRoom( setIsFavoriteResult = setIsFavoriteResult, canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, @@ -599,7 +648,7 @@ class RoomDetailsPresenterTest { val analyticsService = FakeAnalyticsService() val presenter = createRoomDetailsPresenter(room = room, analyticsService = analyticsService) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { val initialState = awaitItem() initialState.eventSink(RoomDetailsEvent.SetFavorite(true)) setIsFavoriteResult.assertions().isCalledOnce().with(value(true)) @@ -619,13 +668,13 @@ class RoomDetailsPresenterTest { @Test fun `present - changes in room info updates the is favorite flag`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, ) val presenter = createRoomDetailsPresenter(room = room) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { room.givenRoomInfo(aRoomInfo(isFavorite = true)) consumeItemsUntilPredicate { it.isFavorite }.last().let { state -> assertThat(state.isFavorite).isTrue() @@ -640,8 +689,7 @@ class RoomDetailsPresenterTest { @Test fun `present - show knock requests`() = runTest { - val room = aMatrixRoom( - emitRoomInfo = true, + val room = aJoinedRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, @@ -654,32 +702,27 @@ class RoomDetailsPresenterTest { room = room, featureFlagService = featureFlagService, ) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { skipItems(1) - with(awaitItem()) { - assertThat(canShowKnockRequests).isFalse() - } + assertThat(awaitItem().canShowKnockRequests).isFalse() featureFlagService.setFeatureEnabled(FeatureFlags.Knock, true) - with(awaitItem()) { - assertThat(canShowKnockRequests).isTrue() - } + assertThat(awaitItem().canShowKnockRequests).isTrue() room.givenRoomInfo(aRoomInfo(joinRule = JoinRule.Private)) - with(awaitItem()) { - assertThat(canShowKnockRequests).isFalse() - } + assertThat(awaitItem().canShowKnockRequests).isFalse() + cancelAndIgnoreRemainingEvents() } } @Test fun `present - show security and privacy`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canSendStateResult = { _, _ -> Result.success(true) }, ) val featureFlagService = FakeFeatureFlagService() val presenter = createRoomDetailsPresenter(room = room, featureFlagService = featureFlagService) - presenter.test { + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { skipItems(1) with(awaitItem()) { assertThat(canShowSecurityAndPrivacy).isFalse() @@ -690,4 +733,23 @@ class RoomDetailsPresenterTest { } } } + + @Test + fun `present - show debug info`() = runTest { + val room = aJoinedRoom( + canInviteResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canSendStateResult = { _, _ -> Result.success(true) }, + ) + val inMemoryAppPreferencesStore = InMemoryAppPreferencesStore( + isDeveloperModeEnabled = true, + ) + val presenter = createRoomDetailsPresenter(room = room, appPreferencesStore = inMemoryAppPreferencesStore) + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { + skipItems(1) + with(awaitItem()) { + assertThat(showDebugInfo).isTrue() + } + } + } } 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 8cc1af16385..2f4eebcad15 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 @@ -15,7 +15,10 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdetails.impl.members.aRoomMember +import io.element.android.features.userprofile.shared.aUserProfileState +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureCalledOnceWithTwoParams @@ -280,6 +283,20 @@ class RoomDetailsViewTest { eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom) } + @Config(qualifiers = "h1500dp") + @Test + fun `click on report room invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + ), + onReportRoomClick = callback, + ) + rule.clickOn(CommonStrings.action_report_room) + } + } + @Config(qualifiers = "h1024dp") @Test fun `click on knock requests invokes expected callback`() { @@ -294,6 +311,21 @@ class RoomDetailsViewTest { rule.clickOn(R.string.screen_room_details_requests_to_join_title) } } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on profile invokes the expected callback`() { + ensureCalledOnceWithParam(A_USER_ID) { callback -> + rule.setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + roomMemberDetailsState = aUserProfileState(userId = A_USER_ID), + ), + onProfileClick = callback, + ) + rule.clickOn(R.string.screen_room_details_profile_row_title) + } + } } private fun AndroidComposeTestRule.setRoomDetailView( @@ -314,6 +346,8 @@ private fun AndroidComposeTestRule.setRoomD onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), onKnockRequestsClick: () -> Unit = EnsureNeverCalled(), onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(), + onProfileClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), + onReportRoomClick: () -> Unit = EnsureNeverCalled(), ) { setContent { RoomDetailsView( @@ -332,6 +366,8 @@ private fun AndroidComposeTestRule.setRoomD onPinnedMessagesClick = onPinnedMessagesClick, onKnockRequestsClick = onKnockRequestsClick, onSecurityAndPrivacyClick = onSecurityAndPrivacyClick, + onProfileClick = onProfileClick, + onReportRoomClick = onReportRoomClick, ) } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt similarity index 92% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt index 7989ab9b425..bb6a9b30277 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenterTest.kt @@ -5,18 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.edit +package io.element.android.features.roomdetails.impl.edit import android.net.Uri import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat -import io.element.android.features.roomdetails.aMatrixRoom -import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditEvents -import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditPresenter +import io.element.android.features.roomdetails.impl.aJoinedRoom import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_ROOM_NAME @@ -76,7 +74,7 @@ class RoomDetailsEditPresenterTest { } private fun createRoomDetailsEditPresenter( - room: MatrixRoom, + room: JoinedRoom, permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), temporaryUriDeleter: TemporaryUriDeleter = FakeTemporaryUriDeleter(), ): RoomDetailsEditPresenter { @@ -91,11 +89,10 @@ class RoomDetailsEditPresenterTest { @Test fun `present - initial state is created from room info`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( avatarUrl = AN_AVATAR_URL, displayName = A_ROOM_NAME, rawName = A_ROOM_RAW_NAME, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} @@ -108,7 +105,7 @@ class RoomDetailsEditPresenterTest { assertThat(initialState.roomId).isEqualTo(room.roomId) assertThat(initialState.roomRawName).isEqualTo(A_ROOM_RAW_NAME) assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri) - assertThat(initialState.roomTopic).isEqualTo(room.topic.orEmpty()) + assertThat(initialState.roomTopic).isEqualTo(room.info().topic.orEmpty()) assertThat(initialState.avatarActions).containsExactly( AvatarAction.ChoosePhoto, AvatarAction.TakePhoto, @@ -121,13 +118,13 @@ class RoomDetailsEditPresenterTest { @Test fun `present - sets canChangeName if user has permission`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( avatarUrl = AN_AVATAR_URL, canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_NAME -> Result.success(true) StateEventType.ROOM_AVATAR -> Result.success(false) - StateEventType.ROOM_TOPIC -> Result.failure(Throwable("Oops")) + StateEventType.ROOM_TOPIC -> Result.failure(RuntimeException("Oops")) else -> lambdaError() } }, @@ -154,13 +151,13 @@ class RoomDetailsEditPresenterTest { @Test fun `present - sets canChangeAvatar if user has permission`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( avatarUrl = AN_AVATAR_URL, canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_NAME -> Result.success(false) StateEventType.ROOM_AVATAR -> Result.success(true) - StateEventType.ROOM_TOPIC -> Result.failure(Throwable("Oops")) + StateEventType.ROOM_TOPIC -> Result.failure(RuntimeException("Oops")) else -> lambdaError() } } @@ -186,12 +183,12 @@ class RoomDetailsEditPresenterTest { @Test fun `present - sets canChangeTopic if user has permission`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( avatarUrl = AN_AVATAR_URL, canSendStateResult = { _, stateEventType -> when (stateEventType) { StateEventType.ROOM_NAME -> Result.success(false) - StateEventType.ROOM_AVATAR -> Result.failure(Throwable("Oops")) + StateEventType.ROOM_AVATAR -> Result.failure(RuntimeException("Oops")) StateEventType.ROOM_TOPIC -> Result.success(true) else -> lambdaError() } @@ -218,11 +215,10 @@ class RoomDetailsEditPresenterTest { @Test fun `present - updates state in response to changes`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} @@ -264,11 +260,10 @@ class RoomDetailsEditPresenterTest { @Test fun `present - obtains avatar uris from gallery`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(anotherAvatarUri) @@ -289,11 +284,10 @@ class RoomDetailsEditPresenterTest { @Test fun `present - obtains avatar uris from camera`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(anotherAvatarUri) @@ -321,8 +315,7 @@ class RoomDetailsEditPresenterTest { stateWithNewAvatar.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto)) val stateWithNewAvatar2 = awaitItem() assertThat(stateWithNewAvatar2.roomAvatarUrl).isEqualTo(roomAvatarUri) - deleteCallback.assertions().isCalledExactly(4).withSequence( - listOf(value(null)), + deleteCallback.assertions().isCalledExactly(3).withSequence( listOf(value(null)), listOf(value(roomAvatarUri)), listOf(value(anotherAvatarUri)), @@ -332,11 +325,10 @@ class RoomDetailsEditPresenterTest { @Test fun `present - updates save button state`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(roomAvatarUri) @@ -383,11 +375,10 @@ class RoomDetailsEditPresenterTest { @Test fun `present - updates save button state when initial values are null`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = null, displayName = "fallback", avatarUrl = null, - emitRoomInfo = true, canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(roomAvatarUri) @@ -437,11 +428,10 @@ class RoomDetailsEditPresenterTest { val setNameResult = lambdaRecorder { _: String -> Result.success(Unit) } val setTopicResult = lambdaRecorder { _: String -> Result.success(Unit) } val removeAvatarResult = lambdaRecorder> { Result.success(Unit) } - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, setNameResult = setNameResult, setTopicResult = setTopicResult, removeAvatarResult = removeAvatarResult, @@ -467,7 +457,7 @@ class RoomDetailsEditPresenterTest { @Test fun `present - save doesn't change room details if they're the same trimmed`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, @@ -489,7 +479,7 @@ class RoomDetailsEditPresenterTest { @Test fun `present - save doesn't change topic if it was unset and is now blank`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = null, displayName = "Name", avatarUrl = AN_AVATAR_URL, @@ -511,7 +501,7 @@ class RoomDetailsEditPresenterTest { @Test fun `present - save doesn't change name if it's now empty`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, @@ -534,7 +524,7 @@ class RoomDetailsEditPresenterTest { @Test fun `present - save processes and sets avatar when processor returns successfully`() = runTest { val updateAvatarResult = lambdaRecorder { _: String, _: ByteArray -> Result.success(Unit) } - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, @@ -555,21 +545,21 @@ class RoomDetailsEditPresenterTest { updateAvatarResult.assertions().isCalledOnce().with(value(MimeTypes.Jpeg), value(fakeFileContents)) deleteCallback.assertions().isCalledExactly(2).withSequence( listOf(value(null)), - listOf(value(null)), + listOf(value(roomAvatarUri)), ) } } @Test fun `present - save does not set avatar data if processor fails`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, canSendStateResult = { _, _ -> Result.success(true) } ) fakePickerProvider.givenResult(anotherAvatarUri) - fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no"))) + fakeMediaPreProcessor.givenResult(Result.failure(RuntimeException("Oh no"))) val deleteCallback = lambdaRecorder {} val presenter = createRoomDetailsEditPresenter( room = room, @@ -586,65 +576,61 @@ class RoomDetailsEditPresenterTest { @Test fun `present - sets save action to failure if name update fails`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, - setNameResult = { Result.failure(Throwable("!")) }, + setNameResult = { Result.failure(RuntimeException("!")) }, canSendStateResult = { _, _ -> Result.success(true) } ) - saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name")) + saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name"), deleteCallbackNumberOfInvocation = 1) } @Test fun `present - sets save action to failure if topic update fails`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, - setTopicResult = { Result.failure(Throwable("!")) }, + setTopicResult = { Result.failure(RuntimeException("!")) }, canSendStateResult = { _, _ -> Result.success(true) } ) - saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic")) + saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic"), deleteCallbackNumberOfInvocation = 1) } @Test fun `present - sets save action to failure if removing avatar fails`() = runTest { - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, - removeAvatarResult = { Result.failure(Throwable("!")) }, + removeAvatarResult = { Result.failure(RuntimeException("!")) }, canSendStateResult = { _, _ -> Result.success(true) } ) - saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 3) + saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 2) } @Test fun `present - sets save action to failure if setting avatar fails`() = runTest { givenPickerReturnsFile() - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - emitRoomInfo = true, - updateAvatarResult = { _, _ -> Result.failure(Throwable("!")) }, + updateAvatarResult = { _, _ -> Result.failure(RuntimeException("!")) }, canSendStateResult = { _, _ -> Result.success(true) } ) - saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 3) + saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 2) } @Test fun `present - CancelSaveChanges resets save action state`() = runTest { givenPickerReturnsFile() - val room = aMatrixRoom( + val room = aJoinedRoom( topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL, - setTopicResult = { Result.failure(Throwable("!")) }, + setTopicResult = { Result.failure(RuntimeException("!")) }, canSendStateResult = { _, _ -> Result.success(true) } ) val deleteCallback = lambdaRecorder {} @@ -664,7 +650,7 @@ class RoomDetailsEditPresenterTest { } private suspend fun saveAndAssertFailure( - room: MatrixRoom, + room: JoinedRoom, event: RoomDetailsEditEvents, deleteCallbackNumberOfInvocation: Int = 2, ) { @@ -706,6 +692,6 @@ class RoomDetailsEditPresenterTest { } private suspend fun ReceiveTurbine.awaitFirstItem(): T { - skipItems(2) + skipItems(1) return awaitItem() } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditViewTest.kt similarity index 95% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditViewTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditViewTest.kt index d3376d2d06c..2ac3d4397cd 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditViewTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.edit +package io.element.android.features.roomdetails.impl.edit import androidx.activity.ComponentActivity import androidx.annotation.StringRes @@ -18,10 +18,6 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditEvents -import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditState -import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditView -import io.element.android.features.roomdetails.impl.edit.aRoomDetailsEditState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.testtags.TestTags @@ -209,7 +205,7 @@ class RoomDetailsEditViewTest { rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, - saveAction = AsyncAction.Failure(Throwable("Whelp")), + saveAction = AsyncAction.Failure(RuntimeException("Whelp")), ), ) rule.clickOn(CommonStrings.action_ok) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt index 7c15b66630f..5aae0f5d3ba 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt @@ -11,19 +11,19 @@ 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.roomdetails.aMatrixRoom +import io.element.android.features.roomdetails.impl.aRoom import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.aRoomMemberList import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.BaseRoom +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.user.MatrixUser import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.usersearch.api.UserSearchResult @@ -47,7 +47,7 @@ internal class RoomInviteMembersPresenterTest { fun `present - initial state has no results and no search`() = runTest { val presenter = RoomInviteMembersPresenter( userRepository = FakeUserRepository(), - roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + roomMemberListDataSource = createDataSource(FakeBaseRoom()), coroutineDispatchers = testCoroutineDispatchers() ) @@ -69,7 +69,7 @@ internal class RoomInviteMembersPresenterTest { fun `present - updates search active state`() = runTest { val presenter = RoomInviteMembersPresenter( userRepository = FakeUserRepository(), - roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + roomMemberListDataSource = createDataSource(FakeBaseRoom()), coroutineDispatchers = testCoroutineDispatchers() ) @@ -91,7 +91,7 @@ internal class RoomInviteMembersPresenterTest { val repository = FakeUserRepository() val presenter = RoomInviteMembersPresenter( userRepository = repository, - roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + roomMemberListDataSource = createDataSource(FakeBaseRoom()), coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) moleculeFlow(RecompositionMode.Immediate) { @@ -118,7 +118,7 @@ internal class RoomInviteMembersPresenterTest { val repository = FakeUserRepository() val presenter = RoomInviteMembersPresenter( userRepository = repository, - roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + roomMemberListDataSource = createDataSource(FakeBaseRoom()), coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) moleculeFlow(RecompositionMode.Immediate) { @@ -159,9 +159,9 @@ internal class RoomInviteMembersPresenterTest { val presenter = RoomInviteMembersPresenter( userRepository = repository, roomMemberListDataSource = createDataSource( - matrixRoom = FakeMatrixRoom().apply { + room = FakeBaseRoom().apply { givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( persistentListOf( aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), @@ -219,9 +219,9 @@ internal class RoomInviteMembersPresenterTest { val repository = FakeUserRepository() val presenter = RoomInviteMembersPresenter( userRepository = repository, - roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply { + roomMemberListDataSource = createDataSource(FakeBaseRoom().apply { givenRoomMembersState( - MatrixRoomMembersState.Ready( + RoomMembersState.Ready( persistentListOf( aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), @@ -295,7 +295,7 @@ internal class RoomInviteMembersPresenterTest { val repository = FakeUserRepository() val presenter = RoomInviteMembersPresenter( userRepository = repository, - roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + roomMemberListDataSource = createDataSource(FakeBaseRoom()), coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) moleculeFlow(RecompositionMode.Immediate) { @@ -336,7 +336,7 @@ internal class RoomInviteMembersPresenterTest { val repository = FakeUserRepository() val presenter = RoomInviteMembersPresenter( userRepository = repository, - roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + roomMemberListDataSource = createDataSource(FakeBaseRoom()), coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) moleculeFlow(RecompositionMode.Immediate) { @@ -397,11 +397,11 @@ internal class RoomInviteMembersPresenterTest { } private fun TestScope.createDataSource( - matrixRoom: MatrixRoom = aMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + room: BaseRoom = aRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) }, coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() - ) = RoomMemberListDataSource(matrixRoom, coroutineDispatchers) + ) = RoomMemberListDataSource(room, coroutineDispatchers) private fun SearchBarResultState>.users() = (this as? SearchBarResultState.Results>)?.results.orEmpty() diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/PowerLevelBaseRoomMemberComparatorTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/PowerLevelBaseRoomMemberComparatorTest.kt new file mode 100644 index 00000000000..80ada2e7b68 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/PowerLevelBaseRoomMemberComparatorTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.roomdetails.impl.members + +import io.element.android.libraries.matrix.api.core.UserId +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_ID_3 +import io.element.android.libraries.matrix.test.A_USER_ID_4 +import io.element.android.libraries.matrix.test.A_USER_ID_5 +import org.junit.Test + +class PowerLevelBaseRoomMemberComparatorTest { + @Test + fun `order is Admin, then Moderator, then User`() { + val memberList = listOf( + aRoomMember(userId = UserId("@admin:example.com"), powerLevel = 100), + aRoomMember(userId = UserId("@moderator:example.com"), powerLevel = 50), + aRoomMember(userId = UserId("@user:example.com"), powerLevel = 0), + ).shuffled() + + val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) + assert(ordered[0].userId == UserId("@admin:example.com")) + assert(ordered[1].userId == UserId("@moderator:example.com")) + assert(ordered[2].userId == UserId("@user:example.com")) + } + + @Test + fun `with the same power level, alphabetical ascending order for name is used`() { + val memberList = listOf( + aRoomMember(userId = A_USER_ID, displayName = "First - admin", powerLevel = 100), + aRoomMember(userId = A_USER_ID_2, displayName = "Second - admin", powerLevel = 100), + aRoomMember(userId = A_USER_ID_3, displayName = "Third - admin", powerLevel = 100), + aRoomMember(userId = A_USER_ID_4, displayName = "First - user", powerLevel = 0), + aRoomMember(userId = A_USER_ID_5, displayName = "Second - user", powerLevel = 0), + ).shuffled() + + val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) + assert(ordered[0].userId == A_USER_ID) + assert(ordered[1].userId == A_USER_ID_2) + assert(ordered[2].userId == A_USER_ID_3) + assert(ordered[3].userId == A_USER_ID_4) + assert(ordered[4].userId == A_USER_ID_5) + } + + @Test + fun `when no names are provided, alphabetical order uses user id`() { + val memberList = listOf( + aRoomMember(userId = A_USER_ID, displayName = "Z - LAST!", powerLevel = 100), + aRoomMember(userId = A_USER_ID_2, powerLevel = 100), + aRoomMember(userId = A_USER_ID_3, powerLevel = 100), + ).shuffled() + + val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) + assert(ordered[0].userId == A_USER_ID_2) + assert(ordered[1].userId == A_USER_ID_3) + assert(ordered[2].userId == A_USER_ID) + } + + @Test + fun `unicode characters are simplified and compared, order ignores case`() { + val memberList = listOf( + aRoomMember(userId = A_USER_ID, displayName = "First", powerLevel = 100), + aRoomMember(userId = A_USER_ID_2, displayName = "Șecond", powerLevel = 100), + aRoomMember(userId = A_USER_ID_3, displayName = "third", powerLevel = 100), + ).shuffled() + + val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) + assert(ordered[0].userId == A_USER_ID) + assert(ordered[1].userId == A_USER_ID_2) + assert(ordered[2].userId == A_USER_ID_3) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt new file mode 100644 index 00000000000..aff05a55e61 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt @@ -0,0 +1,253 @@ +/* + * 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. + */ + +package io.element.android.features.roomdetails.impl.members + +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.roommembermoderation.api.RoomMemberModerationState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.RoomMembersState +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class RoomMemberListPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `member loading is done automatically on start, but is async`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + updateMembersResult = { Result.success(Unit) }, + canInviteResult = { Result.success(true) } + ).apply { + // Needed to avoid discarding the loaded members as a partial and invalid result + givenRoomInfo(aRoomInfo(joinedMembersCount = 2)) + } + ) + val presenter = createPresenter(joinedRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.roomMembers.isLoading()).isTrue() + assertThat(initialState.searchQuery).isEmpty() + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) + assertThat(initialState.isSearchActive).isFalse() + room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + // Skip item while the new members state is processed + skipItems(1) + val loadedMembersState = awaitItem() + assertThat(loadedMembersState.roomMembers.isLoading()).isFalse() + assertThat(loadedMembersState.roomMembers.dataOrNull()?.invited) + .isEqualTo(listOf(RoomMemberWithIdentityState(aVictor(), null), RoomMemberWithIdentityState(aWalter(), null))) + assertThat(loadedMembersState.roomMembers.dataOrNull()?.joined).isNotEmpty() + } + } + + @Test + fun `open search`() = runTest { + val presenter = createPresenter( + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + updateMembersResult = { Result.success(Unit) }, + canInviteResult = { Result.success(true) } + ) + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) + skipItems(1) + val searchActiveState = awaitItem() + assertThat(searchActiveState.isSearchActive).isTrue() + } + } + + @Test + fun `search for something which is not found`() = runTest { + val presenter = createPresenter( + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + updateMembersResult = { Result.success(Unit) }, + canInviteResult = { Result.success(true) } + ) + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) + val searchActiveState = awaitItem() + searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something")) + skipItems(1) + val searchQueryUpdatedState = awaitItem() + assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something") + val searchSearchResultDelivered = awaitItem() + assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) + } + } + + @Test + fun `search for something which is found`() = runTest { + val presenter = createPresenter( + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + updateMembersResult = { Result.success(Unit) }, + canInviteResult = { Result.success(true) } + ) + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) + val searchActiveState = awaitItem() + searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("Alice")) + skipItems(1) + val searchQueryUpdatedState = awaitItem() + assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("Alice") + val searchSearchResultDelivered = awaitItem() + assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + assertThat((searchSearchResultDelivered.searchResults as SearchBarResultState.Results).results.dataOrNull()!!.joined.first().roomMember.displayName) + .isEqualTo("Alice") + } + } + + @Test + fun `present - asynchronously sets canInvite when user has correct power level`() = runTest { + val presenter = createPresenter( + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canInviteResult = { Result.success(true) }, + updateMembersResult = { Result.success(Unit) } + ) + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + assertThat(loadedState.canInvite).isTrue() + } + } + + @Test + fun `present - asynchronously sets canInvite when user does not have correct power level`() = runTest { + val presenter = createPresenter( + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canInviteResult = { Result.success(false) }, + updateMembersResult = { Result.success(Unit) } + ) + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + assertThat(loadedState.canInvite).isFalse() + } + } + + @Test + fun `present - asynchronously sets canInvite when power level check fails`() = runTest { + val presenter = createPresenter( + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canInviteResult = { Result.failure(RuntimeException("Eek")) }, + updateMembersResult = { Result.success(Unit) } + ) + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + assertThat(loadedState.canInvite).isFalse() + } + } + + @Test + fun `present - RoomMemberSelected will open the moderation options when target user is not banned`() = runTest { + val roomMemberModerationPresenter = Presenter { + aRoomMemberModerationState(canBan = true, canKick = true) + } + val presenter = createPresenter( + roomMemberModerationPresenter = roomMemberModerationPresenter, + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + updateMembersResult = { Result.success(Unit) }, + canInviteResult = { Result.success(true) } + ) + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) + } + } +} + +@ExperimentalCoroutinesApi +private fun TestScope.createDataSource( + room: BaseRoom = FakeBaseRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + }, + coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() +) = RoomMemberListDataSource(room, coroutineDispatchers) + +@ExperimentalCoroutinesApi +private fun TestScope.createPresenter( + coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + joinedRoom: JoinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + updateMembersResult = { Result.success(Unit) } + ) + ), + roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), + encryptedService: FakeEncryptionService = FakeEncryptionService(), + roomMemberModerationPresenter: Presenter = Presenter { + aRoomMemberModerationState() + }, +) = RoomMemberListPresenter( + room = joinedRoom, + roomMemberListDataSource = roomMemberListDataSource, + coroutineDispatchers = coroutineDispatchers, + roomMembersModerationPresenter = roomMemberModerationPresenter, + encryptionService = encryptedService, +) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt new file mode 100644 index 00000000000..73859b712f5 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt @@ -0,0 +1,387 @@ +/* + * 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. + */ + +package io.element.android.features.roomdetails.impl.members.details + +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.roomdetails.impl.aJoinedRoom +import io.element.android.features.roomdetails.impl.members.aRoomMember +import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfilePresenterFactory +import io.element.android.features.userprofile.api.UserProfileVerificationState +import io.element.android.features.userprofile.shared.aUserProfileState +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.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.RoomMembersState +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class RoomMemberDetailsPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - returns the room member's data, then updates it if needed`() = runTest { + val roomMember = aRoomMember(displayName = "Alice") + val room = aJoinedRoom( + userDisplayNameResult = { Result.success("A custom name") }, + userAvatarUrlResult = { Result.success("A custom avatar") }, + getUpdatedMemberResult = { Result.success(roomMember) }, + ).apply { + givenRoomMembersState(RoomMembersState.Ready(persistentListOf(roomMember))) + } + val presenter = createRoomMemberDetailsPresenter( + room = room, + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.userName).isEqualTo("Alice") + assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url") + skipItems(1) + val nextState = awaitItem() + assertThat(nextState.userName).isEqualTo("A custom name") + assertThat(nextState.avatarUrl).isEqualTo("A custom avatar") + } + } + + @Test + fun `present - will recover when retrieving room member details fails`() = runTest { + val roomMember = aRoomMember( + displayName = "Alice", + avatarUrl = "Alice Avatar url", + ) + val room = aJoinedRoom( + userDisplayNameResult = { Result.failure(RuntimeException()) }, + userAvatarUrlResult = { Result.failure(RuntimeException()) }, + getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) }, + ).apply { + givenRoomMembersState(RoomMembersState.Ready(persistentListOf(roomMember))) + } + + val presenter = createRoomMemberDetailsPresenter( + room = room, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.userName).isEqualTo("Alice") + assertThat(initialState.avatarUrl).isEqualTo("Alice Avatar url") + } + } + + @Test + fun `present - will fallback to original data if the updated data is null`() = runTest { + val roomMember = aRoomMember(displayName = "Alice") + val room = aJoinedRoom( + userDisplayNameResult = { Result.success(null) }, + userAvatarUrlResult = { Result.success(null) }, + getUpdatedMemberResult = { Result.success(roomMember) } + ).apply { + givenRoomMembersState(RoomMembersState.Ready(persistentListOf(roomMember))) + } + val presenter = createRoomMemberDetailsPresenter( + room = room, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.userName).isEqualTo("Alice") + assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url") + } + } + + @Test + fun `present - will fallback to user profile if user is not a member of the room`() = runTest { + val room = aJoinedRoom( + userDisplayNameResult = { Result.failure(Exception("Not a member!")) }, + userAvatarUrlResult = { Result.failure(Exception("Not a member!")) }, + getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) }, + ) + val presenter = createRoomMemberDetailsPresenter( + room = room, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.userName).isEqualTo("Profile user name") + assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url") + } + } + + @Test + fun `present - null cases`() = runTest { + val roomMember = aRoomMember( + displayName = null, + avatarUrl = null, + ) + val room = aJoinedRoom( + userDisplayNameResult = { Result.success(null) }, + userAvatarUrlResult = { Result.success(null) }, + getUpdatedMemberResult = { Result.success(roomMember) }, + ) + val presenter = createRoomMemberDetailsPresenter( + room = room, + userProfilePresenterFactory = { + Presenter { + aUserProfileState( + userName = null, + avatarUrl = null, + ) + } + }, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.userName).isNull() + assertThat(initialState.avatarUrl).isNull() + } + } + + @Test + fun `present - when user's identity is verified, the value in the state is VERIFIED`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, + userDisplayNameResult = { Result.success("A custom name") }, + userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), + ) + ) + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(IdentityState.Verified) }, + ) + val presenter = createRoomMemberDetailsPresenter(room = room, encryptionService = encryptionService) + presenter.test { + // Initial state, then the verification state is updated + assertThat(awaitItem().verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN) + consumeItemsUntilPredicate { it.verificationState == UserProfileVerificationState.VERIFIED } + } + } + + @Test + fun `present - when user's identity is unknown, the value in the state is UNKNOWN`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, + userDisplayNameResult = { Result.success("A custom name") }, + userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), + ) + ) + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(null) }, + ) + val presenter = createRoomMemberDetailsPresenter(room = room, encryptionService = encryptionService) + presenter.test { + // Initial state, then the verification state is updated + assertThat(awaitItem().verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN) + ensureAllEventsConsumed() + } + } + + @Test + fun `present - when user's identity is pinned, the value in the state is UNVERIFIED`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, + userDisplayNameResult = { Result.success("A custom name") }, + userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), + ) + ) + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(IdentityState.Pinned) }, + ) + val presenter = createRoomMemberDetailsPresenter(room = room, encryptionService = encryptionService) + presenter.test { + // Initial state, then the verification state is updated + assertThat(awaitItem().verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN) + consumeItemsUntilPredicate { it.verificationState == UserProfileVerificationState.UNVERIFIED } + } + } + + @Test + fun `present - when user's identity is pin violation, the value in the state is UNVERIFIED`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, + userDisplayNameResult = { Result.success("A custom name") }, + userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), + ) + ) + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(IdentityState.PinViolation) }, + ) + val presenter = createRoomMemberDetailsPresenter(room = room, encryptionService = encryptionService) + presenter.test { + // Initial state, then the verification state is updated + assertThat(awaitItem().verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN) + consumeItemsUntilPredicate { it.verificationState == UserProfileVerificationState.UNVERIFIED } + } + } + + @Test + fun `present - when user's identity has a verification violation, the value in the state is VERIFICATION_VIOLATION`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, + userDisplayNameResult = { Result.success("A custom name") }, + userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), + ) + ) + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(IdentityState.VerificationViolation) }, + ) + val presenter = createRoomMemberDetailsPresenter(room = room, encryptionService = encryptionService) + presenter.test { + // Initial state, then the verification state is updated + assertThat(awaitItem().verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN) + consumeItemsUntilPredicate { it.verificationState == UserProfileVerificationState.VERIFICATION_VIOLATION } + } + } + + @Test + fun `present - user identity updates in real time if the room is encrypted`() = runTest { + val identityStateChanges = MutableStateFlow(emptyList()) + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, + userDisplayNameResult = { Result.success("A custom name") }, + userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), + ), + identityStateChangesFlow = identityStateChanges, + ) + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(null) }, + ) + val presenter = createRoomMemberDetailsPresenter(room = room, encryptionService = encryptionService) + presenter.test { + // Initial state, then the verification state is updated + assertThat(awaitItem().verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN) + + room.emitSyncUpdate() + + identityStateChanges.emit(listOf(IdentityStateChange(A_USER_ID, IdentityState.Pinned))) + consumeItemsUntilPredicate { it.verificationState == UserProfileVerificationState.UNVERIFIED } + + identityStateChanges.emit(listOf(IdentityStateChange(A_USER_ID, IdentityState.Verified))) + consumeItemsUntilPredicate { it.verificationState == UserProfileVerificationState.VERIFIED } + + identityStateChanges.emit(listOf(IdentityStateChange(A_USER_ID, IdentityState.VerificationViolation))) + consumeItemsUntilPredicate { it.verificationState == UserProfileVerificationState.VERIFICATION_VIOLATION } + } + } + + @Test + fun `present - user identity can't update in real time if the room is not encrypted`() = runTest { + val identityStateChanges = MutableStateFlow(emptyList()) + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, + userDisplayNameResult = { Result.success("A custom name") }, + userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = false), + ), + identityStateChangesFlow = identityStateChanges, + ) + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(null) }, + ) + val presenter = createRoomMemberDetailsPresenter(room = room, encryptionService = encryptionService) + presenter.test { + // Initial state, then the verification state is updated + assertThat(awaitItem().verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN) + + room.emitSyncUpdate() + identityStateChanges.emit(listOf(IdentityStateChange(A_USER_ID, IdentityState.Pinned))) + + // No new events emitted + ensureAllEventsConsumed() + } + } + + @Test + fun `present - handles WithdrawVerification action`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + getUpdatedMemberResult = { Result.success(aRoomMember(A_USER_ID)) }, + userDisplayNameResult = { Result.success("A custom name") }, + userAvatarUrlResult = { Result.success("A custom avatar") }, + initialRoomInfo = aRoomInfo(isEncrypted = true), + ) + ) + val withdrawVerificationResult = lambdaRecorder> { Result.success(Unit) } + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(IdentityState.VerificationViolation) }, + withdrawVerificationResult = withdrawVerificationResult, + ) + val presenter = createRoomMemberDetailsPresenter(room = room, encryptionService = encryptionService) + presenter.test { + // Initial state, then the verification state is updated + val initialState = awaitItem() + assertThat(initialState.verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN) + + consumeItemsUntilPredicate { it.verificationState == UserProfileVerificationState.VERIFICATION_VIOLATION } + + initialState.eventSink(UserProfileEvents.WithdrawVerification) + withdrawVerificationResult.assertions().isCalledOnce() + } + } + + private fun createRoomMemberDetailsPresenter( + room: JoinedRoom, + userProfilePresenterFactory: UserProfilePresenterFactory = UserProfilePresenterFactory { + Presenter { + aUserProfileState( + userName = "Profile user name", + avatarUrl = "Profile avatar url", + ) + } + }, + encryptionService: FakeEncryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(null) }), + clipboardHelper: ClipboardHelper = FakeClipboardHelper(), + ): RoomMemberDetailsPresenter { + return RoomMemberDetailsPresenter( + roomMemberId = UserId("@alice:server.org"), + room = room, + userProfilePresenterFactory = userProfilePresenterFactory, + encryptionService = encryptionService, + clipboardHelper = clipboardHelper, + ) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenterTest.kt similarity index 92% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenterTest.kt index b27e9a95d0e..308ad943791 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenterTest.kt @@ -5,20 +5,18 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.notificationsettings +package io.element.android.features.roomdetails.impl.notificationsettings 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.roomdetails.aMatrixRoom -import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsEvents -import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsPresenter +import io.element.android.features.roomdetails.impl.aJoinedRoom import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.test.runTest @@ -73,7 +71,7 @@ class RoomNotificationSettingsPresenterTest { @Test fun `present - notification settings set custom failed`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() - notificationSettingsService.givenSetNotificationModeError(A_THROWABLE) + notificationSettingsService.givenSetNotificationModeError(AN_EXCEPTION) val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -133,7 +131,7 @@ class RoomNotificationSettingsPresenterTest { @Test fun `present - notification settings restore default failed`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() - notificationSettingsService.givenRestoreDefaultNotificationModeError(A_THROWABLE) + notificationSettingsService.givenRestoreDefaultNotificationModeError(AN_EXCEPTION) val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -160,7 +158,7 @@ class RoomNotificationSettingsPresenterTest { val notificationService = FakeNotificationSettingsService().apply { givenCanHomeServerPushEncryptedEventsToDeviceResult(Result.success(false)) } - val room = aMatrixRoom(notificationSettingsService = notificationService, isEncrypted = true) + val room = aJoinedRoom(notificationSettingsService = notificationService, isEncrypted = true) val presenter = createRoomNotificationSettingsPresenter(notificationService, room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -174,7 +172,7 @@ class RoomNotificationSettingsPresenterTest { val notificationService = FakeNotificationSettingsService().apply { givenCanHomeServerPushEncryptedEventsToDeviceResult(Result.success(false)) } - val room = aMatrixRoom(notificationSettingsService = notificationService, isEncrypted = false) + val room = aJoinedRoom(notificationSettingsService = notificationService, isEncrypted = false) val presenter = createRoomNotificationSettingsPresenter(notificationService, room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -185,7 +183,7 @@ class RoomNotificationSettingsPresenterTest { private fun createRoomNotificationSettingsPresenter( notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), - room: FakeMatrixRoom = aMatrixRoom(notificationSettingsService = notificationSettingsService), + room: FakeJoinedRoom = aJoinedRoom(notificationSettingsService = notificationSettingsService), ): RoomNotificationSettingsPresenter { return RoomNotificationSettingsPresenter( room = room, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionPresenterTest.kt similarity index 90% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionPresenterTest.kt index 7767c61ce51..718c618484a 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionPresenterTest.kt @@ -5,20 +5,17 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.rolesandpermissions +package io.element.android.features.roomdetails.impl.rolesandpermissions import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.RoomModeration -import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsEvents -import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsPresenter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevels +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -61,7 +58,7 @@ class RolesAndPermissionPresenterTest { fun `present - DemoteSelfTo changes own role to the specified one`() = runTest(StandardTestDispatcher()) { val presenter = createRolesAndPermissionsPresenter( dispatchers = testCoroutineDispatchers(), - room = FakeMatrixRoom( + room = FakeJoinedRoom( updateUserRoleResult = { Result.success(Unit) } ), ) @@ -82,7 +79,7 @@ class RolesAndPermissionPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - DemoteSelfTo can handle failures and clean them`() = runTest(StandardTestDispatcher()) { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( updateUserRoleResult = { Result.failure(Exception("Failed to update role")) } ) val presenter = createRolesAndPermissionsPresenter(room = room, dispatchers = testCoroutineDispatchers()) @@ -122,8 +119,8 @@ class RolesAndPermissionPresenterTest { val analyticsService = FakeAnalyticsService() val presenter = createRolesAndPermissionsPresenter( analyticsService = analyticsService, - room = FakeMatrixRoom( - resetPowerLevelsResult = { Result.success(defaultRoomPowerLevels()) } + room = FakeJoinedRoom( + resetPowerLevelsResult = { Result.success(Unit) } ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -155,7 +152,7 @@ class RolesAndPermissionPresenterTest { } private fun TestScope.createRolesAndPermissionsPresenter( - room: FakeMatrixRoom = FakeMatrixRoom(), + room: FakeJoinedRoom = FakeJoinedRoom(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), analyticsService: FakeAnalyticsService = FakeAnalyticsService() ): RolesAndPermissionsPresenter { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt similarity index 91% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt index ff06dee5438..8aded9f698b 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt @@ -5,18 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.rolesandpermissions +package io.element.android.features.roomdetails.impl.rolesandpermissions import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdetails.impl.R -import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsEvents -import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsNavigator -import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsState -import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsView -import io.element.android.features.roomdetails.impl.rolesandpermissions.aRolesAndPermissionsState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.ui.strings.CommonStrings @@ -26,6 +21,7 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledTimes import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.setSafeContent import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -164,14 +160,14 @@ class RolesAndPermissionsViewTest { private fun AndroidComposeTestRule.setRolesAndPermissionsView( state: RolesAndPermissionsState = aRolesAndPermissionsState( - eventSink = EventsRecorder(expectEvents = false), + eventSink = EventsRecorder(expectEvents = false), ), goBack: () -> Unit = EnsureNeverCalled(), openAdminList: () -> Unit = EnsureNeverCalled(), openModeratorList: () -> Unit = EnsureNeverCalled(), openPermissionScreens: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setSafeContent { RolesAndPermissionsView( state = state, rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenterTest.kt similarity index 77% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenterTest.kt index 1270dc72951..18ddf2ae04f 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenterTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.rolesandpermissions.changeroles +package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow @@ -13,18 +13,20 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.features.roomdetails.impl.members.aRoomMemberList -import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesEvent -import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesPresenter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +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.RoomMembersState +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevelValues import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.persistentMapOf @@ -56,8 +58,8 @@ class ChangeRolesPresenterTest { @Test fun `present - initial results are loaded automatically`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) } val presenter = createChangeRolesPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -70,8 +72,8 @@ class ChangeRolesPresenterTest { @Test fun `present - ToggleSearchActive changes the value`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) } val presenter = createChangeRolesPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -89,8 +91,8 @@ class ChangeRolesPresenterTest { @Test fun `present - QueryChanged produces new results`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) } val presenter = createChangeRolesPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -115,8 +117,8 @@ class ChangeRolesPresenterTest { @Test fun `present - changes in the room members produce new results`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) } val presenter = createChangeRolesPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -128,7 +130,7 @@ class ChangeRolesPresenterTest { assertThat(initialResults?.moderators).hasSize(1) assertThat(initialResults?.admins).hasSize(1) - room.givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList().take(1).toPersistentList())) + room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList().take(1).toPersistentList())) skipItems(1) val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results @@ -141,9 +143,9 @@ class ChangeRolesPresenterTest { @Test fun `present - UserSelectionToggle adds and removes users from the selected user list`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN))) } val presenter = createChangeRolesPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -163,9 +165,9 @@ class ChangeRolesPresenterTest { @Test fun `present - hasPendingChanges is true when the initial selected users don't match the new ones`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN))) } val presenter = createChangeRolesPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -192,9 +194,9 @@ class ChangeRolesPresenterTest { @Test fun `present - Exit will display success if no pending changes`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN))) } val presenter = createChangeRolesPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -212,9 +214,9 @@ class ChangeRolesPresenterTest { @Test fun `present - CancelExit will remove exit confirmation`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN))) } val presenter = createChangeRolesPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -238,9 +240,9 @@ class ChangeRolesPresenterTest { @Test fun `present - Exit will display a confirmation dialog if there are pending changes, calling it again will actually exit`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN))) } val presenter = createChangeRolesPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -266,12 +268,12 @@ class ChangeRolesPresenterTest { @Test fun `present - Save will display a confirmation when adding admins`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( updateUserRoleResult = { Result.success(Unit) }, - updateMembersResult = { Result.success(Unit) }, + baseRoom = FakeBaseRoom(updateMembersResult = { Result.success(Unit) }), ).apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN))) } val presenter = createChangeRolesPresenter(role = RoomMember.Role.ADMIN, room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -287,15 +289,20 @@ class ChangeRolesPresenterTest { assertThat(confirmingState.savingState).isEqualTo(AsyncAction.ConfirmingNoParams) confirmingState.eventSink(ChangeRolesEvent.Save) + + val loadingState = awaitItem() + assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java) + skipItems(1) + assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit)) } } @Test fun `present - CancelSave will remove the confirmation dialog`() = runTest { - val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + val room = FakeJoinedRoom().apply { + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.ADMIN))) } val presenter = createChangeRolesPresenter(role = RoomMember.Role.ADMIN, room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -319,12 +326,12 @@ class ChangeRolesPresenterTest { @Test fun `present - Save will just save the data for moderators`() = runTest { val analyticsService = FakeAnalyticsService() - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( updateUserRoleResult = { Result.success(Unit) }, - updateMembersResult = { Result.success(Unit) }, + baseRoom = FakeBaseRoom(updateMembersResult = { Result.success(Unit) }), ).apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 50))) + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.MODERATOR))) } val presenter = createChangeRolesPresenter( role = RoomMember.Role.MODERATOR, @@ -341,6 +348,11 @@ class ChangeRolesPresenterTest { initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) awaitItem().eventSink(ChangeRolesEvent.Save) + + val loadingState = awaitItem() + assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java) + skipItems(1) + assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit)) assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.ChangeMemberRole, RoomModeration.Role.Moderator)) } @@ -348,11 +360,11 @@ class ChangeRolesPresenterTest { @Test fun `present - Save can handle failures and ClearError clears them`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( updateUserRoleResult = { Result.failure(IllegalStateException("Failed")) } ).apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 50))) + givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(roomPowerLevels = roomPowerLevelsWithRole(RoomMember.Role.MODERATOR))) } val presenter = createChangeRolesPresenter(role = RoomMember.Role.MODERATOR, room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -365,6 +377,9 @@ class ChangeRolesPresenterTest { initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) awaitItem().eventSink(ChangeRolesEvent.Save) + val loadingState = awaitItem() + assertThat(loadingState.savingState).isInstanceOf(AsyncAction.Loading::class.java) + skipItems(1) val failedState = awaitItem() assertThat(failedState.savingState).isInstanceOf(AsyncAction.Failure::class.java) @@ -373,9 +388,19 @@ class ChangeRolesPresenterTest { } } + private fun roomPowerLevelsWithRole( + role: RoomMember.Role, + userId: UserId = A_USER_ID, + ): RoomPowerLevels { + return RoomPowerLevels( + values = defaultRoomPowerLevelValues(), + users = persistentMapOf(userId to role.powerLevel) + ) + } + private fun TestScope.createChangeRolesPresenter( role: RoomMember.Role = RoomMember.Role.ADMIN, - room: FakeMatrixRoom = FakeMatrixRoom(), + room: FakeJoinedRoom = FakeJoinedRoom(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), ): ChangeRolesPresenter { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt similarity index 93% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesViewTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt index 773005983de..4bb0253e6af 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.rolesandpermissions.changeroles +package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule @@ -16,12 +16,8 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesEvent -import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesState -import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesView -import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.aChangeRolesState -import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.aChangeRolesStateWithSelectedUsers import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.toMatrixUser @@ -39,7 +35,6 @@ import org.junit.Test import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config -import java.lang.IllegalStateException @RunWith(AndroidJUnit4::class) class ChangeRolesViewTest { @@ -47,7 +42,7 @@ class ChangeRolesViewTest { @Test fun `passing a 'USER' role throws an exception`() { - val exception = runCatching { + val exception = runCatchingExceptions { rule.setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.USER, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/MembersByRoleTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/MembersByRoleTest.kt similarity index 94% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/MembersByRoleTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/MembersByRoleTest.kt index 16f5683f4af..f788624ccc9 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/MembersByRoleTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/MembersByRoleTest.kt @@ -5,10 +5,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.rolesandpermissions.changeroles +package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles import com.google.common.truth.Truth.assertThat -import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.MembersByRole import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeBaseRoomPermissionsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeBaseRoomPermissionsPresenterTest.kt new file mode 100644 index 00000000000..70770a6c5c1 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeBaseRoomPermissionsPresenterTest.kt @@ -0,0 +1,305 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.Event +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.RoomModeration +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.room.RoomMember.Role.ADMIN +import io.element.android.libraries.matrix.api.room.RoomMember.Role.MODERATOR +import io.element.android.libraries.matrix.api.room.RoomMember.Role.USER +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +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.defaultRoomPowerLevelValues +import io.element.android.services.analytics.test.FakeAnalyticsService +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ChangeBaseRoomPermissionsPresenterTest { + @Test + fun `present - initial state`() = runTest { + val section = ChangeRoomPermissionsSection.RoomDetails + val presenter = createChangeRoomPermissionsPresenter(section = section) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Initial state, no permissions loaded + awaitItem().run { + assertThat(this.section).isEqualTo(section) + assertThat(this.currentPermissions).isNull() + assertThat(this.items).isNotEmpty() + assertThat(this.hasChanges).isFalse() + assertThat(this.saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(this.confirmExitAction).isEqualTo(AsyncAction.Uninitialized) + } + + // Updated state, permissions loaded + assertThat(awaitItem().currentPermissions).isEqualTo(defaultPermissions()) + } + } + + @Test + fun `present - RoomDetails section contains the right items`() = runTest { + val section = ChangeRoomPermissionsSection.RoomDetails + val presenter = createChangeRoomPermissionsPresenter(section = section) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitUpdatedItem().items).containsExactly( + RoomPermissionType.ROOM_NAME, + RoomPermissionType.ROOM_AVATAR, + RoomPermissionType.ROOM_TOPIC, + ) + } + } + + @Test + fun `present - MessagesAndContent section contains the right items`() = runTest { + val section = ChangeRoomPermissionsSection.MessagesAndContent + val presenter = createChangeRoomPermissionsPresenter(section = section) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitUpdatedItem().items).containsExactly( + RoomPermissionType.SEND_EVENTS, + RoomPermissionType.REDACT_EVENTS, + ) + } + } + + @Test + fun `present - MembershipModeration section contains the right items`() = runTest { + val section = ChangeRoomPermissionsSection.MembershipModeration + val presenter = createChangeRoomPermissionsPresenter(section = section) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitUpdatedItem().items).containsExactly( + RoomPermissionType.INVITE, + RoomPermissionType.KICK, + RoomPermissionType.BAN, + ) + } + } + + @Test + fun `present - ChangeMinimumRoleForAction updates the current permissions and hasChanges`() = runTest { + val presenter = createChangeRoomPermissionsPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = awaitUpdatedItem() + assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel) + assertThat(state.hasChanges).isFalse() + + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) + + awaitItem().run { + assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel) + assertThat(hasChanges).isTrue() + } + } + } + + @Test + fun `present - ChangeMinimumRoleForAction works for all actions`() = runTest { + val presenter = createChangeRoomPermissionsPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = awaitUpdatedItem() + + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, MODERATOR)) + + val items = cancelAndConsumeRemainingEvents() + + (items.last() as? Event.Item)?.value?.run { + assertThat(currentPermissions).isEqualTo( + RoomPowerLevelsValues( + invite = MODERATOR.powerLevel, + kick = MODERATOR.powerLevel, + ban = MODERATOR.powerLevel, + redactEvents = MODERATOR.powerLevel, + sendEvents = MODERATOR.powerLevel, + roomName = MODERATOR.powerLevel, + roomAvatar = MODERATOR.powerLevel, + roomTopic = MODERATOR.powerLevel, + ) + ) + } + } + } + + @Test + fun `present - Save updates the current permissions and resets hasChanges`() = runTest { + val analyticsService = FakeAnalyticsService() + val presenter = createChangeRoomPermissionsPresenter( + analyticsService = analyticsService, + room = FakeJoinedRoom( + updatePowerLevelsResult = { Result.success(Unit) }, + baseRoom = FakeBaseRoom(powerLevelsResult = { Result.success(defaultPermissions()) }), + ), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = awaitUpdatedItem() + assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel) + assertThat(state.hasChanges).isFalse() + + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, MODERATOR)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, USER)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, ADMIN)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, ADMIN)) + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, ADMIN)) + skipItems(7) + assertThat(awaitItem().hasChanges).isTrue() + + state.eventSink(ChangeRoomPermissionsEvent.Save) + + assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Loading) + assertThat(awaitItem().hasChanges).isFalse() + awaitItem().run { + assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel) + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + } + assertThat(analyticsService.capturedEvents).containsExactlyElementsIn( + listOf( + RoomModeration(RoomModeration.Action.ChangePermissionsRoomName, RoomModeration.Role.Moderator), + RoomModeration(RoomModeration.Action.ChangePermissionsRoomAvatar, RoomModeration.Role.Moderator), + RoomModeration(RoomModeration.Action.ChangePermissionsRoomTopic, RoomModeration.Role.Moderator), + RoomModeration(RoomModeration.Action.ChangePermissionsSendMessages, RoomModeration.Role.Moderator), + RoomModeration(RoomModeration.Action.ChangePermissionsRedactMessages, RoomModeration.Role.User), + RoomModeration(RoomModeration.Action.ChangePermissionsKickMembers, RoomModeration.Role.Administrator), + RoomModeration(RoomModeration.Action.ChangePermissionsBanMembers, RoomModeration.Role.Administrator), + RoomModeration(RoomModeration.Action.ChangePermissionsInviteUsers, RoomModeration.Role.Administrator), + ) + ) + } + } + + @Test + fun `present - Save will fail if there are not current permissions`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom(powerLevelsResult = { Result.failure(IllegalStateException("Failed to load power levels")) }), + ) + val presenter = createChangeRoomPermissionsPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = awaitItem() + assertThat(state.currentPermissions).isNull() + + state.eventSink(ChangeRoomPermissionsEvent.Save) + assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java) + } + } + + @Test + fun `present - Save can handle failures and they can be cleared`() = runTest { + val room = FakeJoinedRoom( + updatePowerLevelsResult = { Result.failure(IllegalStateException("Failed to update power levels")) }, + baseRoom = FakeBaseRoom(powerLevelsResult = { Result.success(defaultPermissions()) }), + ) + val presenter = createChangeRoomPermissionsPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = awaitUpdatedItem() + assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel) + assertThat(state.hasChanges).isFalse() + + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) + assertThat(awaitItem().hasChanges).isTrue() + + state.eventSink(ChangeRoomPermissionsEvent.Save) + + assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Loading) + awaitItem().run { + assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel) + // Couldn't save the changes, so they're still pending + assertThat(hasChanges).isTrue() + assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) + } + + state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) + awaitItem().run { + assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel) + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(hasChanges).isTrue() + } + } + } + + @Test + fun `present - Exit does not need a confirmation when there are no pending changes`() = runTest { + val presenter = createChangeRoomPermissionsPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = awaitUpdatedItem() + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) + assertThat(awaitItem().hasChanges).isTrue() + + state.eventSink(ChangeRoomPermissionsEvent.Exit) + assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.ConfirmingNoParams) + + state.eventSink(ChangeRoomPermissionsEvent.Exit) + assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Success(Unit)) + } + } + + @Test + fun `present - Exit needs confirmation when there are pending changes`() = runTest { + val presenter = createChangeRoomPermissionsPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = awaitUpdatedItem() + + state.eventSink(ChangeRoomPermissionsEvent.Exit) + + assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Success(Unit)) + } + } + + private fun createChangeRoomPermissionsPresenter( + section: ChangeRoomPermissionsSection = ChangeRoomPermissionsSection.RoomDetails, + room: FakeJoinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom(powerLevelsResult = { Result.success(defaultPermissions()) }), + ), + analyticsService: FakeAnalyticsService = FakeAnalyticsService(), + ) = ChangeRoomPermissionsPresenter( + section = section, + room = room, + analyticsService = analyticsService, + ) + + private fun defaultPermissions() = defaultRoomPowerLevelValues() + + private suspend fun TurbineTestContext.awaitUpdatedItem(): ChangeRoomPermissionsState { + skipItems(1) + return awaitItem() + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeBaseRoomPermissionsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeBaseRoomPermissionsViewTest.kt new file mode 100644 index 00000000000..8ed659d31cc --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeBaseRoomPermissionsViewTest.kt @@ -0,0 +1,186 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions + +import androidx.activity.ComponentActivity +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.onFirst +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.clickOnFirst +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 ChangeBaseRoomPermissionsViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `click on back icon invokes Exit`() { + val recorder = EventsRecorder() + rule.setChangeRoomPermissionsRule( + eventsRecorder = recorder, + ) + rule.pressBack() + recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) + } + + @Test + fun `click on back key invokes Exit`() { + val recorder = EventsRecorder() + rule.setChangeRoomPermissionsRule( + eventsRecorder = recorder, + ) + rule.pressBackKey() + recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) + } + + @Test + fun `when confirming exit with pending changes, using the back key actually exits`() { + val recorder = EventsRecorder() + rule.setChangeRoomPermissionsRule( + state = aChangeRoomPermissionsState( + section = ChangeRoomPermissionsSection.RoomDetails, + hasChanges = true, + eventSink = recorder, + ), + eventsRecorder = recorder, + ) + rule.pressBackKey() + recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) + } + + @Test + fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() { + val recorder = EventsRecorder() + rule.setChangeRoomPermissionsRule( + state = aChangeRoomPermissionsState( + section = ChangeRoomPermissionsSection.RoomDetails, + hasChanges = true, + confirmExitAction = AsyncAction.ConfirmingNoParams, + eventSink = recorder, + ), + eventsRecorder = recorder, + ) + 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`() { + val recorder = EventsRecorder() + rule.setChangeRoomPermissionsRule( + state = aChangeRoomPermissionsState( + section = ChangeRoomPermissionsSection.RoomDetails, + hasChanges = true, + confirmExitAction = AsyncAction.ConfirmingNoParams, + eventSink = recorder, + ), + eventsRecorder = recorder, + ) + rule.clickOnFirst(CommonStrings.action_save) + recorder.assertSingle(ChangeRoomPermissionsEvent.Save) + } + + @Test + fun `click on a role item triggers ChangeRole event`() { + val recorder = EventsRecorder() + rule.setChangeRoomPermissionsRule( + eventsRecorder = recorder, + ) + val admins = rule.activity.getText(R.string.screen_room_change_permissions_administrators).toString() + val moderators = rule.activity.getText(R.string.screen_room_change_permissions_moderators).toString() + val users = rule.activity.getText(R.string.screen_room_change_permissions_everyone).toString() + rule.onAllNodesWithText(admins).onFirst().performClick() + rule.onAllNodesWithText(moderators).onFirst().performClick() + rule.onAllNodesWithText(users).onFirst().performClick() + recorder.assertList( + listOf( + ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.ADMIN), + ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.MODERATOR), + ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.USER), + ) + ) + } + + @Test + fun `click on the Save menu item triggers Save event`() { + val recorder = EventsRecorder() + rule.setChangeRoomPermissionsRule( + state = aChangeRoomPermissionsState( + section = ChangeRoomPermissionsSection.RoomDetails, + hasChanges = true, + eventSink = recorder, + ), + eventsRecorder = recorder, + ) + rule.clickOn(CommonStrings.action_save) + recorder.assertSingle(ChangeRoomPermissionsEvent.Save) + } + + @Test + fun `a successful save exits the screen`() { + ensureCalledOnce { callback -> + rule.setChangeRoomPermissionsRule( + state = aChangeRoomPermissionsState( + section = ChangeRoomPermissionsSection.RoomDetails, + hasChanges = true, + saveAction = AsyncAction.Success(Unit), + ), + onBackClick = callback + ) + rule.clickOn(CommonStrings.action_save) + } + } + + @Test + fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() { + val recorder = EventsRecorder() + rule.setChangeRoomPermissionsRule( + state = aChangeRoomPermissionsState( + section = ChangeRoomPermissionsSection.RoomDetails, + hasChanges = true, + saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels")), + eventSink = recorder, + ), + eventsRecorder = recorder, + ) + rule.clickOn(CommonStrings.action_ok) + recorder.assertSingle(ChangeRoomPermissionsEvent.ResetPendingActions) + } +} + +private fun AndroidComposeTestRule.setChangeRoomPermissionsRule( + eventsRecorder: EventsRecorder = EventsRecorder(expectEvents = false), + state: ChangeRoomPermissionsState = aChangeRoomPermissionsState( + section = ChangeRoomPermissionsSection.RoomDetails, + eventSink = eventsRecorder, + ), + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + ChangeRoomPermissionsView( + state = state, + onBackClick = onBackClick, + ) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt similarity index 79% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt index e4dfbcdbbad..0d68afae1c2 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/FakeSecurityAndPrivacyNavigator.kt @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.securityandprivacy +package io.element.android.features.roomdetails.impl.securityandprivacy -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator import io.element.android.tests.testutils.lambda.lambdaError class FakeSecurityAndPrivacyNavigator( diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenterTest.kt similarity index 89% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenterTest.kt index 1618e15b882..bc2a3f62f7d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyPresenterTest.kt @@ -5,23 +5,18 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.securityandprivacy +package io.element.android.features.roomdetails.impl.securityandprivacy import com.google.common.truth.Truth.assertThat -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyEvents -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyHistoryVisibility -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyPresenter -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyRoomAccess import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.room.MatrixRoom 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.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -59,19 +54,19 @@ class SecurityAndPrivacyPresenterTest { @Test fun `present - room info change updates saved and edited settings`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canSendStateResult = { _, _ -> Result.success(true) }, + initialRoomInfo = aRoomInfo( + joinRule = JoinRule.Public, + historyVisibility = RoomHistoryVisibility.WorldReadable, + canonicalAlias = A_ROOM_ALIAS, + ) + ) ) val presenter = createSecurityAndPrivacyPresenter(room = room) presenter.test { - skipItems(2) - room.givenRoomInfo( - aRoomInfo( - joinRule = JoinRule.Public, - historyVisibility = RoomHistoryVisibility.WorldReadable, - canonicalAlias = A_ROOM_ALIAS, - ) - ) + skipItems(1) with(awaitItem()) { assertThat(editedSettings).isEqualTo(savedSettings) assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) @@ -156,6 +151,7 @@ class SecurityAndPrivacyPresenterTest { assertThat(canBeSaved).isTrue() eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) } + skipItems(1) with(awaitItem()) { assertThat(editedSettings.isEncrypted).isFalse() assertThat(canBeSaved).isFalse() @@ -165,9 +161,12 @@ class SecurityAndPrivacyPresenterTest { @Test fun `present - room visibility loading and change`() = runTest { - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canSendStateResult = { _, _ -> Result.success(true) }, - roomVisibilityResult = { Result.success(RoomVisibility.Private) } + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared) + ) ) val presenter = createSecurityAndPrivacyPresenter(room = room) presenter.test { @@ -211,13 +210,16 @@ class SecurityAndPrivacyPresenterTest { val updateJoinRuleLambda = lambdaRecorder> { Result.success(Unit) } val updateRoomVisibilityLambda = lambdaRecorder> { Result.success(Unit) } val updateRoomHistoryVisibilityLambda = lambdaRecorder> { Result.success(Unit) } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canSendStateResult = { _, _ -> Result.success(true) }, + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared) + ), enableEncryptionResult = enableEncryptionLambda, updateJoinRuleResult = updateJoinRuleLambda, updateRoomVisibilityResult = updateRoomVisibilityLambda, updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, - roomVisibilityResult = { Result.success(RoomVisibility.Private) } ) val presenter = createSecurityAndPrivacyPresenter(room = room) presenter.test { @@ -250,6 +252,7 @@ class SecurityAndPrivacyPresenterTest { aRoomInfo( joinRule = JoinRule.Public, historyVisibility = RoomHistoryVisibility.WorldReadable, + isEncrypted = true, ) ) // Saved settings are updated 3 times to match the edited settings @@ -274,13 +277,16 @@ class SecurityAndPrivacyPresenterTest { Result.failure(Exception("Failed to update room visibility")) } val updateRoomHistoryVisibilityLambda = lambdaRecorder> { Result.success(Unit) } - val room = FakeMatrixRoom( + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canSendStateResult = { _, _ -> Result.success(true) }, + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private) + ), enableEncryptionResult = enableEncryptionLambda, updateJoinRuleResult = updateJoinRuleLambda, updateRoomVisibilityResult = updateRoomVisibilityLambda, updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, - roomVisibilityResult = { Result.success(RoomVisibility.Private) } ) val presenter = createSecurityAndPrivacyPresenter(room = room) presenter.test { @@ -316,7 +322,7 @@ class SecurityAndPrivacyPresenterTest { ) ) // Saved settings are updated 2 times to match the edited settings - skipItems(2) + skipItems(3) with(awaitItem()) { assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory) @@ -331,9 +337,12 @@ class SecurityAndPrivacyPresenterTest { private fun createSecurityAndPrivacyPresenter( serverName: String = "matrix.org", - room: MatrixRoom = FakeMatrixRoom( - canSendStateResult = { _, _ -> Result.success(true) }, - roomVisibilityResult = { Result.success(RoomVisibility.Private) } + room: FakeJoinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canSendStateResult = { _, _ -> Result.success(true) }, + getRoomVisibilityResult = { Result.success(RoomVisibility.Private) }, + initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private) + ), ), navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(), ): SecurityAndPrivacyPresenter { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyViewTest.kt similarity index 89% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyViewTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyViewTest.kt index 558fbcc4278..9bbd7703342 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/SecurityAndPrivacyViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyViewTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.securityandprivacy +package io.element.android.features.roomdetails.impl.securityandprivacy import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule @@ -14,13 +14,6 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdetails.impl.R -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyEvents -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyHistoryVisibility -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyRoomAccess -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyState -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyView -import io.element.android.features.roomdetails.impl.securityandprivacy.aSecurityAndPrivacySettings -import io.element.android.features.roomdetails.impl.securityandprivacy.aSecurityAndPrivacyState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -100,7 +93,7 @@ class SecurityAndPrivacyViewTest { } @Test - @Config(qualifiers = "h640dp") + @Config(qualifiers = "h1024dp") fun `click on room visibility item emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditBaseRoomAddressPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditBaseRoomAddressPresenterTest.kt new file mode 100644 index 00000000000..9b9cb4120cb --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditBaseRoomAddressPresenterTest.kt @@ -0,0 +1,371 @@ +/* + * 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.roomdetails.impl.securityandprivacy.editroomaddress + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdetails.impl.aJoinedRoom +import io.element.android.features.roomdetails.impl.securityandprivacy.FakeSecurityAndPrivacyNavigator +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias +import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity +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.test.runTest +import org.junit.Test +import java.util.Optional + +class EditBaseRoomAddressPresenterTest { + @Test + fun `present - initial state no address`() = runTest { + val presenter = createEditRoomAddressPresenter( + room = aJoinedRoom(displayName = "") + ) + presenter.test { + with(awaitItem()) { + assertThat(homeserverName).isEqualTo("matrix.org") + assertThat(canBeSaved).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + assertThat(roomAddress).isEmpty() + } + } + } + + @Test + fun `present - initial state address matching own homeserver`() = runTest { + val room = aJoinedRoom( + canonicalAlias = RoomAlias("#canonical:matrix.org"), + ) + val presenter = createEditRoomAddressPresenter(room = room) + presenter.test { + with(awaitItem()) { + assertThat(homeserverName).isEqualTo("matrix.org") + assertThat(canBeSaved).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + assertThat(roomAddress).isEqualTo("canonical") + } + } + } + + @Test + fun `present - initial state address not matching own homeserver`() = runTest { + val room = aJoinedRoom( + displayName = "", + canonicalAlias = RoomAlias("#canonical:notmatrix.org"), + ) + val presenter = createEditRoomAddressPresenter(room = room) + presenter.test { + with(awaitItem()) { + assertThat(homeserverName).isEqualTo("matrix.org") + assertThat(canBeSaved).isFalse() + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + assertThat(roomAddress).isEmpty() + } + } + } + + @Test + fun `present - room address change invalid state`() = runTest { + val roomAliasHelper = FakeRoomAliasHelper( + isRoomAliasValidLambda = { false } + ) + val presenter = createEditRoomAddressPresenter(roomAliasHelper = roomAliasHelper) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("invalid")) + } + with(awaitItem()) { + assertThat(roomAddress).isEqualTo("invalid") + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + } + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols) + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - room address change valid state`() = runTest { + val presenter = createEditRoomAddressPresenter() + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + with(awaitItem()) { + assertThat(roomAddress).isEqualTo("valid") + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + } + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + } + } + } + + @Test + fun `present - room address change alias unavailable`() = runTest { + val client = createMatrixClient(isAliasAvailable = false) + val presenter = createEditRoomAddressPresenter(client = client) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + with(awaitItem()) { + assertThat(roomAddress).isEqualTo("valid") + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) + } + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable) + assertThat(canBeSaved).isFalse() + } + } + } + + @Test + fun `present - save success no current alias`() = runTest { + val publishAliasInRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val updateCanonicalAliasResult = lambdaRecorder, Result> { _, _ -> Result.success(Unit) } + val removeAliasFromRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val closeEditAddressLambda = lambdaRecorder { } + val navigator = FakeSecurityAndPrivacyNavigator( + closeEditRoomAddressLambda = closeEditAddressLambda + ) + val room = FakeJoinedRoom( + updateCanonicalAliasResult = updateCanonicalAliasResult, + publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult + ) + val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + skipItems(1) + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + eventSink(EditRoomAddressEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + } + + val createdAlias = RoomAlias("#valid:matrix.org") + assert(updateCanonicalAliasResult) + .isCalledOnce() + .with(value(createdAlias), value(emptyList())) + + assert(publishAliasInRoomDirectoryResult) + .isCalledOnce() + .with(value(createdAlias)) + + assert(removeAliasFromRoomDirectoryResult).isNeverCalled() + + assert(closeEditAddressLambda).isCalledOnce() + } + } + + @Test + fun `present - save success current canonical alias from own homeserver`() = runTest { + val publishAliasInRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val removeAliasFromRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val updateCanonicalAliasResult = lambdaRecorder, Result> { _, _ -> Result.success(Unit) } + val closeEditAddressLambda = lambdaRecorder { } + + val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) + val canonicalAlias = RoomAlias("#canonical:matrix.org") + val room = aJoinedRoom( + canonicalAlias = canonicalAlias, + updateCanonicalAliasResult = updateCanonicalAliasResult, + publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, + removeRoomAliasFromRoomDirectoryResult = removeAliasFromRoomDirectoryResult + ) + val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + skipItems(1) + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + eventSink(EditRoomAddressEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + } + + val createdAlias = RoomAlias("#valid:matrix.org") + assert(updateCanonicalAliasResult) + .isCalledOnce() + .with(value(createdAlias), value(emptyList())) + + assert(publishAliasInRoomDirectoryResult) + .isCalledOnce() + .with(value(createdAlias)) + + assert(removeAliasFromRoomDirectoryResult) + .isCalledOnce() + .with(value(canonicalAlias)) + + assert(closeEditAddressLambda).isCalledOnce() + } + } + + @Test + fun `present - save success current canonical alias from other homeserver`() = runTest { + val publishAliasInRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val removeAliasFromRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } + val updateCanonicalAliasResult = lambdaRecorder, Result> { _, _ -> Result.success(Unit) } + val closeEditAddressLambda = lambdaRecorder { } + + val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) + val canonicalAlias = RoomAlias("#canonical:notmatrix.org") + val room = aJoinedRoom( + canonicalAlias = canonicalAlias, + updateCanonicalAliasResult = updateCanonicalAliasResult, + publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, + removeRoomAliasFromRoomDirectoryResult = removeAliasFromRoomDirectoryResult + ) + val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + skipItems(1) + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + eventSink(EditRoomAddressEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) + } + + val createdAlias = RoomAlias("#valid:matrix.org") + assert(updateCanonicalAliasResult) + .isCalledOnce() + .with(value(canonicalAlias), value(listOf(createdAlias))) + + assert(publishAliasInRoomDirectoryResult) + .isCalledOnce() + .with(value(createdAlias)) + + assert(removeAliasFromRoomDirectoryResult).isNeverCalled() + + assert(closeEditAddressLambda).isCalledOnce() + } + } + + @Test + fun `present - save failure`() = runTest { + val closeEditAddressLambda = lambdaRecorder { } + val navigator = FakeSecurityAndPrivacyNavigator( + closeEditRoomAddressLambda = closeEditAddressLambda + ) + val presenter = createEditRoomAddressPresenter( + navigator = navigator, + room = FakeJoinedRoom( + publishRoomAliasInRoomDirectoryResult = { + Result.failure(AN_EXCEPTION) + }, + ) + ) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + } + skipItems(1) + with(awaitItem()) { + assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) + assertThat(canBeSaved).isTrue() + eventSink(EditRoomAddressEvents.Save) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Loading) + } + with(awaitItem()) { + assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) + } + + assert(closeEditAddressLambda).isNeverCalled() + } + } + + @Test + fun `present - dismiss error`() = runTest { + val presenter = createEditRoomAddressPresenter( + room = FakeJoinedRoom( + publishRoomAliasInRoomDirectoryResult = { + Result.failure(AN_EXCEPTION) + }, + ) + ) + presenter.test { + with(awaitItem()) { + eventSink(EditRoomAddressEvents.Save) + } + assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java) + with(awaitItem()) { + assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) + eventSink(EditRoomAddressEvents.DismissError) + } + with(awaitItem()) { + assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) + } + } + } + + private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient( + userIdServerNameLambda = { "matrix.org" }, + resolveRoomAliasResult = { + val resolvedRoomAlias = if (isAliasAvailable) { + Optional.empty() + } else { + Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList())) + } + Result.success(resolvedRoomAlias) + } + ) + + private fun createEditRoomAddressPresenter( + client: FakeMatrixClient = createMatrixClient(), + room: JoinedRoom = FakeJoinedRoom(), + navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(), + roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper() + ): EditRoomAddressPresenter { + return EditRoomAddressPresenter( + room = room, + client = client, + roomAliasHelper = roomAliasHelper, + navigator = navigator + ) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditBaseRoomAddressViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditBaseRoomAddressViewTest.kt new file mode 100644 index 00000000000..1d2c716710d --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditBaseRoomAddressViewTest.kt @@ -0,0 +1,116 @@ +/* + * 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.roomdetails.impl.securityandprivacy.editroomaddress + +import androidx.activity.ComponentActivity +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.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EditBaseRoomAddressViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `click on back invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setEditRoomAddressView(onBackClick = callback) + rule.pressBack() + } + } + + @Test + fun `click on disabled save doesn't emit event`() { + val recorder = EventsRecorder(expectEvents = false) + val state = anEditRoomAddressState(eventSink = recorder) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_save) + recorder.assertEmpty() + } + + @Test + fun `click on enabled save emits the expected event`() { + val recorder = EventsRecorder() + val state = anEditRoomAddressState( + roomAddress = "room", + roomAddressValidity = RoomAddressValidity.Valid, + eventSink = recorder + ) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_save) + recorder.assertSingle(EditRoomAddressEvents.Save) + } + + @Test + fun `text changes on text field emits the expected event`() { + val recorder = EventsRecorder() + val state = anEditRoomAddressState( + roomAddress = "", + eventSink = recorder + ) + rule.setEditRoomAddressView(state) + + rule.onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") + recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias")) + } + + @Test + fun `click on dismiss error emits the expected event`() { + val recorder = EventsRecorder() + val state = anEditRoomAddressState( + roomAddress = "", + saveAction = AsyncAction.Failure(IllegalStateException()), + eventSink = recorder + ) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_cancel) + recorder.assertSingle(EditRoomAddressEvents.DismissError) + } + + @Test + fun `click on retry error emits the expected event`() { + val recorder = EventsRecorder() + val state = anEditRoomAddressState( + roomAddress = "", + saveAction = AsyncAction.Failure(IllegalStateException()), + eventSink = recorder + ) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_retry) + recorder.assertSingle(EditRoomAddressEvents.Save) + } +} + +private fun AndroidComposeTestRule.setEditRoomAddressView( + state: EditRoomAddressState = anEditRoomAddressState( + eventSink = EventsRecorder(expectEvents = false), + ), + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + EditRoomAddressView( + state = state, + onBackClick = onBackClick, + ) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/PowerLevelRoomMemberComparatorTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/PowerLevelRoomMemberComparatorTest.kt deleted file mode 100644 index 3ae6b86f5da..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/PowerLevelRoomMemberComparatorTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.members - -import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator -import io.element.android.features.roomdetails.impl.members.aRoomMember -import io.element.android.libraries.matrix.api.core.UserId -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_ID_3 -import io.element.android.libraries.matrix.test.A_USER_ID_4 -import io.element.android.libraries.matrix.test.A_USER_ID_5 -import org.junit.Test - -class PowerLevelRoomMemberComparatorTest { - @Test - fun `order is Admin, then Moderator, then User`() { - val memberList = listOf( - aRoomMember(userId = UserId("@admin:example.com"), powerLevel = 100), - aRoomMember(userId = UserId("@moderator:example.com"), powerLevel = 50), - aRoomMember(userId = UserId("@user:example.com"), powerLevel = 0), - ).shuffled() - - val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) - assert(ordered[0].userId == UserId("@admin:example.com")) - assert(ordered[1].userId == UserId("@moderator:example.com")) - assert(ordered[2].userId == UserId("@user:example.com")) - } - - @Test - fun `with the same power level, alphabetical ascending order for name is used`() { - val memberList = listOf( - aRoomMember(userId = A_USER_ID, displayName = "First - admin", powerLevel = 100), - aRoomMember(userId = A_USER_ID_2, displayName = "Second - admin", powerLevel = 100), - aRoomMember(userId = A_USER_ID_3, displayName = "Third - admin", powerLevel = 100), - aRoomMember(userId = A_USER_ID_4, displayName = "First - user", powerLevel = 0), - aRoomMember(userId = A_USER_ID_5, displayName = "Second - user", powerLevel = 0), - ).shuffled() - - val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) - assert(ordered[0].userId == A_USER_ID) - assert(ordered[1].userId == A_USER_ID_2) - assert(ordered[2].userId == A_USER_ID_3) - assert(ordered[3].userId == A_USER_ID_4) - assert(ordered[4].userId == A_USER_ID_5) - } - - @Test - fun `when no names are provided, alphabetical order uses user id`() { - val memberList = listOf( - aRoomMember(userId = A_USER_ID, displayName = "Z - LAST!", powerLevel = 100), - aRoomMember(userId = A_USER_ID_2, powerLevel = 100), - aRoomMember(userId = A_USER_ID_3, powerLevel = 100), - ).shuffled() - - val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) - assert(ordered[0].userId == A_USER_ID_2) - assert(ordered[1].userId == A_USER_ID_3) - assert(ordered[2].userId == A_USER_ID) - } - - @Test - fun `unicode characters are simplified and compared, order ignores case`() { - val memberList = listOf( - aRoomMember(userId = A_USER_ID, displayName = "First", powerLevel = 100), - aRoomMember(userId = A_USER_ID_2, displayName = "Șecond", powerLevel = 100), - aRoomMember(userId = A_USER_ID_3, displayName = "third", powerLevel = 100), - ).shuffled() - - val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) - assert(ordered[0].userId == A_USER_ID) - assert(ordered[1].userId == A_USER_ID_2) - assert(ordered[2].userId == A_USER_ID_3) - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTest.kt deleted file mode 100644 index f426086269b..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTest.kt +++ /dev/null @@ -1,276 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomdetails.members - -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.roomdetails.impl.members.RoomMemberListDataSource -import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents -import io.element.android.features.roomdetails.impl.members.RoomMemberListNavigator -import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter -import io.element.android.features.roomdetails.impl.members.aRoomMemberList -import io.element.android.features.roomdetails.impl.members.aVictor -import io.element.android.features.roomdetails.impl.members.aWalter -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState -import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -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.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.room.aRoomInfo -import io.element.android.tests.testutils.EventsRecorder -import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -@ExperimentalCoroutinesApi -class RoomMemberListPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `member loading is done automatically on start, but is async`() = runTest { - val room = FakeMatrixRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ).apply { - // Needed to avoid discarding the loaded members as a partial and invalid result - givenRoomInfo(aRoomInfo(joinedMembersCount = 2)) - } - val presenter = createPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val initialState = awaitItem() - assertThat(initialState.roomMembers.isLoading()).isTrue() - assertThat(initialState.searchQuery).isEmpty() - assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) - assertThat(initialState.isSearchActive).isFalse() - room.givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - // Skip item while the new members state is processed - skipItems(1) - val loadedMembersState = awaitItem() - assertThat(loadedMembersState.roomMembers.isLoading()).isFalse() - assertThat(loadedMembersState.roomMembers.dataOrNull()?.invited).isEqualTo(listOf(aVictor(), aWalter())) - assertThat(loadedMembersState.roomMembers.dataOrNull()?.joined).isNotEmpty() - } - } - - @Test - fun `open search`() = runTest { - val presenter = createPresenter( - matrixRoom = FakeMatrixRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val loadedState = awaitItem() - loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) - skipItems(1) - val searchActiveState = awaitItem() - assertThat(searchActiveState.isSearchActive).isTrue() - } - } - - @Test - fun `search for something which is not found`() = runTest { - val presenter = createPresenter( - matrixRoom = FakeMatrixRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val loadedState = awaitItem() - loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) - val searchActiveState = awaitItem() - searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something")) - skipItems(1) - val searchQueryUpdatedState = awaitItem() - assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something") - val searchSearchResultDelivered = awaitItem() - assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) - } - } - - @Test - fun `search for something which is found`() = runTest { - val presenter = createPresenter( - matrixRoom = FakeMatrixRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val loadedState = awaitItem() - loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) - val searchActiveState = awaitItem() - searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("Alice")) - skipItems(1) - val searchQueryUpdatedState = awaitItem() - assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("Alice") - val searchSearchResultDelivered = awaitItem() - assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) - assertThat((searchSearchResultDelivered.searchResults as SearchBarResultState.Results).results.dataOrNull()!!.joined.first().displayName) - .isEqualTo("Alice") - } - } - - @Test - fun `present - asynchronously sets canInvite when user has correct power level`() = runTest { - val presenter = createPresenter( - matrixRoom = FakeMatrixRoom( - canInviteResult = { Result.success(true) }, - updateMembersResult = { Result.success(Unit) } - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val loadedState = awaitItem() - assertThat(loadedState.canInvite).isTrue() - } - } - - @Test - fun `present - asynchronously sets canInvite when user does not have correct power level`() = runTest { - val presenter = createPresenter( - matrixRoom = FakeMatrixRoom( - canInviteResult = { Result.success(false) }, - updateMembersResult = { Result.success(Unit) } - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val loadedState = awaitItem() - assertThat(loadedState.canInvite).isFalse() - } - } - - @Test - fun `present - asynchronously sets canInvite when power level check fails`() = runTest { - val presenter = createPresenter( - matrixRoom = FakeMatrixRoom( - canInviteResult = { Result.failure(Throwable("Eek")) }, - updateMembersResult = { Result.success(Unit) } - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val loadedState = awaitItem() - assertThat(loadedState.canInvite).isFalse() - } - } - - @Test - fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest { - val navigator = FakeRoomMemberListNavigator() - val roomMembersModerationStateLambda = { aRoomMembersModerationState(canDisplayModerationActions = false) } - val presenter = createPresenter( - roomMembersModerationStateLambda = roomMembersModerationStateLambda, - navigator = navigator, - matrixRoom = FakeMatrixRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) - assertThat(navigator.openRoomMemberDetailsCallCount).isEqualTo(1) - } - } - - @Test - fun `present - RoomMemberSelected will open the moderation options if the current user can use them`() = runTest { - val navigator = FakeRoomMemberListNavigator() - val eventsRecorder = EventsRecorder() - val roomMembersModerationStateLambda = { - aRoomMembersModerationState( - canDisplayModerationActions = true, - eventSink = eventsRecorder, - ) - } - val presenter = createPresenter( - roomMembersModerationStateLambda = roomMembersModerationStateLambda, - navigator = navigator, - matrixRoom = FakeMatrixRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) - eventsRecorder.assertSingle(RoomMembersModerationEvents.SelectRoomMember(aVictor())) - } - } -} - -private class FakeRoomMemberListNavigator : RoomMemberListNavigator { - var openRoomMemberDetailsCallCount = 0 - private set - - override fun openRoomMemberDetails(roomMemberId: UserId) { - openRoomMemberDetailsCallCount++ - } -} - -@ExperimentalCoroutinesApi -private fun TestScope.createDataSource( - matrixRoom: MatrixRoom = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) - }, - coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() -) = RoomMemberListDataSource(matrixRoom, coroutineDispatchers) - -@ExperimentalCoroutinesApi -private fun TestScope.createPresenter( - coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), - matrixRoom: MatrixRoom = FakeMatrixRoom( - updateMembersResult = { Result.success(Unit) } - ), - roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), - roomMembersModerationStateLambda: () -> RoomMembersModerationState = { aRoomMembersModerationState() }, - navigator: RoomMemberListNavigator = object : RoomMemberListNavigator {} -) = RoomMemberListPresenter( - room = matrixRoom, - roomMemberListDataSource = roomMemberListDataSource, - coroutineDispatchers = coroutineDispatchers, - roomMembersModerationPresenter = { roomMembersModerationStateLambda() }, - navigator = navigator -) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt deleted file mode 100644 index d837fda376e..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomdetails.members.details - -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.roomdetails.aMatrixRoom -import io.element.android.features.roomdetails.impl.members.aRoomMember -import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter -import io.element.android.features.userprofile.api.UserProfilePresenterFactory -import io.element.android.features.userprofile.shared.aUserProfileState -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState -import io.element.android.libraries.matrix.test.AN_EXCEPTION -import io.element.android.tests.testutils.WarmUpRule -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -@ExperimentalCoroutinesApi -class RoomMemberDetailsPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - returns the room member's data, then updates it if needed`() = runTest { - val roomMember = aRoomMember(displayName = "Alice") - val room = aMatrixRoom( - userDisplayNameResult = { Result.success("A custom name") }, - userAvatarUrlResult = { Result.success("A custom avatar") }, - getUpdatedMemberResult = { Result.success(roomMember) }, - ).apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember))) - } - val presenter = createRoomMemberDetailsPresenter( - room = room, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.userName).isEqualTo("Alice") - assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url") - skipItems(1) - val nextState = awaitItem() - assertThat(nextState.userName).isEqualTo("A custom name") - assertThat(nextState.avatarUrl).isEqualTo("A custom avatar") - } - } - - @Test - fun `present - will recover when retrieving room member details fails`() = runTest { - val roomMember = aRoomMember( - displayName = "Alice", - avatarUrl = "Alice Avatar url", - ) - val room = aMatrixRoom( - userDisplayNameResult = { Result.failure(Throwable()) }, - userAvatarUrlResult = { Result.failure(Throwable()) }, - getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) }, - ).apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember))) - } - - val presenter = createRoomMemberDetailsPresenter( - room = room, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.userName).isEqualTo("Alice") - assertThat(initialState.avatarUrl).isEqualTo("Alice Avatar url") - } - } - - @Test - fun `present - will fallback to original data if the updated data is null`() = runTest { - val roomMember = aRoomMember(displayName = "Alice") - val room = aMatrixRoom( - userDisplayNameResult = { Result.success(null) }, - userAvatarUrlResult = { Result.success(null) }, - getUpdatedMemberResult = { Result.success(roomMember) } - ).apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember))) - } - val presenter = createRoomMemberDetailsPresenter( - room = room, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.userName).isEqualTo("Alice") - assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url") - } - } - - @Test - fun `present - will fallback to user profile if user is not a member of the room`() = runTest { - val room = aMatrixRoom( - userDisplayNameResult = { Result.failure(Exception("Not a member!")) }, - userAvatarUrlResult = { Result.failure(Exception("Not a member!")) }, - getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) }, - ) - val presenter = createRoomMemberDetailsPresenter( - room = room, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.userName).isEqualTo("Profile user name") - assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url") - } - } - - @Test - fun `present - null cases`() = runTest { - val roomMember = aRoomMember( - displayName = null, - avatarUrl = null, - ) - val room = aMatrixRoom( - userDisplayNameResult = { Result.success(null) }, - userAvatarUrlResult = { Result.success(null) }, - getUpdatedMemberResult = { Result.success(roomMember) }, - ) - val presenter = createRoomMemberDetailsPresenter( - room = room, - userProfilePresenterFactory = { - Presenter { - aUserProfileState( - userName = null, - avatarUrl = null, - ) - } - }, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.userName).isNull() - assertThat(initialState.avatarUrl).isNull() - } - } - - private fun createRoomMemberDetailsPresenter( - room: MatrixRoom, - userProfilePresenterFactory: UserProfilePresenterFactory = UserProfilePresenterFactory { - Presenter { - aUserProfileState( - userName = "Profile user name", - avatarUrl = "Profile avatar url", - ) - } - }, - ): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter( - roomMemberId = UserId("@alice:server.org"), - room = room, - userProfilePresenterFactory = userProfilePresenterFactory - ) - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt deleted file mode 100644 index 0b3050bd90c..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.members.moderation - -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import im.vector.app.features.analytics.plan.RoomModeration -import io.element.android.features.roomdetails.impl.members.aRoomMember -import io.element.android.features.roomdetails.impl.members.aVictor -import io.element.android.features.roomdetails.impl.members.moderation.ConfirmingRoomMemberAction -import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipState -import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.A_USER_ID_2 -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.room.aRoomInfo -import io.element.android.services.analytics.test.FakeAnalyticsService -import io.element.android.tests.testutils.test -import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class RoomMembersModerationPresenterTest { - @Test - fun `canDisplayModerationActions - when room is DM is false`() = runTest { - val room = FakeMatrixRoom( - isDirect = true, - isPublic = true, - activeMemberCount = 2, - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ).apply { - givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2)) - } - val presenter = createRoomMembersModerationPresenter(matrixRoom = room) - presenter.test { - assertThat(awaitItem().canDisplayModerationActions).isFalse() - } - } - - @Test - fun `canDisplayModerationActions - when user can kick other users, FF is enabled and room is not a DM returns true`() = runTest { - val room = FakeMatrixRoom( - isDirect = false, - activeMemberCount = 10, - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val presenter = createRoomMembersModerationPresenter(matrixRoom = room) - presenter.test { - skipItems(1) - assertThat(awaitItem().canDisplayModerationActions).isTrue() - } - } - - @Test - fun `canDisplayModerationActions - when user can ban other users, FF is enabled and room is not a DM returns true`() = runTest { - val room = FakeMatrixRoom( - isDirect = false, - activeMemberCount = 10, - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val presenter = createRoomMembersModerationPresenter(matrixRoom = room) - presenter.test { - skipItems(1) - assertThat(awaitItem().canDisplayModerationActions).isTrue() - } - } - - @Test - fun `present - SelectRoomMember when the current user has permissions displays member actions`() = runTest { - val room = FakeMatrixRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val selectedMember = aVictor() - val presenter = createRoomMembersModerationPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) - with(awaitItem()) { - assertThat(this.selectedRoomMember).isNotNull() - assertThat(this.selectedRoomMember?.userId).isEqualTo(selectedMember.userId) - assertThat(actions).containsExactly( - ModerationAction.DisplayProfile(selectedMember.userId), - ModerationAction.KickUser(selectedMember.userId), - ModerationAction.BanUser(selectedMember.userId) - ) - } - } - } - - @Test - fun `present - SelectRoomMember displays only view profile if selected member has same power level as the current user`() = runTest { - val room = FakeMatrixRoom( - sessionId = A_USER_ID, - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L) - val presenter = createRoomMembersModerationPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) - with(awaitItem()) { - assertThat(this.selectedRoomMember).isNotNull() - assertThat(this.selectedRoomMember?.userId).isEqualTo(selectedMember.userId) - assertThat(actions).containsExactly( - ModerationAction.DisplayProfile(selectedMember.userId), - ) - } - } - } - - @Test - fun `present - SelectRoomMember displays an unban confirmation dialog when the member is banned`() = runTest { - val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN) - val room = FakeMatrixRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val presenter = createRoomMembersModerationPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) - with(awaitItem()) { - assertThat(selectedRoomMember).isNull() - assertThat(unbanUserAsyncAction).isEqualTo(ConfirmingRoomMemberAction(selectedMember)) - } - } - } - - @Test - fun `present - Kick removes the user`() = runTest { - val analyticsService = FakeAnalyticsService() - val room = FakeMatrixRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - kickUserResult = { _, _ -> Result.success(Unit) }, - ) - val selectedMember = aVictor() - val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) - awaitItem().eventSink(RoomMembersModerationEvents.KickUser) - skipItems(1) - val loadingState = awaitItem() - assertThat(loadingState.actions).isEmpty() - assertThat(loadingState.kickUserAsyncAction).isEqualTo(AsyncAction.Loading) - with(awaitItem()) { - assertThat(kickUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) - assertThat(selectedRoomMember).isNull() - } - assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.KickMember)) - } - } - - @Test - fun `present - BanUser requires confirmation and then bans the user`() = runTest { - val analyticsService = FakeAnalyticsService() - val room = FakeMatrixRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - banUserResult = { _, _ -> Result.success(Unit) }, - ) - val selectedMember = aVictor() - val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) - awaitItem().eventSink(RoomMembersModerationEvents.BanUser) - val confirmingState = awaitItem() - assertThat(confirmingState.banUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams) - - // Confirm - confirmingState.eventSink(RoomMembersModerationEvents.BanUser) - skipItems(1) - val loadingItem = awaitItem() - assertThat(loadingItem.actions).isEmpty() - assertThat(loadingItem.selectedRoomMember).isNull() - assertThat(loadingItem.banUserAsyncAction).isEqualTo(AsyncAction.Loading) - with(awaitItem()) { - assertThat(banUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) - assertThat(selectedRoomMember).isNull() - } - assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.BanMember)) - } - } - - @Test - fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest { - val analyticsService = FakeAnalyticsService() - val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN) - val room = FakeMatrixRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - unBanUserResult = { _, _ -> Result.success(Unit) }, - ).apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember))) - } - val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - // Displays unban confirmation dialog - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) - val confirmingState = awaitItem() - assertThat(confirmingState.selectedRoomMember).isNull() - assertThat(confirmingState.actions).isEmpty() - assertThat(confirmingState.unbanUserAsyncAction).isEqualTo(ConfirmingRoomMemberAction(selectedMember)) - // Confirms unban - confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(selectedMember.userId)) - assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Loading) - with(awaitItem()) { - assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) - assertThat(selectedRoomMember).isNull() - } - assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.UnbanMember)) - } - } - - @Test - fun `present - Reset removes the selected user and actions`() = runTest { - val room = FakeMatrixRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.USER) }, - ) - val presenter = createRoomMembersModerationPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - // Select a user - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) - // Reset state - awaitItem().eventSink(RoomMembersModerationEvents.Reset) - val finalItem = awaitItem() - assertThat(finalItem.selectedRoomMember).isNull() - assertThat(finalItem.actions).isEmpty() - } - } - - @Test - fun `present - Reset resets any async actions`() = runTest { - val room = FakeMatrixRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - kickUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, - banUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, - unBanUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, - userRoleResult = { Result.success(RoomMember.Role.USER) }, - ) - val presenter = createRoomMembersModerationPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialItem = awaitItem() - // Kick user and fail - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) - awaitItem().eventSink(RoomMembersModerationEvents.KickUser) - skipItems(1) - assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) - assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) - // Reset it - initialItem.eventSink(RoomMembersModerationEvents.Reset) - assertThat(awaitItem().kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) - - // Ban user and fail - initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) - awaitItem().eventSink(RoomMembersModerationEvents.BanUser) - awaitItem().eventSink(RoomMembersModerationEvents.BanUser) - skipItems(1) - assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) - assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) - // Reset it - initialItem.eventSink(RoomMembersModerationEvents.Reset) - assertThat(awaitItem().banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) - - // Unban user and fail - initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor().copy(membership = RoomMembershipState.BAN))) - val confirmingState = awaitItem() - assertThat(confirmingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Confirming::class.java) - confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(aVictor().userId)) - assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) - assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) - // Reset it - initialItem.eventSink(RoomMembersModerationEvents.Reset) - assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) - } - } - - private fun TestScope.createRoomMembersModerationPresenter( - matrixRoom: FakeMatrixRoom = FakeMatrixRoom(), - dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), - analyticsService: FakeAnalyticsService = FakeAnalyticsService(), - ): RoomMembersModerationPresenter { - return RoomMembersModerationPresenter( - room = matrixRoom, - dispatchers = dispatchers, - analyticsService = analyticsService, - ) - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt deleted file mode 100644 index dff3b2fd01b..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationViewTest.kt +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.members.moderation - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.roomdetails.impl.R -import io.element.android.features.roomdetails.impl.members.anAlice -import io.element.android.features.roomdetails.impl.members.moderation.ConfirmingRoomMemberAction -import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationView -import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureNeverCalledWithParam -import io.element.android.tests.testutils.EventsRecorder -import io.element.android.tests.testutils.clickOn -import io.element.android.tests.testutils.ensureCalledOnceWithParam -import io.element.android.tests.testutils.pressBackKey -import org.junit.Ignore -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 RoomMembersModerationViewTest { - @get:Rule val rule = createAndroidComposeRule() - - @Ignore("This test is not passing yet, need to investigate") - @Test - fun `clicking on back emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - actions = listOf( - ModerationAction.DisplayProfile(roomMember.userId), - ), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - rule.pressBackKey() - // Give time for the bottom sheet to animate - rule.mainClock.advanceTimeBy(1_000) - eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset) - } - - @Test - fun `clicking on 'See user info' invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - actions = listOf( - ModerationAction.DisplayProfile(roomMember.userId), - ), - eventSink = eventsRecorder - ) - ensureCalledOnceWithParam(roomMember.userId) { callback -> - rule.setRoomMembersModerationView( - state = state, - onDisplayMemberProfile = callback - ) - rule.clickOn(R.string.screen_room_member_list_manage_member_user_info) - } - } - - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on 'Remove member' emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - actions = listOf( - ModerationAction.DisplayProfile(roomMember.userId), - ModerationAction.KickUser(roomMember.userId), - ), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - rule.clickOn(R.string.screen_room_member_list_manage_member_remove) - // Give time for the bottom sheet to animate - rule.mainClock.advanceTimeBy(1_000) - eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser) - } - - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on 'Remove and ban member' emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - actions = listOf( - ModerationAction.DisplayProfile(roomMember.userId), - ModerationAction.KickUser(roomMember.userId), - ModerationAction.BanUser(roomMember.userId), - ), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(R.string.screen_room_member_list_manage_member_remove_confirmation_ban) - // Give time for the bottom sheet to animate - rule.mainClock.advanceTimeBy(1_000) - eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser) - } - - @Test - fun `cancelling 'Remove and ban member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - banUserAsyncAction = AsyncAction.ConfirmingNoParams, - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset) - } - - @Test - fun `confirming 'Remove and ban member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - banUserAsyncAction = AsyncAction.ConfirmingNoParams, - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(R.string.screen_room_member_list_ban_member_confirmation_action) - eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser) - } - - @Test - fun `cancelling 'Unban member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - unbanUserAsyncAction = ConfirmingRoomMemberAction(roomMember), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset) - } - - @Test - fun `confirming 'Unban member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - unbanUserAsyncAction = ConfirmingRoomMemberAction(roomMember), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(R.string.screen_room_member_list_manage_member_unban_action) - eventsRecorder.assertSingle(RoomMembersModerationEvents.UnbanUser(roomMember.userId)) - } -} - -private fun AndroidComposeTestRule.setRoomMembersModerationView( - state: RoomMembersModerationState, - onDisplayMemberProfile: (UserId) -> Unit = EnsureNeverCalledWithParam() -) { - setContent { - RoomMembersModerationView( - state = state, - onDisplayMemberProfile = onDisplayMemberProfile, - ) - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsPresenterTest.kt deleted file mode 100644 index 3ee1f9584c8..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsPresenterTest.kt +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.rolesandpermissions.permissions - -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.Event -import app.cash.turbine.TurbineTestContext -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import im.vector.app.features.analytics.plan.RoomModeration -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsEvent -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsPresenter -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsSection -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsState -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.RoomPermissionType -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.room.RoomMember.Role.ADMIN -import io.element.android.libraries.matrix.api.room.RoomMember.Role.MODERATOR -import io.element.android.libraries.matrix.api.room.RoomMember.Role.USER -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevels -import io.element.android.services.analytics.test.FakeAnalyticsService -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class ChangeRoomPermissionsPresenterTest { - @Test - fun `present - initial state`() = runTest { - val section = ChangeRoomPermissionsSection.RoomDetails - val presenter = createChangeRoomPermissionsPresenter(section = section) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - // Initial state, no permissions loaded - awaitItem().run { - assertThat(this.section).isEqualTo(section) - assertThat(this.currentPermissions).isNull() - assertThat(this.items).isNotEmpty() - assertThat(this.hasChanges).isFalse() - assertThat(this.saveAction).isEqualTo(AsyncAction.Uninitialized) - assertThat(this.confirmExitAction).isEqualTo(AsyncAction.Uninitialized) - } - - // Updated state, permissions loaded - assertThat(awaitItem().currentPermissions).isEqualTo(defaultPermissions()) - } - } - - @Test - fun `present - RoomDetails section contains the right items`() = runTest { - val section = ChangeRoomPermissionsSection.RoomDetails - val presenter = createChangeRoomPermissionsPresenter(section = section) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - assertThat(awaitUpdatedItem().items).containsExactly( - RoomPermissionType.ROOM_NAME, - RoomPermissionType.ROOM_AVATAR, - RoomPermissionType.ROOM_TOPIC, - ) - } - } - - @Test - fun `present - MessagesAndContent section contains the right items`() = runTest { - val section = ChangeRoomPermissionsSection.MessagesAndContent - val presenter = createChangeRoomPermissionsPresenter(section = section) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - assertThat(awaitUpdatedItem().items).containsExactly( - RoomPermissionType.SEND_EVENTS, - RoomPermissionType.REDACT_EVENTS, - ) - } - } - - @Test - fun `present - MembershipModeration section contains the right items`() = runTest { - val section = ChangeRoomPermissionsSection.MembershipModeration - val presenter = createChangeRoomPermissionsPresenter(section = section) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - assertThat(awaitUpdatedItem().items).containsExactly( - RoomPermissionType.INVITE, - RoomPermissionType.KICK, - RoomPermissionType.BAN, - ) - } - } - - @Test - fun `present - ChangeMinimumRoleForAction updates the current permissions and hasChanges`() = runTest { - val presenter = createChangeRoomPermissionsPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val state = awaitUpdatedItem() - assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel) - assertThat(state.hasChanges).isFalse() - - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) - - awaitItem().run { - assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel) - assertThat(hasChanges).isTrue() - } - } - } - - @Test - fun `present - ChangeMinimumRoleForAction works for all actions`() = runTest { - val presenter = createChangeRoomPermissionsPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val state = awaitUpdatedItem() - - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, MODERATOR)) - - val items = cancelAndConsumeRemainingEvents() - - (items.last() as? Event.Item)?.value?.run { - assertThat(currentPermissions).isEqualTo( - MatrixRoomPowerLevels( - invite = MODERATOR.powerLevel, - kick = MODERATOR.powerLevel, - ban = MODERATOR.powerLevel, - redactEvents = MODERATOR.powerLevel, - sendEvents = MODERATOR.powerLevel, - roomName = MODERATOR.powerLevel, - roomAvatar = MODERATOR.powerLevel, - roomTopic = MODERATOR.powerLevel, - ) - ) - } - } - } - - @Test - fun `present - Save updates the current permissions and resets hasChanges`() = runTest { - val analyticsService = FakeAnalyticsService() - val presenter = createChangeRoomPermissionsPresenter( - analyticsService = analyticsService, - room = FakeMatrixRoom( - updatePowerLevelsResult = { Result.success(Unit) }, - powerLevelsResult = { Result.success(defaultPermissions()) } - ), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val state = awaitUpdatedItem() - assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel) - assertThat(state.hasChanges).isFalse() - - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_AVATAR, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_TOPIC, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.SEND_EVENTS, MODERATOR)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.REDACT_EVENTS, USER)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.KICK, ADMIN)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.BAN, ADMIN)) - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.INVITE, ADMIN)) - skipItems(7) - assertThat(awaitItem().hasChanges).isTrue() - - state.eventSink(ChangeRoomPermissionsEvent.Save) - - assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Loading) - assertThat(awaitItem().hasChanges).isFalse() - awaitItem().run { - assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel) - assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) - } - assertThat(analyticsService.capturedEvents).containsExactlyElementsIn( - listOf( - RoomModeration(RoomModeration.Action.ChangePermissionsRoomName, RoomModeration.Role.Moderator), - RoomModeration(RoomModeration.Action.ChangePermissionsRoomAvatar, RoomModeration.Role.Moderator), - RoomModeration(RoomModeration.Action.ChangePermissionsRoomTopic, RoomModeration.Role.Moderator), - RoomModeration(RoomModeration.Action.ChangePermissionsSendMessages, RoomModeration.Role.Moderator), - RoomModeration(RoomModeration.Action.ChangePermissionsRedactMessages, RoomModeration.Role.User), - RoomModeration(RoomModeration.Action.ChangePermissionsKickMembers, RoomModeration.Role.Administrator), - RoomModeration(RoomModeration.Action.ChangePermissionsBanMembers, RoomModeration.Role.Administrator), - RoomModeration(RoomModeration.Action.ChangePermissionsInviteUsers, RoomModeration.Role.Administrator), - ) - ) - } - } - - @Test - fun `present - Save will fail if there are not current permissions`() = runTest { - val room = FakeMatrixRoom( - powerLevelsResult = { Result.failure(IllegalStateException("Failed to load power levels")) } - ) - val presenter = createChangeRoomPermissionsPresenter(room = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val state = awaitItem() - assertThat(state.currentPermissions).isNull() - - state.eventSink(ChangeRoomPermissionsEvent.Save) - assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java) - } - } - - @Test - fun `present - Save can handle failures and they can be cleared`() = runTest { - val room = FakeMatrixRoom( - powerLevelsResult = { Result.success(defaultPermissions()) }, - updatePowerLevelsResult = { Result.failure(IllegalStateException("Failed to update power levels")) }, - ) - val presenter = createChangeRoomPermissionsPresenter(room = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val state = awaitUpdatedItem() - assertThat(state.currentPermissions?.roomName).isEqualTo(ADMIN.powerLevel) - assertThat(state.hasChanges).isFalse() - - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) - assertThat(awaitItem().hasChanges).isTrue() - - state.eventSink(ChangeRoomPermissionsEvent.Save) - - assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Loading) - awaitItem().run { - assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel) - // Couldn't save the changes, so they're still pending - assertThat(hasChanges).isTrue() - assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) - } - - state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) - awaitItem().run { - assertThat(currentPermissions?.roomName).isEqualTo(MODERATOR.powerLevel) - assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) - assertThat(hasChanges).isTrue() - } - } - } - - @Test - fun `present - Exit does not need a confirmation when there are no pending changes`() = runTest { - val presenter = createChangeRoomPermissionsPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val state = awaitUpdatedItem() - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, MODERATOR)) - assertThat(awaitItem().hasChanges).isTrue() - - state.eventSink(ChangeRoomPermissionsEvent.Exit) - assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.ConfirmingNoParams) - - state.eventSink(ChangeRoomPermissionsEvent.Exit) - assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Success(Unit)) - } - } - - @Test - fun `present - Exit needs confirmation when there are pending changes`() = runTest { - val presenter = createChangeRoomPermissionsPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val state = awaitUpdatedItem() - - state.eventSink(ChangeRoomPermissionsEvent.Exit) - - assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Success(Unit)) - } - } - - private fun createChangeRoomPermissionsPresenter( - section: ChangeRoomPermissionsSection = ChangeRoomPermissionsSection.RoomDetails, - room: FakeMatrixRoom = FakeMatrixRoom( - powerLevelsResult = { Result.success(defaultPermissions()) } - ), - analyticsService: FakeAnalyticsService = FakeAnalyticsService(), - ) = ChangeRoomPermissionsPresenter( - section = section, - room = room, - analyticsService = analyticsService, - ) - - private fun defaultPermissions() = defaultRoomPowerLevels().run { - MatrixRoomPowerLevels( - invite = invite, - kick = kick, - ban = ban, - redactEvents = redactEvents, - sendEvents = sendEvents, - roomName = roomName, - roomAvatar = roomAvatar, - roomTopic = roomTopic, - ) - } - - private suspend fun TurbineTestContext.awaitUpdatedItem(): ChangeRoomPermissionsState { - skipItems(1) - return awaitItem() - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsViewTest.kt deleted file mode 100644 index 7ccce9e3de1..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/permissions/ChangeRoomPermissionsViewTest.kt +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomdetails.rolesandpermissions.permissions - -import androidx.activity.ComponentActivity -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.onFirst -import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.roomdetails.impl.R -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsEvent -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsSection -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsState -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.ChangeRoomPermissionsView -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.RoomPermissionType -import io.element.android.features.roomdetails.impl.rolesandpermissions.permissions.aChangeRoomPermissionsState -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureNeverCalled -import io.element.android.tests.testutils.EventsRecorder -import io.element.android.tests.testutils.clickOn -import io.element.android.tests.testutils.clickOnFirst -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 ChangeRoomPermissionsViewTest { - @get:Rule val rule = createAndroidComposeRule() - - @Test - fun `click on back icon invokes Exit`() { - val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( - eventsRecorder = recorder, - ) - rule.pressBack() - recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) - } - - @Test - fun `click on back key invokes Exit`() { - val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( - eventsRecorder = recorder, - ) - rule.pressBackKey() - recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) - } - - @Test - fun `when confirming exit with pending changes, using the back key actually exits`() { - val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( - state = aChangeRoomPermissionsState( - section = ChangeRoomPermissionsSection.RoomDetails, - hasChanges = true, - eventSink = recorder, - ), - eventsRecorder = recorder, - ) - rule.pressBackKey() - recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) - } - - @Test - fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() { - val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( - state = aChangeRoomPermissionsState( - section = ChangeRoomPermissionsSection.RoomDetails, - hasChanges = true, - confirmExitAction = AsyncAction.ConfirmingNoParams, - eventSink = recorder, - ), - eventsRecorder = recorder, - ) - 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`() { - val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( - state = aChangeRoomPermissionsState( - section = ChangeRoomPermissionsSection.RoomDetails, - hasChanges = true, - confirmExitAction = AsyncAction.ConfirmingNoParams, - eventSink = recorder, - ), - eventsRecorder = recorder, - ) - rule.clickOnFirst(CommonStrings.action_save) - recorder.assertSingle(ChangeRoomPermissionsEvent.Save) - } - - @Test - fun `click on a role item triggers ChangeRole event`() { - val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( - eventsRecorder = recorder, - ) - val admins = rule.activity.getText(R.string.screen_room_change_permissions_administrators).toString() - val moderators = rule.activity.getText(R.string.screen_room_change_permissions_moderators).toString() - val users = rule.activity.getText(R.string.screen_room_change_permissions_everyone).toString() - rule.onAllNodesWithText(admins).onFirst().performClick() - rule.onAllNodesWithText(moderators).onFirst().performClick() - rule.onAllNodesWithText(users).onFirst().performClick() - recorder.assertList( - listOf( - ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.ADMIN), - ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.MODERATOR), - ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, RoomMember.Role.USER), - ) - ) - } - - @Test - fun `click on the Save menu item triggers Save event`() { - val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( - state = aChangeRoomPermissionsState( - section = ChangeRoomPermissionsSection.RoomDetails, - hasChanges = true, - eventSink = recorder, - ), - eventsRecorder = recorder, - ) - rule.clickOn(CommonStrings.action_save) - recorder.assertSingle(ChangeRoomPermissionsEvent.Save) - } - - @Test - fun `a successful save exits the screen`() { - ensureCalledOnce { callback -> - rule.setChangeRoomPermissionsRule( - state = aChangeRoomPermissionsState( - section = ChangeRoomPermissionsSection.RoomDetails, - hasChanges = true, - saveAction = AsyncAction.Success(Unit), - ), - onBackClick = callback - ) - rule.clickOn(CommonStrings.action_save) - } - } - - @Test - fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() { - val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( - state = aChangeRoomPermissionsState( - section = ChangeRoomPermissionsSection.RoomDetails, - hasChanges = true, - saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels")), - eventSink = recorder, - ), - eventsRecorder = recorder, - ) - rule.clickOn(CommonStrings.action_ok) - recorder.assertSingle(ChangeRoomPermissionsEvent.ResetPendingActions) - } -} - -private fun AndroidComposeTestRule.setChangeRoomPermissionsRule( - eventsRecorder: EventsRecorder = EventsRecorder(expectEvents = false), - state: ChangeRoomPermissionsState = aChangeRoomPermissionsState( - section = ChangeRoomPermissionsSection.RoomDetails, - eventSink = eventsRecorder, - ), - onBackClick: () -> Unit = EnsureNeverCalled(), -) { - setContent { - ChangeRoomPermissionsView( - state = state, - onBackClick = onBackClick, - ) - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt deleted file mode 100644 index 4c10fff350d..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressPresenterTest.kt +++ /dev/null @@ -1,354 +0,0 @@ -/* - * 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.roomdetails.securityandprivacy.editroomaddress - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator -import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressEvents -import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressPresenter -import io.element.android.features.roomdetails.securityandprivacy.FakeSecurityAndPrivacyNavigator -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias -import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper -import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity -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.test.runTest -import org.junit.Test -import java.util.Optional - -class EditRoomAddressPresenterTest { - @Test - fun `present - initial state no address`() = runTest { - val presenter = createEditRoomAddressPresenter() - presenter.test { - with(awaitItem()) { - assertThat(homeserverName).isEqualTo("matrix.org") - assertThat(canBeSaved).isFalse() - assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) - assertThat(roomAddress).isEmpty() - } - } - } - - @Test - fun `present - initial state address matching own homeserver`() = runTest { - val room = FakeMatrixRoom( - canonicalAlias = RoomAlias("#canonical:matrix.org"), - ) - val presenter = createEditRoomAddressPresenter(room = room) - presenter.test { - with(awaitItem()) { - assertThat(homeserverName).isEqualTo("matrix.org") - assertThat(canBeSaved).isFalse() - assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) - assertThat(roomAddress).isEqualTo("canonical") - } - } - } - - @Test - fun `present - initial state address not matching own homeserver`() = runTest { - val room = FakeMatrixRoom( - canonicalAlias = RoomAlias("#canonical:notmatrix.org"), - ) - val presenter = createEditRoomAddressPresenter(room = room) - presenter.test { - with(awaitItem()) { - assertThat(homeserverName).isEqualTo("matrix.org") - assertThat(canBeSaved).isFalse() - assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) - assertThat(roomAddress).isEmpty() - } - } - } - - @Test - fun `present - room address change invalid state`() = runTest { - val roomAliasHelper = FakeRoomAliasHelper( - isRoomAliasValidLambda = { false } - ) - val presenter = createEditRoomAddressPresenter(roomAliasHelper = roomAliasHelper) - presenter.test { - with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("invalid")) - } - with(awaitItem()) { - assertThat(roomAddress).isEqualTo("invalid") - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) - } - with(awaitItem()) { - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols) - assertThat(canBeSaved).isFalse() - } - } - } - - @Test - fun `present - room address change valid state`() = runTest { - val presenter = createEditRoomAddressPresenter() - presenter.test { - with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) - } - with(awaitItem()) { - assertThat(roomAddress).isEqualTo("valid") - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) - } - with(awaitItem()) { - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) - assertThat(canBeSaved).isTrue() - } - } - } - - @Test - fun `present - room address change alias unavailable`() = runTest { - val client = createMatrixClient(isAliasAvailable = false) - val presenter = createEditRoomAddressPresenter(client = client) - presenter.test { - with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) - } - with(awaitItem()) { - assertThat(roomAddress).isEqualTo("valid") - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown) - } - with(awaitItem()) { - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable) - assertThat(canBeSaved).isFalse() - } - } - } - - @Test - fun `present - save success no current alias`() = runTest { - val publishAliasInRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } - val updateCanonicalAliasResult = lambdaRecorder, Result> { _, _ -> Result.success(Unit) } - val removeAliasFromRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } - val closeEditAddressLambda = lambdaRecorder { } - val navigator = FakeSecurityAndPrivacyNavigator( - closeEditRoomAddressLambda = closeEditAddressLambda - ) - val room = FakeMatrixRoom( - updateCanonicalAliasResult = updateCanonicalAliasResult, - publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult - ) - val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) - presenter.test { - with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) - } - skipItems(1) - with(awaitItem()) { - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) - assertThat(canBeSaved).isTrue() - eventSink(EditRoomAddressEvents.Save) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Loading) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) - } - - val createdAlias = RoomAlias("#valid:matrix.org") - assert(updateCanonicalAliasResult) - .isCalledOnce() - .with(value(createdAlias), value(emptyList())) - - assert(publishAliasInRoomDirectoryResult) - .isCalledOnce() - .with(value(createdAlias)) - - assert(removeAliasFromRoomDirectoryResult).isNeverCalled() - - assert(closeEditAddressLambda).isCalledOnce() - } - } - - @Test - fun `present - save success current canonical alias from own homeserver`() = runTest { - val publishAliasInRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } - val removeAliasFromRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } - val updateCanonicalAliasResult = lambdaRecorder, Result> { _, _ -> Result.success(Unit) } - val closeEditAddressLambda = lambdaRecorder { } - - val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) - val canonicalAlias = RoomAlias("#canonical:matrix.org") - val room = FakeMatrixRoom( - canonicalAlias = canonicalAlias, - updateCanonicalAliasResult = updateCanonicalAliasResult, - publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, - removeRoomAliasFromRoomDirectoryResult = removeAliasFromRoomDirectoryResult - ) - val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) - presenter.test { - with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) - } - skipItems(1) - with(awaitItem()) { - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) - assertThat(canBeSaved).isTrue() - eventSink(EditRoomAddressEvents.Save) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Loading) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) - } - - val createdAlias = RoomAlias("#valid:matrix.org") - assert(updateCanonicalAliasResult) - .isCalledOnce() - .with(value(createdAlias), value(emptyList())) - - assert(publishAliasInRoomDirectoryResult) - .isCalledOnce() - .with(value(createdAlias)) - - assert(removeAliasFromRoomDirectoryResult) - .isCalledOnce() - .with(value(canonicalAlias)) - - assert(closeEditAddressLambda).isCalledOnce() - } - } - - @Test - fun `present - save success current canonical alias from other homeserver`() = runTest { - val publishAliasInRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } - val removeAliasFromRoomDirectoryResult = lambdaRecorder> { _ -> Result.success(true) } - val updateCanonicalAliasResult = lambdaRecorder, Result> { _, _ -> Result.success(Unit) } - val closeEditAddressLambda = lambdaRecorder { } - - val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda) - val canonicalAlias = RoomAlias("#canonical:notmatrix.org") - val room = FakeMatrixRoom( - canonicalAlias = canonicalAlias, - updateCanonicalAliasResult = updateCanonicalAliasResult, - publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult, - removeRoomAliasFromRoomDirectoryResult = removeAliasFromRoomDirectoryResult - ) - val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) - presenter.test { - with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) - } - skipItems(1) - with(awaitItem()) { - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) - assertThat(canBeSaved).isTrue() - eventSink(EditRoomAddressEvents.Save) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Loading) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) - } - - val createdAlias = RoomAlias("#valid:matrix.org") - assert(updateCanonicalAliasResult) - .isCalledOnce() - .with(value(canonicalAlias), value(listOf(createdAlias))) - - assert(publishAliasInRoomDirectoryResult) - .isCalledOnce() - .with(value(createdAlias)) - - assert(removeAliasFromRoomDirectoryResult).isNeverCalled() - - assert(closeEditAddressLambda).isCalledOnce() - } - } - - @Test - fun `present - save failure`() = runTest { - val closeEditAddressLambda = lambdaRecorder { } - val navigator = FakeSecurityAndPrivacyNavigator( - closeEditRoomAddressLambda = closeEditAddressLambda - ) - val presenter = createEditRoomAddressPresenter(navigator = navigator) - presenter.test { - with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) - } - skipItems(1) - with(awaitItem()) { - assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) - assertThat(canBeSaved).isTrue() - eventSink(EditRoomAddressEvents.Save) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Loading) - } - with(awaitItem()) { - assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) - } - - assert(closeEditAddressLambda).isNeverCalled() - } - } - - @Test - fun `present - dismiss error`() = runTest { - val presenter = createEditRoomAddressPresenter() - presenter.test { - with(awaitItem()) { - eventSink(EditRoomAddressEvents.Save) - } - with(awaitItem()) { - assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) - eventSink(EditRoomAddressEvents.DismissError) - } - with(awaitItem()) { - assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) - } - } - } - - private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient( - userIdServerNameLambda = { "matrix.org" }, - resolveRoomAliasResult = { - val resolvedRoomAlias = if (isAliasAvailable) { - Optional.empty() - } else { - Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList())) - } - Result.success(resolvedRoomAlias) - } - ) - - private fun createEditRoomAddressPresenter( - client: FakeMatrixClient = createMatrixClient(), - room: MatrixRoom = FakeMatrixRoom(), - navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(), - roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper() - ): EditRoomAddressPresenter { - return EditRoomAddressPresenter( - room = room, - client = client, - roomAliasHelper = roomAliasHelper, - navigator = navigator - ) - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressViewTest.kt deleted file mode 100644 index b5c3a8b3bd7..00000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/securityandprivacy/editroomaddress/EditRoomAddressViewTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.roomdetails.securityandprivacy.editroomaddress - -import androidx.activity.ComponentActivity -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.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressEvents -import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressState -import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressView -import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.anEditRoomAddressState -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity -import io.element.android.libraries.testtags.TestTags -import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureNeverCalled -import io.element.android.tests.testutils.EventsRecorder -import io.element.android.tests.testutils.clickOn -import io.element.android.tests.testutils.ensureCalledOnce -import io.element.android.tests.testutils.pressBack -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class EditRoomAddressViewTest { - @get:Rule val rule = createAndroidComposeRule() - - @Test - fun `click on back invokes expected callback`() { - ensureCalledOnce { callback -> - rule.setEditRoomAddressView(onBackClick = callback) - rule.pressBack() - } - } - - @Test - fun `click on disabled save doesn't emit event`() { - val recorder = EventsRecorder(expectEvents = false) - val state = anEditRoomAddressState(eventSink = recorder) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_save) - recorder.assertEmpty() - } - - @Test - fun `click on enabled save emits the expected event`() { - val recorder = EventsRecorder() - val state = anEditRoomAddressState( - roomAddress = "room", - roomAddressValidity = RoomAddressValidity.Valid, - eventSink = recorder - ) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_save) - recorder.assertSingle(EditRoomAddressEvents.Save) - } - - @Test - fun `text changes on text field emits the expected event`() { - val recorder = EventsRecorder() - val state = anEditRoomAddressState( - roomAddress = "", - eventSink = recorder - ) - rule.setEditRoomAddressView(state) - - rule.onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") - recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias")) - } - - @Test - fun `click on dismiss error emits the expected event`() { - val recorder = EventsRecorder() - val state = anEditRoomAddressState( - roomAddress = "", - saveAction = AsyncAction.Failure(IllegalStateException()), - eventSink = recorder - ) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_cancel) - recorder.assertSingle(EditRoomAddressEvents.DismissError) - } - - @Test - fun `click on retry error emits the expected event`() { - val recorder = EventsRecorder() - val state = anEditRoomAddressState( - roomAddress = "", - saveAction = AsyncAction.Failure(IllegalStateException()), - eventSink = recorder - ) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_retry) - recorder.assertSingle(EditRoomAddressEvents.Save) - } -} - -private fun AndroidComposeTestRule.setEditRoomAddressView( - state: EditRoomAddressState = anEditRoomAddressState( - eventSink = EventsRecorder(expectEvents = false), - ), - onBackClick: () -> Unit = EnsureNeverCalled(), -) { - setContent { - EditRoomAddressView( - state = state, - onBackClick = onBackClick, - ) - } -} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index fc7f68d5061..8b2944f745a 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -40,10 +40,10 @@ import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.impl.R import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType 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.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.FilledTextField import io.element.android.libraries.designsystem.theme.components.Icon @@ -90,12 +90,7 @@ private fun RoomDirectoryTopBar( navigationIcon = { BackButton(onClick = onBackClick) }, - title = { - Text( - text = stringResource(id = R.string.screen_room_directory_search_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - } + titleStr = stringResource(id = R.string.screen_room_directory_search_title), ) } @@ -248,7 +243,8 @@ private fun RoomDirectoryRoomRow( ) { Avatar( avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem), - modifier = Modifier.align(Alignment.CenterVertically) + avatarType = AvatarType.Room(), + modifier = Modifier.align(Alignment.CenterVertically), ) Column( modifier = Modifier diff --git a/features/roomdirectory/impl/src/main/res/values-cy/translations.xml b/features/roomdirectory/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..7e5572c943a --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,5 @@ + + + "Wedi methu llwytho" + "Cyfeiriadur ystafelloedd" + diff --git a/features/roomdirectory/impl/src/main/res/values-da/translations.xml b/features/roomdirectory/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..34717235281 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,5 @@ + + + "Indlæsning mislykkedes" + "Register over rum" + diff --git a/features/roomdirectory/impl/src/main/res/values-el/translations.xml b/features/roomdirectory/impl/src/main/res/values-el/translations.xml index 9b3f8bb12d3..c2dad699794 100644 --- a/features/roomdirectory/impl/src/main/res/values-el/translations.xml +++ b/features/roomdirectory/impl/src/main/res/values-el/translations.xml @@ -1,5 +1,5 @@ "Αποτυχία φόρτωσης" - "Κατάλογος δωματίων" + "Κατάλογος αιθουσών" diff --git a/features/roomdirectory/impl/src/main/res/values-es/translations.xml b/features/roomdirectory/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..341799cc6cd --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,5 @@ + + + "Carga fallida" + "Directorio de salas" + diff --git a/features/roomdirectory/impl/src/main/res/values-eu/translations.xml b/features/roomdirectory/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..63c2cda14a5 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,5 @@ + + + "Ezin izan da kargatu" + "Gelen direktorioa" + diff --git a/features/roomdirectory/impl/src/main/res/values-in/translations.xml b/features/roomdirectory/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..3cf6342e1bc --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,5 @@ + + + "Gagal memuat" + "Direktori ruangan" + diff --git a/features/roomdirectory/impl/src/main/res/values-ka/translations.xml b/features/roomdirectory/impl/src/main/res/values-ka/translations.xml new file mode 100644 index 00000000000..86290f07284 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-ka/translations.xml @@ -0,0 +1,5 @@ + + + "ჩატვირთვა წარუმატებელია" + "ოთახის კატალოგი" + diff --git a/features/roomdirectory/impl/src/main/res/values-pt-rBR/translations.xml b/features/roomdirectory/impl/src/main/res/values-pt-rBR/translations.xml index 07bbad975f8..ab2e51a531e 100644 --- a/features/roomdirectory/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/roomdirectory/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,4 +1,5 @@ + "Falha no carregamento" "Diretório de salas" diff --git a/features/roomdirectory/impl/src/main/res/values-tr/translations.xml b/features/roomdirectory/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..22a76932e36 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,5 @@ + + + "Yükleme başarısız" + "Oda dizini" + diff --git a/features/roomdirectory/impl/src/main/res/values-ur/translations.xml b/features/roomdirectory/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..a6b7a127f13 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,5 @@ + + + "لادنا ناکام" + "کمرے کا راہنامچہ" + diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt index 3165d1f928e..d6ebb6cd955 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt @@ -26,7 +26,8 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test -@OptIn(ExperimentalCoroutinesApi::class) class RoomDirectoryPresenterTest { +@OptIn(ExperimentalCoroutinesApi::class) +class RoomDirectoryPresenterTest { @Test fun `present - initial state`() = runTest { val presenter = createRoomDirectoryPresenter() diff --git a/features/roomlist/api/build.gradle.kts b/features/roomlist/api/build.gradle.kts deleted file mode 100644 index e7486a80c04..00000000000 --- a/features/roomlist/api/build.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ -plugins { - id("io.element.android-library") -} - -android { - namespace = "io.element.android.features.roomlist.api" -} - -dependencies { - implementation(projects.libraries.architecture) - implementation(projects.libraries.matrix.api) -} diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt deleted file mode 100644 index 97325fc247e..00000000000 --- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomlist.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 - -interface RoomListEntryPoint : FeatureEntryPoint { - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder - interface NodeBuilder { - fun callback(callback: Callback): NodeBuilder - fun build(): Node - } - - interface Callback : Plugin { - fun onRoomClick(roomId: RoomId) - fun onCreateRoomClick() - fun onSettingsClick() - fun onSetUpRecoveryClick() - fun onSessionConfirmRecoveryKeyClick() - fun onRoomSettingsClick(roomId: RoomId) - fun onReportBugClick() - fun onRoomDirectorySearchClick() - fun onLogoutForNativeSlidingSyncMigrationNeeded() - } -} diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts deleted file mode 100644 index a39971b4157..00000000000 --- a/features/roomlist/impl/build.gradle.kts +++ /dev/null @@ -1,77 +0,0 @@ -import extension.setupAnvil - -/* - * Copyright 2022-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. - */ - -plugins { - id("io.element.android-compose-library") - id("kotlin-parcelize") -} - -android { - namespace = "io.element.android.features.roomlist.impl" - - testOptions { - unitTests { - isIncludeAndroidResources = true - } - } -} - -setupAnvil() - -dependencies { - implementation(projects.appconfig) - 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.matrixui) - implementation(projects.libraries.designsystem) - implementation(projects.libraries.testtags) - implementation(projects.libraries.uiStrings) - implementation(projects.libraries.dateformatter.api) - implementation(projects.libraries.eventformatter.api) - implementation(projects.libraries.indicator.api) - implementation(projects.libraries.deeplink) - implementation(projects.libraries.fullscreenintent.api) - implementation(projects.libraries.permissions.api) - implementation(projects.libraries.permissions.noop) - implementation(projects.libraries.preferences.api) - implementation(projects.libraries.push.api) - implementation(projects.features.invite.api) - implementation(projects.features.networkmonitor.api) - implementation(projects.features.logout.api) - implementation(projects.features.leaveroom.api) - implementation(projects.services.analytics.api) - implementation(libs.androidx.datastore.preferences) - api(projects.features.roomlist.api) - - testImplementation(libs.androidx.compose.ui.test.junit) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(libs.test.robolectric) - testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.featureflag.test) - testImplementation(projects.libraries.dateformatter.test) - testImplementation(projects.libraries.eventformatter.test) - testImplementation(projects.libraries.indicator.impl) - testImplementation(projects.libraries.permissions.noop) - testImplementation(projects.libraries.permissions.test) - testImplementation(projects.libraries.preferences.test) - testImplementation(projects.libraries.push.test) - testImplementation(projects.services.analytics.test) - testImplementation(projects.services.toolbox.test) - testImplementation(projects.features.networkmonitor.test) - testImplementation(projects.features.logout.test) - testImplementation(projects.tests.testutils) -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomListEntryPoint.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomListEntryPoint.kt deleted file mode 100644 index 727a4046021..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomListEntryPoint.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomlist.impl - -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.roomlist.api.RoomListEntryPoint -import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultRoomListEntryPoint @Inject constructor() : RoomListEntryPoint { - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomListEntryPoint.NodeBuilder { - val plugins = ArrayList() - - return object : RoomListEntryPoint.NodeBuilder { - override fun callback(callback: RoomListEntryPoint.Callback): RoomListEntryPoint.NodeBuilder { - plugins += callback - return this - } - - override fun build(): Node { - return parentNode.createNode(buildContext, plugins) - } - } - } -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt deleted file mode 100644 index a95f6b7b6d8..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomlist.impl - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState -import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -open class RoomListContentStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aRoomsContentState(), - aRoomsContentState(summaries = persistentListOf()), - aSkeletonContentState(), - anEmptyContentState(), - anEmptyContentState(securityBannerState = SecurityBannerState.SetUpRecovery), - aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration), - ) -} - -internal fun aRoomsContentState( - securityBannerState: SecurityBannerState = SecurityBannerState.None, - summaries: ImmutableList = aRoomListRoomSummaryList(), - fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), -) = RoomListContentState.Rooms( - securityBannerState = securityBannerState, - fullScreenIntentPermissionsState = fullScreenIntentPermissionsState, - summaries = summaries, -) - -internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16) - -internal fun anEmptyContentState( - securityBannerState: SecurityBannerState = SecurityBannerState.None, -) = RoomListContentState.Empty( - securityBannerState = securityBannerState, -) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt deleted file mode 100644 index 9e9f7013803..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomlist.impl - -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.libraries.matrix.api.core.RoomId - -sealed interface RoomListEvents { - data class UpdateVisibleRange(val range: IntRange) : RoomListEvents - data object DismissRequestVerificationPrompt : RoomListEvents - data object DismissBanner : RoomListEvents - data object ToggleSearchResults : RoomListEvents - data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents - data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents - data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents - - sealed interface ContextMenuEvents : RoomListEvents - data object HideContextMenu : ContextMenuEvents - data class LeaveRoom(val roomId: RoomId) : ContextMenuEvents - data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents - data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents - data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents - data class ClearCacheOfRoom(val roomId: RoomId) : ContextMenuEvents -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt deleted file mode 100644 index ba69e31ca2c..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomlist.impl - -import android.app.Activity -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import com.bumble.appyx.core.lifecycle.subscribe -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import com.bumble.appyx.core.plugin.plugins -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.invite.api.response.AcceptDeclineInviteView -import io.element.android.features.logout.api.direct.DirectLogoutEvents -import io.element.android.features.logout.api.direct.DirectLogoutView -import io.element.android.features.roomlist.api.RoomListEntryPoint -import io.element.android.features.roomlist.impl.components.RoomListMenuAction -import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase -import io.element.android.services.analytics.api.AnalyticsService - -@ContributesNode(SessionScope::class) -class RoomListNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: RoomListPresenter, - private val inviteFriendsUseCase: InviteFriendsUseCase, - private val analyticsService: AnalyticsService, - private val acceptDeclineInviteView: AcceptDeclineInviteView, - private val directLogoutView: DirectLogoutView, - private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase, -) : Node(buildContext, plugins = plugins) { - init { - lifecycle.subscribe( - onResume = { - analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Home)) - } - ) - } - - private fun onRoomClick(roomId: RoomId) { - plugins().forEach { it.onRoomClick(roomId) } - } - - private fun onOpenSettings() { - plugins().forEach { it.onSettingsClick() } - } - - private fun onCreateRoomClick() { - plugins().forEach { it.onCreateRoomClick() } - } - - private fun onSetUpRecoveryClick() { - plugins().forEach { it.onSetUpRecoveryClick() } - } - - private fun onSessionConfirmRecoveryKeyClick() { - plugins().forEach { it.onSessionConfirmRecoveryKeyClick() } - } - - private fun onRoomSettingsClick(roomId: RoomId) { - plugins().forEach { it.onRoomSettingsClick(roomId) } - } - - private fun onMenuActionClick(activity: Activity, roomListMenuAction: RoomListMenuAction) { - when (roomListMenuAction) { - RoomListMenuAction.InviteFriends -> { - inviteFriendsUseCase.execute(activity) - } - RoomListMenuAction.ReportBug -> { - plugins().forEach { it.onReportBugClick() } - } - } - } - - private fun onRoomDirectorySearchClick() { - plugins().forEach { it.onRoomDirectorySearchClick() } - } - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - val activity = LocalContext.current as Activity - - RoomListView( - state = state, - onRoomClick = this::onRoomClick, - onSettingsClick = this::onOpenSettings, - onCreateRoomClick = this::onCreateRoomClick, - onSetUpRecoveryClick = this::onSetUpRecoveryClick, - onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick, - onRoomSettingsClick = this::onRoomSettingsClick, - onMenuActionClick = { onMenuActionClick(activity, it) }, - onRoomDirectorySearchClick = this::onRoomDirectorySearchClick, - onMigrateToNativeSlidingSyncClick = { - if (state.directLogoutState.canDoDirectSignOut) { - state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) - } else { - plugins().forEach { it.onLogoutForNativeSlidingSyncMigrationNeeded() } - } - }, - modifier = modifier, - ) { - acceptDeclineInviteView.Render( - state = state.acceptDeclineInviteState, - onAcceptInvite = this::onRoomClick, - onDeclineInvite = { }, - modifier = Modifier - ) - } - - directLogoutView.Render(state.directLogoutState) { - enableNativeSlidingSyncUseCase() - } - } -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt deleted file mode 100644 index 59918848562..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ /dev/null @@ -1,362 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomlist.impl - -import androidx.annotation.VisibleForTesting -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.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import im.vector.app.features.analytics.plan.Interaction -import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.InviteData -import io.element.android.features.leaveroom.api.LeaveRoomEvent -import io.element.android.features.leaveroom.api.LeaveRoomState -import io.element.android.features.logout.api.direct.DirectLogoutState -import io.element.android.features.roomlist.impl.datasource.RoomListDataSource -import io.element.android.features.roomlist.impl.filters.RoomListFiltersState -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.features.roomlist.impl.search.RoomListSearchEvents -import io.element.android.features.roomlist.impl.search.RoomListSearchState -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher -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.fullscreenintent.api.FullScreenIntentPermissionsState -import io.element.android.libraries.indicator.api.IndicatorService -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.encryption.EncryptionService -import io.element.android.libraries.matrix.api.encryption.RecoveryState -import io.element.android.libraries.matrix.api.roomlist.RoomList -import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion -import io.element.android.libraries.matrix.api.sync.SyncService -import io.element.android.libraries.matrix.api.sync.isOnline -import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.preferences.api.store.SessionPreferencesStore -import io.element.android.libraries.push.api.notifications.NotificationCleaner -import io.element.android.services.analytics.api.AnalyticsService -import io.element.android.services.analyticsproviders.api.trackers.captureInteraction -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.takeWhile -import kotlinx.coroutines.launch -import javax.inject.Inject - -private const val EXTENDED_RANGE_SIZE = 40 -private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L - -class RoomListPresenter @Inject constructor( - private val client: MatrixClient, - private val syncService: SyncService, - private val snackbarDispatcher: SnackbarDispatcher, - private val leaveRoomPresenter: Presenter, - private val roomListDataSource: RoomListDataSource, - private val featureFlagService: FeatureFlagService, - private val indicatorService: IndicatorService, - private val filtersPresenter: Presenter, - private val searchPresenter: Presenter, - private val sessionPreferencesStore: SessionPreferencesStore, - private val analyticsService: AnalyticsService, - private val acceptDeclineInvitePresenter: Presenter, - private val fullScreenIntentPermissionsPresenter: Presenter, - private val notificationCleaner: NotificationCleaner, - private val logoutPresenter: Presenter, -) : Presenter { - private val encryptionService: EncryptionService = client.encryptionService() - - @Composable - override fun present(): RoomListState { - val coroutineScope = rememberCoroutineScope() - val leaveRoomState = leaveRoomPresenter.present() - val matrixUser = client.userProfile.collectAsState() - val isOnline by syncService.isOnline().collectAsState() - val filtersState = filtersPresenter.present() - val searchState = searchPresenter.present() - val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() - - LaunchedEffect(Unit) { - roomListDataSource.launchIn(this) - // Force a refresh of the profile - client.getUserProfile() - } - - var securityBannerDismissed by rememberSaveable { mutableStateOf(true) } - - // Avatar indicator - val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() - - val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } - - val directLogoutState = logoutPresenter.present() - - fun handleEvents(event: RoomListEvents) { - when (event) { - is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch { - updateVisibleRange(event.range) - } - RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true - RoomListEvents.DismissBanner -> securityBannerDismissed = true - RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility) - is RoomListEvents.ShowContextMenu -> { - coroutineScope.showContextMenu(event, contextMenu) - } - is RoomListEvents.HideContextMenu -> { - contextMenu.value = RoomListState.ContextMenu.Hidden - } - is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) - is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite) - is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId) - is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId) - is RoomListEvents.AcceptInvite -> { - acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(event.roomListRoomSummary.toInviteData()) - ) - } - is RoomListEvents.DeclineInvite -> { - acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(event.roomListRoomSummary.toInviteData()) - ) - } - is RoomListEvents.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId) - } - } - - val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() - - val contentState = roomListContentState(securityBannerDismissed) - - return RoomListState( - matrixUser = matrixUser.value, - showAvatarIndicator = showAvatarIndicator, - snackbarMessage = snackbarMessage, - hasNetworkConnection = isOnline, - contextMenu = contextMenu.value, - leaveRoomState = leaveRoomState, - filtersState = filtersState, - searchState = searchState, - contentState = contentState, - acceptDeclineInviteState = acceptDeclineInviteState, - directLogoutState = directLogoutState, - eventSink = ::handleEvents, - ) - } - - @Composable - private fun rememberSecurityBannerState( - securityBannerDismissed: Boolean, - needsSlidingSyncMigration: Boolean, - ): State { - val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed) - val currentNeedsSlidingSyncMigration by rememberUpdatedState(needsSlidingSyncMigration) - val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() - return remember { - derivedStateOf { - calculateBannerState( - securityBannerDismissed = currentSecurityBannerDismissed, - needsSlidingSyncMigration = currentNeedsSlidingSyncMigration, - recoveryState = recoveryState, - ) - } - } - } - - private fun calculateBannerState( - securityBannerDismissed: Boolean, - needsSlidingSyncMigration: Boolean, - recoveryState: RecoveryState, - ): SecurityBannerState { - if (securityBannerDismissed) { - return SecurityBannerState.None - } - - when (recoveryState) { - RecoveryState.DISABLED -> return SecurityBannerState.SetUpRecovery - RecoveryState.INCOMPLETE -> return SecurityBannerState.RecoveryKeyConfirmation - RecoveryState.UNKNOWN, - RecoveryState.WAITING_FOR_SYNC, - RecoveryState.ENABLED -> Unit - } - - if (needsSlidingSyncMigration) { - return SecurityBannerState.NeedsNativeSlidingSyncMigration - } - - return SecurityBannerState.None - } - - @Composable - private fun roomListContentState( - securityBannerDismissed: Boolean, - ): RoomListContentState { - val roomSummaries by produceState(initialValue = AsyncData.Loading()) { - roomListDataSource.allRooms.collect { value = AsyncData.Success(it) } - } - val loadingState by roomListDataSource.loadingState.collectAsState() - val showEmpty by remember { - derivedStateOf { - (loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0 - } - } - val showSkeleton by remember { - derivedStateOf { - loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading - } - } - val needsSlidingSyncMigration by produceState(false) { - value = client.needsSlidingSyncMigration().getOrDefault(false) - } - val securityBannerState by rememberSecurityBannerState(securityBannerDismissed, needsSlidingSyncMigration) - return when { - showEmpty -> RoomListContentState.Empty(securityBannerState = securityBannerState) - showSkeleton -> RoomListContentState.Skeleton(count = 16) - else -> { - RoomListContentState.Rooms( - securityBannerState = securityBannerState, - fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(), - summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList() - ) - } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState) = launch { - val initialState = RoomListState.ContextMenu.Shown( - roomId = event.roomListRoomSummary.roomId, - roomName = event.roomListRoomSummary.name, - isDm = event.roomListRoomSummary.isDm, - isFavorite = event.roomListRoomSummary.isFavorite, - markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread), - hasNewContent = event.roomListRoomSummary.hasNewContent, - eventCacheFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.EventCache), - ) - contextMenuState.value = initialState - - client.getRoom(event.roomListRoomSummary.roomId)?.use { room -> - - val isShowingContextMenuFlow = snapshotFlow { contextMenuState.value is RoomListState.ContextMenu.Shown } - .distinctUntilChanged() - - val isFavoriteFlow = room.roomInfoFlow - .map { it.isFavorite } - .distinctUntilChanged() - - isFavoriteFlow - .onEach { isFavorite -> - contextMenuState.value = initialState.copy(isFavorite = isFavorite) - } - .flatMapLatest { isShowingContextMenuFlow } - .takeWhile { isShowingContextMenu -> isShowingContextMenu } - .collect() - } - } - - private fun CoroutineScope.setRoomIsFavorite(roomId: RoomId, isFavorite: Boolean) = launch { - client.getRoom(roomId)?.use { room -> - room.setIsFavorite(isFavorite) - .onSuccess { - analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuFavouriteToggle) - } - } - } - - private fun CoroutineScope.markAsRead(roomId: RoomId) = launch { - notificationCleaner.clearMessagesForRoom(client.sessionId, roomId) - client.getRoom(roomId)?.use { room -> - 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(roomId: RoomId) = launch { - client.getRoom(roomId)?.use { room -> - room.setUnreadFlag(isUnread = true) - .onSuccess { - analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle) - } - } - } - - private fun CoroutineScope.clearCacheOfRoom(roomId: RoomId) = launch { - client.getRoom(roomId)?.use { room -> - room.clearEventCacheStorage() - } - } - - /** - * Checks if the user needs to migrate to a native sliding sync version. - */ - private suspend fun MatrixClient.needsSlidingSyncMigration(): Result = runCatching { - val currentSlidingSyncVersion = currentSlidingSyncVersion().getOrThrow() - if (currentSlidingSyncVersion != SlidingSyncVersion.Native) { - val availableSlidingSyncVersions = availableSlidingSyncVersions().getOrThrow() - availableSlidingSyncVersions.contains(SlidingSyncVersion.Native) - } else { - false - } - } - - private var currentUpdateVisibleRangeJob: Job? = null - private fun CoroutineScope.updateVisibleRange(range: IntRange) { - currentUpdateVisibleRangeJob?.cancel() - currentUpdateVisibleRangeJob = launch { - // Debounce the subscription to avoid subscribing to too many rooms - delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS) - - if (range.isEmpty()) return@launch - val currentRoomList = roomListDataSource.allRooms.first() - // Use extended range to 'prefetch' the next rooms info - val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2 - val extendedRange = range.first until range.last + midExtendedRangeSize - val roomIds = extendedRange.mapNotNull { index -> - currentRoomList.getOrNull(index)?.roomId - } - roomListDataSource.subscribeToVisibleRooms(roomIds) - } - } -} - -@VisibleForTesting -internal fun RoomListRoomSummary.toInviteData() = InviteData( - roomId = roomId, - // Note: `name` should not be null at this point, but just in case, fallback to the roomId - roomName = name ?: roomId.value, - isDm = isDm, -) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt deleted file mode 100644 index 65cf5a35a6e..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomlist.impl - -import androidx.compose.runtime.Immutable -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.leaveroom.api.LeaveRoomState -import io.element.android.features.logout.api.direct.DirectLogoutState -import io.element.android.features.roomlist.impl.filters.RoomListFiltersState -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.features.roomlist.impl.search.RoomListSearchState -import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.user.MatrixUser -import kotlinx.collections.immutable.ImmutableList - -@Immutable -data class RoomListState( - val matrixUser: MatrixUser, - val showAvatarIndicator: Boolean, - val hasNetworkConnection: Boolean, - val snackbarMessage: SnackbarMessage?, - val contextMenu: ContextMenu, - val leaveRoomState: LeaveRoomState, - val filtersState: RoomListFiltersState, - val searchState: RoomListSearchState, - val contentState: RoomListContentState, - val acceptDeclineInviteState: AcceptDeclineInviteState, - val directLogoutState: DirectLogoutState, - val eventSink: (RoomListEvents) -> Unit, -) { - val displayFilters = contentState is RoomListContentState.Rooms - val displayActions = true - - sealed interface ContextMenu { - data object Hidden : ContextMenu - data class Shown( - val roomId: RoomId, - val roomName: String?, - val isDm: Boolean, - val isFavorite: Boolean, - val markAsUnreadFeatureFlagEnabled: Boolean, - val eventCacheFeatureFlagEnabled: Boolean, - val hasNewContent: Boolean, - ) : ContextMenu - } -} - -enum class SecurityBannerState { - None, - SetUpRecovery, - RecoveryKeyConfirmation, - NeedsNativeSlidingSyncMigration, -} - -@Immutable -sealed interface RoomListContentState { - data class Skeleton(val count: Int) : RoomListContentState - data class Empty( - val securityBannerState: SecurityBannerState, - ) : RoomListContentState - data class Rooms( - val securityBannerState: SecurityBannerState, - val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState, - val summaries: ImmutableList, - ) : RoomListContentState -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt deleted file mode 100644 index 4c21d797f16..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomlist.impl - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.invite.api.response.AcceptDeclineInviteState -import io.element.android.features.invite.api.response.anAcceptDeclineInviteState -import io.element.android.features.leaveroom.api.LeaveRoomState -import io.element.android.features.leaveroom.api.aLeaveRoomState -import io.element.android.features.logout.api.direct.DirectLogoutState -import io.element.android.features.logout.api.direct.aDirectLogoutState -import io.element.android.features.roomlist.impl.filters.RoomListFiltersState -import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType -import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary -import io.element.android.features.roomlist.impl.model.anInviteSender -import io.element.android.features.roomlist.impl.search.RoomListSearchState -import io.element.android.features.roomlist.impl.search.aRoomListSearchState -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -open class RoomListStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aRoomListState(), - aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), - aRoomListState(hasNetworkConnection = false), - aRoomListState(contextMenu = aContextMenuShown(roomName = null)), - aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")), - aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)), - aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)), - aRoomListState(contentState = anEmptyContentState()), - aRoomListState(contentState = aSkeletonContentState()), - aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), - aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)), - ) -} - -internal fun aRoomListState( - matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), - showAvatarIndicator: Boolean = false, - hasNetworkConnection: Boolean = true, - snackbarMessage: SnackbarMessage? = null, - contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden, - leaveRoomState: LeaveRoomState = aLeaveRoomState(), - searchState: RoomListSearchState = aRoomListSearchState(), - filtersState: RoomListFiltersState = aRoomListFiltersState(), - contentState: RoomListContentState = aRoomsContentState(), - acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), - directLogoutState: DirectLogoutState = aDirectLogoutState(), - eventSink: (RoomListEvents) -> Unit = {} -) = RoomListState( - matrixUser = matrixUser, - showAvatarIndicator = showAvatarIndicator, - hasNetworkConnection = hasNetworkConnection, - snackbarMessage = snackbarMessage, - contextMenu = contextMenu, - leaveRoomState = leaveRoomState, - filtersState = filtersState, - searchState = searchState, - contentState = contentState, - acceptDeclineInviteState = acceptDeclineInviteState, - directLogoutState = directLogoutState, - eventSink = eventSink, -) - -internal fun aRoomListRoomSummaryList(): ImmutableList { - return persistentListOf( - aRoomListRoomSummary( - name = "Room Invited", - avatarData = AvatarData("!roomId", "Room with Alice and Bob", size = AvatarSize.RoomListItem), - id = "!roomId:domain", - inviteSender = anInviteSender(), - displayType = RoomSummaryDisplayType.INVITE, - ), - aRoomListRoomSummary( - name = "Room", - numberOfUnreadMessages = 1, - timestamp = "14:18", - lastMessage = "A very very very very long message which suites on two lines", - avatarData = AvatarData("!id", "R", size = AvatarSize.RoomListItem), - id = "!roomId:domain", - ), - aRoomListRoomSummary( - name = "Room#2", - numberOfUnreadMessages = 0, - timestamp = "14:16", - lastMessage = "A short message", - avatarData = AvatarData("!id", "Z", size = AvatarSize.RoomListItem), - id = "!roomId2:domain", - ), - aRoomListRoomSummary( - id = "!roomId3:domain", - displayType = RoomSummaryDisplayType.PLACEHOLDER, - ), - aRoomListRoomSummary( - id = "!roomId4:domain", - displayType = RoomSummaryDisplayType.PLACEHOLDER, - ), - ) -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt deleted file mode 100644 index d40546b5dd1..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.roomlist.impl - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.leaveroom.api.LeaveRoomView -import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer -import io.element.android.features.roomlist.impl.components.RoomListContentView -import io.element.android.features.roomlist.impl.components.RoomListMenuAction -import io.element.android.features.roomlist.impl.components.RoomListTopBar -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.features.roomlist.impl.search.RoomListSearchView -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.FloatingActionButton -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Scaffold -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.RoomId - -@Composable -fun RoomListView( - state: RoomListState, - onRoomClick: (RoomId) -> Unit, - onSettingsClick: () -> Unit, - onSetUpRecoveryClick: () -> Unit, - onConfirmRecoveryKeyClick: () -> Unit, - onCreateRoomClick: () -> Unit, - onRoomSettingsClick: (roomId: RoomId) -> Unit, - onMenuActionClick: (RoomListMenuAction) -> Unit, - onRoomDirectorySearchClick: () -> Unit, - onMigrateToNativeSlidingSyncClick: () -> Unit, - modifier: Modifier = Modifier, - acceptDeclineInviteView: @Composable () -> Unit, -) { - ConnectivityIndicatorContainer( - modifier = modifier, - isOnline = state.hasNetworkConnection, - ) { topPadding -> - Box { - if (state.contextMenu is RoomListState.ContextMenu.Shown) { - RoomListContextMenu( - contextMenu = state.contextMenu, - eventSink = state.eventSink, - onRoomSettingsClick = onRoomSettingsClick, - ) - } - - LeaveRoomView(state = state.leaveRoomState) - - RoomListScaffold( - state = state, - onSetUpRecoveryClick = onSetUpRecoveryClick, - onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, - onRoomClick = onRoomClick, - onOpenSettings = onSettingsClick, - onCreateRoomClick = onCreateRoomClick, - onMenuActionClick = onMenuActionClick, - onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, - modifier = Modifier.padding(top = topPadding), - ) - // This overlaid view will only be visible when state.displaySearchResults is true - RoomListSearchView( - state = state.searchState, - eventSink = state.eventSink, - onRoomClick = onRoomClick, - onRoomDirectorySearchClick = onRoomDirectorySearchClick, - modifier = Modifier - .statusBarsPadding() - .padding(top = topPadding) - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - ) - acceptDeclineInviteView() - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun RoomListScaffold( - state: RoomListState, - onSetUpRecoveryClick: () -> Unit, - onConfirmRecoveryKeyClick: () -> Unit, - onRoomClick: (RoomId) -> Unit, - onOpenSettings: () -> Unit, - onCreateRoomClick: () -> Unit, - onMenuActionClick: (RoomListMenuAction) -> Unit, - onMigrateToNativeSlidingSyncClick: () -> Unit, - modifier: Modifier = Modifier, -) { - fun onRoomClick(room: RoomListRoomSummary) { - onRoomClick(room.roomId) - } - - val appBarState = rememberTopAppBarState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState) - val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) - - Scaffold( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - RoomListTopBar( - matrixUser = state.matrixUser, - showAvatarIndicator = state.showAvatarIndicator, - areSearchResultsDisplayed = state.searchState.isSearchActive, - onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) }, - onMenuActionClick = onMenuActionClick, - onOpenSettings = onOpenSettings, - scrollBehavior = scrollBehavior, - displayMenuItems = state.displayActions, - displayFilters = state.displayFilters, - filtersState = state.filtersState, - ) - }, - content = { padding -> - RoomListContentView( - contentState = state.contentState, - filtersState = state.filtersState, - eventSink = state.eventSink, - onSetUpRecoveryClick = onSetUpRecoveryClick, - onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, - onRoomClick = ::onRoomClick, - onCreateRoomClick = onCreateRoomClick, - onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - ) - }, - floatingActionButton = { - if (state.displayActions) { - FloatingActionButton( - // FIXME align on Design system theme - containerColor = MaterialTheme.colorScheme.primary, - onClick = onCreateRoomClick - ) { - Icon( - // Note cannot use Icons.Outlined.EditSquare, it does not exist :/ - imageVector = CompoundIcons.Compose(), - contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message) - ) - } - } - }, - snackbarHost = { SnackbarHost(snackbarHostState) }, - ) -} - -internal fun RoomListRoomSummary.contentType() = displayType.ordinal - -@PreviewsDayNight -@Composable -internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class) state: RoomListState) = ElementPreview { - RoomListView( - state = state, - onRoomClick = {}, - onSettingsClick = {}, - onSetUpRecoveryClick = {}, - onConfirmRecoveryKeyClick = {}, - onCreateRoomClick = {}, - onRoomSettingsClick = {}, - onMenuActionClick = {}, - onRoomDirectorySearchClick = {}, - acceptDeclineInviteView = {}, - onMigrateToNativeSlidingSyncClick = {}, - ) -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt deleted file mode 100644 index 132f0a0dc94..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomlist.impl.components - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import io.element.android.features.roomlist.impl.R -import io.element.android.libraries.designsystem.components.Announcement -import io.element.android.libraries.designsystem.components.AnnouncementType -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight - -@Composable -internal fun NativeSlidingSyncMigrationBanner( - onContinueClick: () -> Unit, - onDismissClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Announcement( - modifier = modifier.roomListBannerPadding(), - title = stringResource(R.string.banner_migrate_to_native_sliding_sync_title), - description = stringResource(R.string.banner_migrate_to_native_sliding_sync_description), - type = AnnouncementType.Actionable( - actionText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action), - onActionClick = onContinueClick, - onDismissClick = onDismissClick, - ) - ) -} - -@PreviewsDayNight -@Composable -internal fun NativeSlidingSyncMigrationBannerPreview() = ElementPreview { - NativeSlidingSyncMigrationBanner( - onContinueClick = {}, - onDismissClick = {}, - ) -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt deleted file mode 100644 index a9c2f1c80b5..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomlist.impl.di - -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module -import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter -import io.element.android.features.roomlist.impl.filters.RoomListFiltersState -import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter -import io.element.android.features.roomlist.impl.search.RoomListSearchState -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.SessionScope - -@ContributesTo(SessionScope::class) -@Module -interface RoomListModule { - @Binds - fun bindSearchPresenter(presenter: RoomListSearchPresenter): Presenter - - @Binds - fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionState.kt deleted file mode 100644 index 62ed9f08a33..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionState.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomlist.impl.filters.selection - -import io.element.android.features.roomlist.impl.filters.RoomListFilter - -data class FilterSelectionState( - val filter: RoomListFilter, - val isSelected: Boolean, -) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt deleted file mode 100644 index 85ce456e47d..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomlist.impl.search - -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import kotlinx.collections.immutable.ImmutableList - -data class RoomListSearchState( - val isSearchActive: Boolean, - val query: String, - val results: ImmutableList, - val isRoomDirectorySearchEnabled: Boolean, - val eventSink: (RoomListSearchEvents) -> Unit -) { - val displayRoomDirectorySearch = query.isEmpty() && isRoomDirectorySearchEnabled -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt deleted file mode 100644 index 9136f5867cf..00000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomlist.impl.search - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.roomlist.impl.aRoomListRoomSummaryList -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -class RoomListSearchStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aRoomListSearchState(), - aRoomListSearchState(isRoomDirectorySearchEnabled = true), - aRoomListSearchState( - isSearchActive = true, - query = "Test", - results = aRoomListRoomSummaryList() - ), - ) -} - -fun aRoomListSearchState( - isSearchActive: Boolean = false, - query: String = "", - results: ImmutableList = persistentListOf(), - isRoomDirectorySearchEnabled: Boolean = false, - eventSink: (RoomListSearchEvents) -> Unit = { }, -) = RoomListSearchState( - isSearchActive = isSearchActive, - query = query, - results = results, - isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, - eventSink = eventSink, -) diff --git a/features/roomlist/impl/src/main/res/values-be/translations.xml b/features/roomlist/impl/src/main/res/values-be/translations.xml deleted file mode 100644 index a6201922ffb..00000000000 --- a/features/roomlist/impl/src/main/res/values-be/translations.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - "Выйсці і абнавіць" - "Ваш сервер зараз падтрымлівае новы, хутчэйшы пратакол. Выйдзіце з сістэмы і зноў увайдзіце, каб абнавіць яе. Гэта дапаможа вам пазбегнуць прымусовага выхаду з сістэмы, калі стары пратакол будзе пазней выдалены." - "Ваш хатні сервер больш не падтрымлівае стары пратакол. Калі ласка, выйдзіце і ўвайдзіце зноў, каб працягнуць выкарыстанне праграмы." - "Даступна абнаўленне" - "Стварыце новы ключ аднаўлення, які можна выкарыстоўваць для аднаўлення зашыфраванай гісторыі паведамленняў у выпадку страты доступу да вашых прылад." - "Наладзьце аднаўленне" - "Наладзіць аднаўленне" - "Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата." - "Увядзіце ключ аднаўлення" - "Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны." - "Палепшыце якасць званкоў" - "Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?" - "Адхіліць запрашэнне" - "Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?" - "Адхіліць чат" - "Няма запрашэнняў" - "%1$s (%2$s) запрасіў(-ла) вас" - "Гэта аднаразовы працэс, дзякуем за чаканне." - "Налада ўліковага запісу." - "Стварыце новую размову або пакой" - "Пачніце з паведамлення каму-небудзь." - "Пакуль няма чатаў." - "Абранае" - "Дадаць чат у абранае можна ў наладах чата. -На дадзены момант вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты." - "У вас пакуль няма абраных чатаў" - "Запрашэнні" - "У вас няма непрынятых запрашэнняў." - "Нізкі прыярытэт" - "Вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты." - "У вас няма чатаў для гэтай катэгорыі" - "Людзі" - "У вас пакуль няма асабістых паведамленняў" - "Пакоі" - "Вас пакуль няма ў ніводным пакоі" - "Непрачытаныя" - "Віншуем! -У вас няма непрачытаных паведамленняў!" - "Усе чаты" - "Пазначыць як прачытанае" - "Пазначыць як непрачытанае" - "Праглядзець усе пакоі" - "Здаецца, вы карыстаецеся новай прыладай. Праверце з дапамогай іншай прылады, каб атрымаць доступ да зашыфраваных паведамленняў." - "Пацвердзіце, што гэта вы" - diff --git a/features/roomlist/impl/src/main/res/values-bg/translations.xml b/features/roomlist/impl/src/main/res/values-bg/translations.xml deleted file mode 100644 index 8012e4283b2..00000000000 --- a/features/roomlist/impl/src/main/res/values-bg/translations.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - "Резервното копие на чатовете ви в момента не е синхронизирано. Въведете ключа си за възстановяване, за да потвърдите достъпа до резервното копие на чатовете си." - "Потвърдете ключа си за възстановяване" - "Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?" - "Отказване на покана" - "Няма покани" - "%1$s (%2$s) ви покани" - "Създаване на нов разговор или стая" - "Започнете, като изпратите съобщение на някого." - "Все още няма чатове." - "Нисък приоритет" - "Хора" - "Стаи" - "Всички чатове" - "Отбелязване като прочетено" - "Отбелязване като непрочетено" - "Изглежда, че използвате ново устройство. Потвърдете с друго устройство за достъп до вашите шифровани съобщения." - "Потвърдете, че сте вие" - diff --git a/features/roomlist/impl/src/main/res/values-cs/translations.xml b/features/roomlist/impl/src/main/res/values-cs/translations.xml deleted file mode 100644 index 921bb77027a..00000000000 --- a/features/roomlist/impl/src/main/res/values-cs/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Odhlásit se a upgradovat" - "Váš server nyní podporuje nový, rychlejší protokol. Chcete-li upgradovat, odhlaste se a znovu se přihlaste. Pokud to uděláte nyní, pomůže vám vyhnout se nucenému odhlášení, když bude starý protokol později odstraněn." - "Váš domovský server již nepodporuje starý protokol. Chcete-li pokračovat v používání aplikace, odhlaste se a znovu se přihlaste." - "Upgrade k dispozici" - "Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením." - "Nastavení obnovy" - "Nastavení obnovy" - "Potvrďte klíč pro obnovení, abyste zachovali přístup k úložišti klíčů a historii zpráv." - "Zadejte klíč pro obnovení" - "Zapomněli jste klíč pro obnovení?" - "Vaše úložiště klíčů není synchronizováno" - "Abyste nikdy nezmeškali důležitý hovor, změňte nastavení tak, abyste povolili oznámení na celé obrazovce, když je telefon uzamčen." - "Vylepšete si zážitek z volání" - "Opravdu chcete odmítnout pozvánku do %1$s?" - "Odmítnout pozvání" - "Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?" - "Odmítnout chat" - "Žádné pozvánky" - "%1$s (%2$s) vás pozval(a)" - "Žádost o vstup odeslána" - "Jedná se o jednorázový proces, prosíme o strpení." - "Nastavení vašeho účtu" - "Vytvořte novou konverzaci nebo místnost" - "Začněte tím, že někomu pošnete zprávu." - "Zatím žádné konverzace." - "Oblíbené" - "V nastavení chatu můžete přidat chat k oblíbeným. -Prozatím můžete zrušit výběr filtrů, abyste viděli své další chaty" - "Zatím nemáte oblíbené chaty" - "Pozvánky" - "Nemáte žádné nevyřízené pozvánky." - "Nízká priorita" - "Můžete zrušit výběr filtrů, abyste viděli své další chaty" - "Nemáte chaty pro tento výběr" - "Lidé" - "Zatím nemáte žádné přímé zprávy" - "Místnosti" - "Ještě nejste v žádné místnosti" - "Nepřečtené" - "Gratulujeme! -Nemáte žádné nepřečtené zprávy!" - "Všechny chaty" - "Označit jako přečtené" - "Označit jako nepřečtené" - "Procházet všechny místnosti" - "Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám." - "Ověřte, že jste to vy" - diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml deleted file mode 100644 index 3b8b70c40f2..00000000000 --- a/features/roomlist/impl/src/main/res/values-de/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Abmelden und aktualisieren" - "Dein Server unterstützt jetzt ein neues, schnelleres Protokoll. Melde dich ab und melde dich wieder an, um zu aktualisieren. Wenn du das jetzt tust, vermeidest du eine erzwungene Abmeldung, wenn das alte Protokoll später entfernt wird." - "Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen." - "Aktualisierung verfügbar" - "Erstelle einen neuen Wiederherstellungsschlüssel, mit dem du deinen verschlüsselten Nachrichtenverlauf wiederherstellen kannst, wenn du dich an einem neuen Gerät anmeldest." - "Wiederherstellung einrichten" - "Wiederherstellung einrichten" - "Bestätigen Sie die Validität Ihres Wiederherstellungsschlüssels, um weiterhin auf Ihren Schlüsselspeicher und den Nachrichtenverlauf zugreifen zu können." - "Geben Sie Ihren Wiederherstellungsschlüssel ein" - "Haben Sie Ihren Wiederherstellungsschlüssel vergessen?" - "Ihr Schlüsselspeicher ist nicht synchronisiert" - "Damit du keinen wichtigen Anruf verpasst, ändere bitte deine Einstellungen so, dass du bei gesperrtem Telefon Benachrichtigungen im Vollbildmodus erhältst." - "Verbessere dein Anruferlebnis" - "Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?" - "Einladung ablehnen" - "Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?" - "Einladung ablehnen" - "Keine Einladungen" - "%1$s (%2$s) hat dich eingeladen" - "Beitrittsanfrage geschickt" - "Dies ist ein einmaliger Vorgang, danke fürs Warten." - "Dein Konto wird eingerichtet." - "Eine Unterthaltung oder Raum erstellen" - "Beginne, indem du jemandem eine Nachricht sendest." - "Noch keine Chats." - "Favoriten" - "In den Chat-Einstellungen kannst du einen Chat als Favorit hinzufügen. -Um deine anderen Chats zu sehen wähle diesen Filter ab." - "Du hast noch keine Chats als Favorit markiert." - "Einladungen" - "Du hast keine ausstehenden Einladungen." - "Niedrige Priorität" - "Wähle Filter ab, um Deine Chats zu sehen." - "Du hast keine Chats für diese Auswahl" - "Personen" - "Du hast noch keine Direktnachrichten" - "Räume" - "Du hast noch keine Chats." - "Ungelesen" - "Glückwunsch! -Du hast keine ungelesenen Nachrichten!" - "Chats" - "Als gelesen markieren" - "Als ungelesen markieren" - "Alle Räume durchsuchen" - "Es sieht aus, als würdest du ein neues Gerät verwenden. Verifiziere es mit einem anderen Gerät, damit du auf deine verschlüsselten Nachrichten zugreifen kannst." - "Bestätige deine Identität" - diff --git a/features/roomlist/impl/src/main/res/values-el/translations.xml b/features/roomlist/impl/src/main/res/values-el/translations.xml deleted file mode 100644 index c1ac4c59451..00000000000 --- a/features/roomlist/impl/src/main/res/values-el/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Αποσύνδεση & Αναβάθμιση" - "Ο διακομιστής σου υποστηρίζει τώρα ένα νέο, ταχύτερο πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για αναβάθμιση τώρα. Κάνοντας αυτό τώρα θα σε βοηθήσει να αποφύγεις μια αναγκαστική αποσύνδεση όταν το παλιό πρωτόκολλο καταργηθεί αργότερα." - "Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή." - "Διαθέσιμη αναβάθμιση" - "Δημιούργησε ένα νέο κλειδί ανάκτησης που μπορεί να χρησιμοποιηθεί για την επαναφορά του ιστορικού των κρυπτογραφημένων μηνυμάτων σου σε περίπτωση που χάσεις την πρόσβαση στις συσκευές σου." - "Ρύθμιση ανάκτησης" - "Ρύθμιση ανάκτησης" - "Επιβεβαίωσε το κλειδί ανάκτησης για να διατηρήσεις την πρόσβαση στο χώρο αποθήκευσης κλειδιών και στο ιστορικό μηνυμάτων." - "Εισήγαγε το κλειδί ανάκτησης" - "Ξέχασες το κλειδί ανάκτησης;" - "Ο χώρος αποθήκευσης κλειδιών σου δεν είναι συγχρονισμένος" - "Για να διασφαλίσεις ότι δεν θα χάσεις ποτέ μια σημαντική κλήση, άλλαξε τις ρυθμίσεις σου για να επιτρέψεις τις ειδοποιήσεις πλήρους οθόνης όταν το τηλέφωνό σου είναι κλειδωμένο." - "Βελτίωσε την εμπειρία κλήσεων" - "Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;" - "Απόρριψη πρόσκλησης" - "Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;" - "Απόρριψη συνομιλίας" - "Χωρίς προσκλήσεις" - "%1$s (%2$s) σέ προσκάλεσε" - "Το αίτημα συμμετοχής στάλθηκε" - "Αυτή είναι μια εφάπαξ διαδικασία, ευχαριστώ που περίμενες." - "Ρύθμιση του λογαριασμού σου." - "Δημιουργία νέας συνομιλίας ή δωματίου" - "Ξεκίνησε στέλνοντας μηνύματα σε κάποιον." - "Δεν υπάρχουν συνομιλίες ακόμα." - "Αγαπημένα" - "Μπορείς να προσθέσεις μια συνομιλία στα αγαπημένα σου στις ρυθμίσεις συνομιλίας. -Προς το παρόν, μπορείς να καταργήσεις την επιλογή φίλτρων για να δεις τις άλλες συνομιλίες σου" - "Δεν έχεις ακόμα αγαπημένες συνομιλίες" - "Προσκλήσεις" - "Δεν έχεις εκκρεμείς προσκλήσεις." - "Χαμηλής Προτεραιότητας" - "Μπορείς να καταργήσεις την επιλογή φίλτρων για να δεις τις άλλες συνομιλίες σου" - "Δεν έχεις συνομιλίες για αυτήν την επιλογή" - "Άτομα" - "Δεν έχεις ακόμα ΠΜ" - "Δωμάτια" - "Δεν είσαι ακόμα σε κανένα δωμάτιο" - "Μη αναγνωσμένα" - "Συγχαρητήρια! -Δεν έχεις μη αναγνωσμένα μηνύματα!" - "Συνομιλίες" - "Επισήμανση ως αναγνωσμένου" - "Επισήμανση ως μη αναγνωσμένου" - "Περιήγηση σε όλα τα δωμάτια" - "Φαίνεται ότι χρησιμοποιείς μια νέα συσκευή. Επαλήθευσε με άλλη συσκευή για πρόσβαση στα κρυπτογραφημένα σου μηνύματα." - "Επαλήθευσε ότι είσαι εσύ" - diff --git a/features/roomlist/impl/src/main/res/values-es/translations.xml b/features/roomlist/impl/src/main/res/values-es/translations.xml deleted file mode 100644 index 25f8e0ba212..00000000000 --- a/features/roomlist/impl/src/main/res/values-es/translations.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - "Configurar la recuperación" - "La copia de seguridad del chat no está sincronizada en este momento. Debes confirmar tu clave de recuperación para mantener el acceso a la copia de seguridad del chat." - "Confirma tu clave de recuperación" - "¿Estás seguro de que quieres rechazar la invitación a unirte a %1$s?" - "Rechazar la invitación" - "¿Estás seguro de que quieres rechazar este chat privado con%1$s?" - "Rechazar el chat" - "Sin invitaciones" - "%1$s (%2$s) te invitó" - "Este proceso solo se hace una vez, gracias por esperar." - "Configura tu cuenta" - "Crear una nueva conversación o sala" - "Empieza enviando un mensaje a alguien." - "Aún no hay chats." - "Favoritos" - "Puedes añadir un chat a tus favoritos en la configuración del chat. -Por ahora, puedes deseleccionar los filtros para ver tus otros chats" - "Aún no tienes chats favoritos" - "Prioridad baja" - "Puedes deseleccionar filtros para ver tus otros chats." - "No tienes chats para esta selección" - "Personas" - "Todavía no tienes ningún mensaje directo" - "Salas" - "Todavía no estás en ninguna sala" - "No leídos" - "¡Felicidades! -¡No tienes ningún mensaje sin leer!" - "Todos los chats" - "Marcar como leído" - "Marcar como no leído" - "Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados." - "Verifica que eres tú" - diff --git a/features/roomlist/impl/src/main/res/values-et/translations.xml b/features/roomlist/impl/src/main/res/values-et/translations.xml deleted file mode 100644 index 2eb1f638328..00000000000 --- a/features/roomlist/impl/src/main/res/values-et/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Logi välja ja uuenda" - "Sinu koduserver toetab uut ja kiiremat protokolli. Uuendamiseks logi korraks rakendusest välja ja siis tagasi. Mingil hetkel tulevikus vana protokoll eemaldatakse kasutusest ja tehes uuenduse nüüd väldid hilisemat sundkorras uuendust." - "Sinu koduserver enam ei toeta vana protokolli. Jätkamaks rakenduse kasutamist palun logi välja ning seejärel tagasi." - "Saadaval on uuendus" - "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?" - "Sinu võtmehoidla pole sünkroonis" - "Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused." - "Sinu tõhusad telefonikõned" - "Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?" - "Lükka kutse tagasi" - "Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?" - "Keeldu vestlusest" - "Kutseid pole" - "%1$s (%2$s) saatis sulle kutse" - "Liitumispalve on saadetud" - "Tänud, et ootad - seda toimingut on vaja teha vaid üks kord." - "Seadistame sinu kasutajakontot." - "Loo uus vestlus või jututuba" - "Alustamiseks saada kellelegi sõnum." - "Veel pole vestlusi." - "Lemmikud" - "Vestluse seadistusest saad ta määrata lemmikuks. -Aga seni… oma teiste vestluste nägemiseks pead eemaldama filtrid" - "Sul veel pole lemmikvestlusi" - "Kutsed" - "Sul pole ootel kutseid." - "Vähetähtis" - "Oma teiste vestluste nägemiseks sa pead filtrid eemaldama" - "Selle valiku jaoks sul veel pole vestlusi" - "Inimesed" - "Sul pole veel otsevestlusi" - "Jututoad" - "Sa veel ei osale mitte üheski jututoas" - "Lugemata" - "Õnnitleme! -Sul pole ühtegi lugemata sõnumit!" - "Vestlused" - "Märgi loetuks" - "Märgi mitteloetuks" - "Sirvi kõiki jututube" - "Tundub, et kasutad uut seadet. Oma krüptitud sõnumite lugemiseks verifitseeri ta mõne muu oma seadmega." - "Verifitseeri, et see oled sina" - diff --git a/features/roomlist/impl/src/main/res/values-fa/translations.xml b/features/roomlist/impl/src/main/res/values-fa/translations.xml deleted file mode 100644 index f99eb289a66..00000000000 --- a/features/roomlist/impl/src/main/res/values-fa/translations.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - "خروج و ارتقا" - "ارتقا موجود است" - "برپایی بازیابی" - "برپایی بازیابی" - "ورود کلید بازیابیتان" - "بهبود تجریهٔ تماستان" - "مطمئنید که می‌خواهید دعوت پیوستن به %1$s را رد کنید؟" - "رد دعوت" - "مطمئنید که می‌خواهید این گپ خصوصی با %1$s را رد کنید؟" - "رد گپ" - "بدون دعوت" - "%1$s (%2$s) دعوتتان کرد" - "فرایندی یک باره است. ممنون از شکیباییتان." - "برپایی حسابتان." - "ایجاد اتاق یا گفت‌وگویی جدید" - "آغاز با پیام دادن به کسی." - "هنوز گپی وجود ندارد." - "علاقه‌مندی‌ها" - "هنوز هیچ گپ مورد علاقه‌ای ندارید" - "دعوت‌ها" - "هیچ دعوت منتظری ندارید." - "اولویت کم" - "می توانید پالایه‌ها را برای دیدن دیگر گپ‌هایتان بردارید" - "هیچ گپی برای این گزینش ندارید" - "افراد" - "هنوز هیچ پیام مستقیمی ندارید" - "اتاق‌ها" - "هنوز در هیچ اتاقی نیستید" - "نخوانده‌ها" - "تبریک! -هیچ پیام نخوانده‌ای ندارید!" - "گپ‌ها" - "علامت‌گذاری به عنوان خوانده شده" - "نشان به ناخوانده" - "مرور همهٔ اتاق‌ها" - "تأیید کنید که خودتانید" - diff --git a/features/roomlist/impl/src/main/res/values-fi/translations.xml b/features/roomlist/impl/src/main/res/values-fi/translations.xml deleted file mode 100644 index 5927a1bc572..00000000000 --- a/features/roomlist/impl/src/main/res/values-fi/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Kirjaudu Ulos & Päivitä" - "Palvelimesi tukee nyt uutta, nopeampaa protokollaa. Kirjaudu ulos ja takaisin sisään päivittääksesi nyt. Jos teet tämän nyt, voit välttää pakotetun uloskirjautumisen, kun vanha protokolla poistetaan myöhemmin." - "Kotipalvelimesi ei enää tue vanhaa protokollaa. Kirjaudu ulos ja takaisin sisään jatkaaksesi sovelluksen käyttöä." - "Päivitys saatavilla" - "Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, mikäli menetät pääsyn kaikkiin laitteisiisi." - "Ota palautus käyttöön" - "Ota palautus käyttöön tilisi suojaamiseksi" - "Vahvista palautusavaimesi, jotta pääset edelleen käyttämään avainten säilytystä ja viestihistoriaa." - "Syötä palautusavaimesi" - "Unohditko palautusavaimesi?" - "Avainten säilytys ei ole synkronoitu" - "Salli koko näytön ilmoitukset, kun laite on lukittu, jos et halua koskaan missata tärkeää puhelua." - "Paranna puhelukokemustasi" - "Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?" - "Hylkää kutsu" - "Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?" - "Hylkää keskustelu" - "Ei kutsuja" - "%1$s (%2$s) kutsui sinut" - "Liittymispyyntö lähetetty" - "Tämä on kertaluonteinen prosessi, kiitos odottamisesta." - "Tiliä määritetään." - "Luo uusi keskustelu tai huone" - "Aloita lähettämällä viesti jollekin." - "Sinulla ei ole vielä keskusteluja." - "Suosikit" - "Voit lisätä keskustelun suosikkeihisi keskustelun asetuksissa. -Toistaiseksi voit poistaa suodattimien valinnan, jotta näet muut keskustelut." - "Sinulla ei ole vielä suosikkikeskusteluja" - "Kutsut" - "Sinulla ei ole yhtään odottavaa kutsua." - "Matala prioriteetti" - "Voit poistaa suodattimien valinnan nähdäksesi muut keskustelusi." - "Sinulla ei ole sopivia keskusteluja tähän valintaan" - "Ihmiset" - "Sinulla ei ole vielä yhtään yksityisviestiä" - "Huoneet" - "Et ole vielä missään huoneessa" - "Lukemattomat" - "Onnittelut! -Sinulla ei ole lukemattomia viestejä!" - "Keskustelut" - "Merkitse luetuksi" - "Merkitse lukemattomaksi" - "Selaa kaikkia huoneita" - "Vaikuttaisi siltä, että käytät uutta laitetta. Vahvista toisella laitteella nähdäksesi salatut viestit." - "Vahvista, että se olet sinä" - diff --git a/features/roomlist/impl/src/main/res/values-fr/translations.xml b/features/roomlist/impl/src/main/res/values-fr/translations.xml deleted file mode 100644 index 04fb371eebc..00000000000 --- a/features/roomlist/impl/src/main/res/values-fr/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Déconnecter et mettre à niveau" - "Votre serveur prend désormais en charge un nouveau protocole plus rapide. Déconnectez-vous, puis reconnectez-vous pour effectuer la mise à niveau dès maintenant. En le faisant tout de suite, vous éviterez une déconnexion forcée lorsque l’ancien protocole sera supprimé." - "Votre serveur d’accueil ne prend plus en charge l’ancien protocole. Veuillez vous déconnecter puis vous reconnecter pour continuer à utiliser l’application." - "Mise à niveau disponible" - "Générez une nouvelle clé de récupération qui peut être utilisée pour restaurer l’historique de vos messages chiffrés au cas où vous perdriez l’accès à vos appareils." - "Configurer la sauvegarde" - "Configurer la récupération" - "Confirmez votre clé de récupération pour conserver l’accès à votre stockage de clés et à l’historique des messages." - "Saisissez votre clé de récupération" - "Clé de récupération oubliée ?" - "Le stockage de vos clés n’est pas synchronisé" - "Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé." - "Améliorez votre expérience d’appel" - "Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?" - "Refuser l’invitation" - "Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?" - "Refuser l’invitation" - "Aucune invitation" - "%1$s (%2$s) vous a invité(e)" - "Demande de rejoindre le salon envoyée" - "Il s’agit d’une opération ponctuelle, merci d’attendre quelques instants." - "Configuration de votre compte." - "Créer une nouvelle discussion ou un nouveau salon" - "Commencez par envoyer un message à quelqu’un." - "Aucune discussion pour le moment." - "Favoris" - "Vous pouvez ajouter une discussion aux favoris depuis les paramètres de la discussion. -En attendant, vous pouvez désélectionner des filtres pour voir vos autres salons." - "Vous n’avez pas encore de discussions favorites" - "Invitations" - "Vous n’avez aucune invitation en attente." - "Priorité basse" - "Veuillez désélectionner des filtres pour voir vos discussions" - "Vous n’avez pas de discussions pour cette sélection" - "Personnes" - "Vous n’avez pas encore de discussions" - "Salons" - "Vous n’êtes membre d’aucun salon" - "Non-lus" - "Félicitations ! -Vous n’avez plus de messages non-lus !" - "Conversations" - "Marquer comme lu" - "Marquer comme non lu" - "Parcourir tous les salons" - "Il semblerait que vous utilisiez un nouvel appareil. Vérifiez la session avec un autre de vos appareils pour accéder à vos messages chiffrés." - "Vérifier que c’est bien vous" - diff --git a/features/roomlist/impl/src/main/res/values-hu/translations.xml b/features/roomlist/impl/src/main/res/values-hu/translations.xml deleted file mode 100644 index 1505f133671..00000000000 --- a/features/roomlist/impl/src/main/res/values-hu/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Kijelentkezés és frissítés" - "A kiszolgálója mostantól egy új, gyorsabb protokollt támogat. A frissítéshez jelentkezzen ki, majd jelentkezzen be újra. Ha ezt most megteszi, elkerülheti a kényszerített kijelentkeztetést a régi protokollt eltávolításakor." - "A Matrix-kiszolgáló már nem támogatja a régi protokollt. Az alkalmazás további használatához jelentkezzen ki és be." - "Frissítés érhető el" - "Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést." - "Helyreállítás beállítása" - "Helyreállítás beállítása" - "Erősítse meg a helyreállítási kulcsát, hogy továbbra is hozzáférjen a kulcstárolójához és az üzenetelőzményekhez." - "Adja meg a helyreállítási kulcsot" - "Elfelejtette a helyreállítási kulcsot?" - "A kulcstároló nincs szinkronizálva" - "Hogy sose maradjon le egyetlen fontos hívásról sem, a beállításokban engedélyezze a teljes képernyős értesítéseket, amikor a telefon zárolva van." - "Fokozza a hívásélményét" - "Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?" - "Meghívás elutasítása" - "Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?" - "Csevegés elutasítása" - "Nincsenek meghívások" - "%1$s (%2$s) meghívta" - "Csatlakozási kérés elküldve" - "Ez egy egyszeri folyamat, köszönjük a türelmét." - "A fiók beállítása." - "Új beszélgetés vagy szoba létrehozása" - "Kezdje azzal, hogy üzenetet küld valakinek." - "Még nincsenek csevegések." - "Kedvencek" - "A csevegési beállításokban csevegéseket adhat hozzá a kedvencekhez. -Egyelőre törölheti a szűrőket a többi csevegés megtekintéséhez." - "Még nincsenek kedvenc csevegései" - "Meghívások" - "Nincsenek függőben lévő meghívásai." - "Alacsony prioritás" - "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez" - "Ehhez a kiválasztáshoz nem tartoznak csevegések" - "Emberek" - "Még nincsenek privát üzenetei" - "Szobák" - "Még nincs egy szobában sem" - "Olvasatlan" - "Gratulálunk! -Nincs olvasatlan üzenete!" - "Összes csevegés" - "Megjelölés olvasottként" - "Megjelölés olvasatlanként" - "Összes szoba böngészése" - "Úgy tűnik, hogy új eszközt használ. Ellenőrizze egy másik eszközzel, hogy a továbbiakban elérje a titkosított üzeneteket." - "Ellenőrizze, hogy Ön az" - diff --git a/features/roomlist/impl/src/main/res/values-in/translations.xml b/features/roomlist/impl/src/main/res/values-in/translations.xml deleted file mode 100644 index e3c3bb4800f..00000000000 --- a/features/roomlist/impl/src/main/res/values-in/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Keluar & Tingkatkan" - "Server Anda kini mendukung protokol baru yang lebih cepat. Keluar dan masuk lagi untuk memperbarui sekarang. Melakukan hal ini sekarang akan membantu Anda menghindari keluar paksa saat protokol lama dihapus nantinya." - "Homeserver Anda tidak lagi mendukung protokol lama. Silakan keluar dan masuk kembali untuk terus menggunakan aplikasi." - "Peningkatan tersedia" - "Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda." - "Siapkan pemulihan" - "Siapkan pemulihan" - "Konfirmasikan kunci pemulihan Anda untuk mempertahankan akses ke penyimpanan kunci dan riwayat pesan Anda." - "Masukkan kunci pemulihan Anda" - "Lupa kunci pemulihan Anda?" - "Penyimpanan kunci Anda tidak sinkron" - "Untuk memastikan Anda tidak melewatkan panggilan penting, silakan ubah pengaturan Anda untuk memperbolehkan notifikasi layar penuh ketika ponsel Anda terkunci." - "Tingkatkan pengalaman panggilan Anda" - "Apakah Anda yakin ingin menolak undangan untuk bergabung ke %1$s?" - "Tolak undangan" - "Apakah Anda yakin ingin menolak obrolan pribadi dengan %1$s?" - "Tolak obrolan" - "Tidak ada undangan" - "%1$s (%2$s) mengundang Anda" - "Permintaan untuk bergabung dikirim" - "Ini adalah proses satu kali, terima kasih telah menunggu." - "Menyiapkan akun Anda." - "Buat percakapan atau ruangan baru" - "Mulailah dengan mengirim pesan kepada seseorang." - "Belum ada obrolan." - "Favorit" - "Anda dapat menambahkan percakapan ke favorit Anda dalam pengaturan percakapan. -Untuk sementara, Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain" - "Anda belum memiliki percakapan favorit" - "Undangan" - "Anda tidak memiliki undangan yang tertunda." - "Prioritas Rendah" - "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain" - "Anda tidak memiliki percakapan untuk pemilihan ini" - "Orang" - "Anda belum memiliki percakapan langsung" - "Ruangan" - "Anda belum berada dalam ruangan" - "Belum dibaca" - "Selamat! -Anda tidak memiliki pesan yang belum dibaca!" - "Semua Obrolan" - "Tandai sebagai dibaca" - "Tandai sebagai belum dibaca" - "Cari semua ruangan" - "Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya." - "Verifikasi bahwa ini Anda" - diff --git a/features/roomlist/impl/src/main/res/values-it/translations.xml b/features/roomlist/impl/src/main/res/values-it/translations.xml deleted file mode 100644 index cd248d73722..00000000000 --- a/features/roomlist/impl/src/main/res/values-it/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Esci e aggiorna" - "Il tuo server ora supporta un nuovo protocollo più veloce. Esci e rientra per effettuare l\'aggiornamento. Se lo fai ora, eviterai una disconnessione forzata quando il vecchio protocollo verrà rimosso in seguito." - "Il tuo homeserver non supporta più il vecchio protocollo. Esci e rientra per continuare a usare l\'app." - "Aggiornamento disponibile" - "Genera una nuova chiave di recupero che può essere usata per ripristinare la cronologia dei messaggi crittografati nel caso in cui tu perda l\'accesso ai tuoi dispositivi." - "Configura il recupero" - "Configura il ripristino" - "Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi." - "Inserisci la tua chiave di recupero" - "Hai dimenticato la chiave di recupero?" - "L\'archiviazione delle chiavi non è sincronizzata" - "Per non perdere mai una chiamata importante, modifica le impostazioni per consentire le notifiche a schermo intero quando il telefono è bloccato." - "Migliora la tua esperienza di chiamata" - "Vuoi davvero rifiutare l\'invito ad entrare in %1$s?" - "Rifiuta l\'invito" - "Vuoi davvero rifiutare questa conversazione privata con %1$s?" - "Rifiuta l\'invito alla conversazione" - "Nessun invito" - "%1$s (%2$s) ti ha invitato" - "Richiesta di accesso inviata" - "Si tratta di una procedura che si effettua una sola volta, grazie per l\'attesa." - "Configurazione del tuo account." - "Crea una nuova conversazione o stanza" - "Inizia inviando un messaggio a qualcuno." - "Ancora nessuna conversazione." - "Preferiti" - "Puoi aggiungere una conversazione ai tuoi preferiti nelle impostazioni della stessa. -Per il momento, puoi deselezionare i filtri per vedere le altre conversazioni." - "Non hai ancora conversazioni preferite" - "Inviti" - "Non hai nessun invito in sospeso." - "Bassa priorità" - "Puoi deselezionare i filtri per vedere le altre conversazioni." - "Non hai conversazioni per questa selezione" - "Persone" - "Non hai ancora nessuna conversazione diretta" - "Stanze" - "Non sei ancora in nessuna stanza" - "Non letti" - "Congratulazioni! -Non hai messaggi non letti!" - "Tutte le conversazioni" - "Segna come letto" - "Segna come non letto" - "Sfoglia tutte le stanze" - "Sembra che tu stia usando un nuovo dispositivo. Verificati con un altro dispositivo per accedere ai tuoi messaggi cifrati." - "Verifica che sei tu" - diff --git a/features/roomlist/impl/src/main/res/values-ka/translations.xml b/features/roomlist/impl/src/main/res/values-ka/translations.xml deleted file mode 100644 index 0086aeb6e46..00000000000 --- a/features/roomlist/impl/src/main/res/values-ka/translations.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - "აღდგენის დაყენება" - "თქვენი ჩეთების სარეზერვო ასლი ამჟამად არ არის სინქრონიზებული. თქვენ უნდა შეიყვანოთ თქვენი აღდგენის გასაღები, რათა შეინარჩუნოთ წვდომა ჩეთების სარეზერვო ასლზე." - "შეიყვანეთ აღდგენის გასაღები" - "დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ მოწვევაზე %1$s-ში?" - "მოწვევაზე უარის თქმა" - "დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ ჩატზე %1$s-თან?" - "ჩატზე უარის თქვა" - "მოწვევები არ არის" - "%1$s (%2$s) მოგიწვიათ" - "ეს არის ერთჯერადი პროცესი, მადლობა ლოდინისთვის." - "თქვენი ანგარიშის კონფიგურაცია" - "ახალი საუბრისა ან ოთახის შექმნა" - "დაიწყეთ ვინმესთვის შეტყობინების გაგზავნით." - "არც ერთი ჩატი ჯერ არაა." - "ხალხი" - "ჩატები" - "როგორც ჩანს, ახალ მოწყობილობას იყენებთ. დაადასტურეთ სხვა მოწყობილობით თქვენს დაშიფრულ შეტყობინებებზე წვდომისთვის." - "დაადასტურეთ, რომ ეს თქვენ ხართ" - diff --git a/features/roomlist/impl/src/main/res/values-nb/translations.xml b/features/roomlist/impl/src/main/res/values-nb/translations.xml deleted file mode 100644 index 7c108c91a43..00000000000 --- a/features/roomlist/impl/src/main/res/values-nb/translations.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - "Oppgradering tilgjengelig" - "Skriv inn gjenopprettingsnøkkelen din" - "Har du glemt din gjenopprettingsnøkkel?" - "Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?" - "Avvis invitasjon" - "Er du sikker på at du vil avslå denne private chatten med %1$s?" - "Avslå chat" - "Ingen invitasjoner" - "%1$s(%2$s) inviterte deg" - "Opprett en ny samtale eller et nytt rom" - "Kom i gang med å sende meldinger til noen." - "Ingen chatter ennå." - "Favoritter" - "Du har ikke favorittchatter ennå" - "Invitasjoner" - "Du har ingen ventende invitasjoner." - "Lav prioritet" - "Personer" - "Du har ingen DM-er ennå" - "Rom" - "Du er ikke i noe rom ennå" - "Uleste" - "Gratulerer! -Du har ingen uleste meldinger!" - "Chatter" - "Marker som lest" - "Merk som ulest" - "Bla gjennom alle rom" - "Det ser ut til at du bruker en ny enhet. Bekreft med en annen enhet for å få tilgang til de krypterte meldingene dine." - "Bekreft at det er deg" - diff --git a/features/roomlist/impl/src/main/res/values-nl/translations.xml b/features/roomlist/impl/src/main/res/values-nl/translations.xml deleted file mode 100644 index d8ea5bc6f3b..00000000000 --- a/features/roomlist/impl/src/main/res/values-nl/translations.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - "Uitloggen & Upgraden" - "Je server ondersteunt nu een nieuw, sneller protocol. Log uit en log opnieuw in om nu te upgraden. Als je dit nu doet, voorkom je dat je geforceerd uitlogt wordt wanneer het oude protocol later wordt verwijderd." - "Je homeserver ondersteunt het oude protocol niet meer. Log uit en log opnieuw in om de app te blijven gebruiken." - "Upgrade beschikbaar" - "Herstelmogelijkheid instellen" - "Je chatback-up is momenteel niet gesynchroniseerd. Je moet je herstelsleutel invoeren om toegang te behouden tot je chatback-up." - "Voer je herstelsleutel in" - "Pas je instellingen aan om meldingen op het volledige scherm toe te staan wanneer de telefoon is vergrendeld. Zo mis je nooit een belangrijk gesprek." - "Verbeter je gesprekservaring" - "Weet je zeker dat je de uitnodiging om toe te treden tot %1$s wilt weigeren?" - "Uitnodiging weigeren" - "Weet je zeker dat je deze privéchat met %1$s wilt weigeren?" - "Chat weigeren" - "Geen uitnodigingen" - "%1$s (%2$s) heeft je uitgenodigd" - "Verzoek om toe te treden verzonden" - "Dit is een eenmalig proces, bedankt voor het wachten." - "Je account instellen." - "Begin een nieuw gesprek of maak een nieuwe kamer" - "Ga aan de slag door iemand een bericht te sturen." - "Nog geen chats." - "Favorieten" - "Je kunt een chat toevoegen aan je favorieten in de chatinstellingen. -Voor nu kun je filters deselecteren om je andere chats te zien" - "Je hebt nog geen favoriete chats" - "Uitnodigingen" - "Je hebt geen openstaande uitnodigingen." - "Lage prioriteit" - "Je kunt filters deselecteren om je andere chats te zien" - "Je hebt geen chats voor deze selectie" - "Personen" - "Je hebt nog geen directe chats" - "Kamers" - "Je zit nog niet in een kamer" - "Ongelezen" - "Gefeliciteerd! -Je hebt geen ongelezen berichten!" - "Chats" - "Markeren als gelezen" - "Markeren als ongelezen" - "Blader door alle kamers" - "Het lijkt erop dat je een nieuw apparaat gebruikt. Verifieer met een ander apparaat om toegang te krijgen tot je versleutelde berichten." - "Verifieer dat jij het bent" - diff --git a/features/roomlist/impl/src/main/res/values-pl/translations.xml b/features/roomlist/impl/src/main/res/values-pl/translations.xml deleted file mode 100644 index 140eb7b1c4e..00000000000 --- a/features/roomlist/impl/src/main/res/values-pl/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Wyloguj się i zaktualizuj" - "Twój serwer obsługuje teraz nowy, szybszy protokół. Wyloguj się i zaloguj ponownie, aby uaktualnić teraz. Zrobienie tego teraz pomoże uniknąć wymuszonego wylogowania, gdy stary protokół zostanie później usunięty." - "Twój serwer domowy już nie wspiera starego protokołu. Zaloguj się ponownie, aby kontynuować korzystanie z aplikacji." - "Dostępna aktualizacja" - "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?" - "Magazyn kluczy nie jest zsynchronizowany" - "Upewnij się, że nie pominiesz żadnego połączenia. Zmień swoje ustawienia i zezwól na powiadomienia na blokadzie ekranu." - "Popraw jakość swoich rozmów" - "Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?" - "Odrzuć zaproszenie" - "Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?" - "Odrzuć czat" - "Brak zaproszeń" - "%1$s (%2$s) zaprosił Cię" - "Wysłano prośbę o dołączenie" - "Jest to jednorazowy proces, dziękujemy za czekanie." - "Konfigurowanie Twojego konta." - "Utwórz nową rozmowę lub pokój" - "Wyślij komuś wiadomość, aby rozpocząć." - "Brak czatów." - "Ulubione" - "Możesz dodać czat do ulubionych w ustawieniach czatu. -Na razie możesz wyczyścić filtry, aby zobaczyć pozostałe czaty" - "Nie masz jeszcze ulubionych czatów" - "Zaproszenia" - "Nie masz żadnych oczekujących zaproszeń." - "Niski priorytet" - "Wyczyść filtry, aby zobaczyć pozostałe czaty" - "Brak czatów dla podanych kryteriów" - "Osoby" - "Nie masz jeszcze żadnych PW" - "Pokoje" - "Nie jesteś jeszcze w żadnym pokoju" - "Nieprzeczytane" - "Gratulacje! -Nie masz żadnych nieprzeczytanych wiadomości!" - "Wszystkie czaty" - "Oznacz jako przeczytane" - "Oznacz jako nieprzeczytane" - "Przeglądaj wszystkie pokoje" - "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/roomlist/impl/src/main/res/values-pt-rBR/translations.xml b/features/roomlist/impl/src/main/res/values-pt-rBR/translations.xml deleted file mode 100644 index 354c6c736f2..00000000000 --- a/features/roomlist/impl/src/main/res/values-pt-rBR/translations.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - "Configurar a recuperação" - "Esqueceu sua chave de recuperação?" - "Insira sua chave de recuperação" - "Tem certeza de que deseja recusar o convite para ingressar em %1$s?" - "Recusar convite" - "Tem certeza de que deseja recusar esse chat privado com %1$s?" - "Recusar chat" - "Sem convites" - "%1$s(%2$s) convidou você" - "Este é um processo único, obrigado por esperar." - "Configurando sua conta." - "Criar uma nova conversa ou sala" - "Comece enviando uma mensagem para alguém." - "Ainda não há conversas." - "Favoritos" - "Você não tem nenhuma conversa favorita ainda" - "Baixa prioridade" - "Você não tem conversas para esta seleção" - "Pessoas" - "Você não tem nenhum conversa privada ainda" - "Salas" - "Você não está em nenhuma sala ainda" - "Não lidos" - "Parabéns! -Você não tem nenhuma mensagem não lida!" - "Conversas" - "Marcar como lido" - "Marcar como não lido" - "Navegar por todas as salas" - "Parece que você está usando um novo dispositivo. Verifique com outro dispositivo para acessar suas mensagens criptografadas." - "Verifique se é você" - diff --git a/features/roomlist/impl/src/main/res/values-pt/translations.xml b/features/roomlist/impl/src/main/res/values-pt/translations.xml deleted file mode 100644 index b6198aa5a4b..00000000000 --- a/features/roomlist/impl/src/main/res/values-pt/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Sair & Atualizar" - "O teu servidor suporta agora um protocolo novo e mais rápido. Termina a sessão e volta a iniciar sessão para atualizar agora. Se o fizeres agora, evitarás um fim de sessão forçado quando o protocolo antigo for removido mais tarde." - "Seu homeserver não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação." - "Atualização disponível" - "Recupera a tua identidade criptográfica e o histórico de mensagens com uma chave de recuperação se tiveres perdido todos os teus dispositivos existentes." - "Configurar recuperação" - "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" - "Esqueceste-te da tua chave de recuperação?" - "O teu armazenamento de chaves não está sincronizado" - "Para garantir que nunca perdes uma chamada importante, altera as configurações para permitir notificações em ecrã inteiro quando o telemóvel está bloqueado." - "Melhora a tua experiência de chamada" - "Tens a certeza que queres rejeitar o convite para entra em %1$s?" - "Rejeitar convite" - "Tem a certeza que queres rejeitar esta conversa privada com %1$s?" - "Rejeitar conversa" - "Sem convites" - "%1$s (%2$s) convidou-te" - "Pedido de adesão enviado" - "Este processo só acontece uma única vez, obrigado por esperares." - "A configurar a tua conta…" - "Criar uma nova conversa ou sala" - "Começa por enviar uma mensagem a alguém." - "Ainda não tens conversas." - "Favoritas" - "Podes adicionar uma conversa às tuas favoritas nas suas configurações. -Por enquanto, podes anular a seleção dos filtros para veres as tuas outras conversas" - "Ainda não tens nenhuma conversa favorita" - "Convites" - "Não tens nenhum convite pendente." - "Prioridade baixa" - "Podes anular a seleção dos filtros para veres as tuas outras conversas" - "Não tens nenhuma conversa selecionada" - "Pessoas" - "Ainda não tens nenhuma MD (mensagem direta)" - "Salas" - "Ainda não estás em nenhuma sala" - "Por ler" - "Parabéns! -Não tens nenhuma mensagem por ler!" - "Conversas" - "Marcar como lida" - "Marcar como não lida" - "Consultar lista completa de salas" - "Parece que estás a utilizar um novo dispositivo. Verifica-o com um outro para poderes aceder às tuas mensagens cifradas." - "Verifica que és tu" - diff --git a/features/roomlist/impl/src/main/res/values-ro/translations.xml b/features/roomlist/impl/src/main/res/values-ro/translations.xml deleted file mode 100644 index 077ed1a4c5b..00000000000 --- a/features/roomlist/impl/src/main/res/values-ro/translations.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - "Deconectați-vă și faceți upgrade" - "Serverul dvs. acceptă acum un protocol nou, mai rapid. Deconectați-vă și conectați-vă din nou pentru a face upgrade acum. Dacă faceți acest lucru acum, vă va ajuta să evitați o deconectare forțată atunci când vechiul protocol este eliminat ulterior." - "Serverul dvs. de acasă nu mai acceptă vechiul protocol. Vă rugăm să vă deconectați și să vă conectați din nou pentru a continua să utilizați aplicația." - "Upgrade disponibil" - "Recuperați-vă identitatea criptografică și istoricul mesajelor 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 în prezent. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup." - "Backup-ul nu este sincronizat" - "Pentru a vă asigura că nu pierdeți niciodată un apel important, vă rugăm să modificați setările pentru a permite notificări fullscreen atunci când telefonul este blocat." - "Îmbunătățiți-vă experiența in timpul unui apel" - "Sigur doriți să refuzați alăturarea la %1$s?" - "Refuzați invitația" - "Sigur doriți să refuzați conversațiile cu %1$s?" - "Refuzați conversația" - "Nicio invitație" - "%1$s (%2$s) v-a invitat." - "Cererea de alăturare a fost trimisă" - "Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare." - "Contul dumneavoastră se configurează" - "Creați o conversație sau o cameră nouă" - "Începeți prin a trimite mesaje cuiva." - "Nu există încă discuții." - "Favorite" - "Puteți adăuga un chat la preferințele dvs. în setările de chat. -Deocamdată, puteți deselecta filtrele pentru a vedea celelalte chat-uri" - "Încă nu aveți conversații preferate" - "Invitații" - "Nu aveți invitații în așteptare." - "Prioritate scăzută" - "Puteți deselecta filtrele pentru a vedea celelalte chat-uri" - "Nu aveți chat-uri pentru această selecție" - "Persoane" - "Încă nu aveți DM-uri" - "Camere" - "Nu sunteți încă în nicio cameră" - "Necitite" - "Felicitari! -Nu aveți mesaje necitite!" - "Toate conversatiile" - "Marcați ca citită" - "Marcați ca necitită" - "Răsfoiți toate camerele" - "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/roomlist/impl/src/main/res/values-ru/translations.xml b/features/roomlist/impl/src/main/res/values-ru/translations.xml deleted file mode 100644 index 9c2e902afcc..00000000000 --- a/features/roomlist/impl/src/main/res/values-ru/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Выйти и обновить" - "Теперь ваш сервер поддерживает новый, более быстрый протокол. Чтобы обновить его прямо сейчас, выйдите и войдите в свою учётную запись снова. Сделав это сейчас, вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола." - "Ваш домашний сервер больше не поддерживает старый протокол. Пожалуйста, выйдите и войдите в свою учётную запись снова, чтобы продолжить использование приложения." - "Доступно обновление" - "Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам." - "Настроить восстановление" - "Для защиты вашего аккаунта рекомендуется настроить восстановление" - "Подтвердите ключ восстановления, чтобы сохранить доступ к хранилищу ключей и истории сообщений." - "Введите ключ восстановления" - "Забыли ключ восстановления?" - "Хранилище ключей не синхронизировано" - "Чтобы больше не пропускать важные звонки, разрешите приложению показывать полноэкранные уведомления на заблокированном экране телефона." - "Улучшите качество звонков" - "Вы уверены, что хотите отклонить приглашение в %1$s?" - "Отклонить приглашение" - "Вы уверены, что хотите отказаться от личного общения с %1$s?" - "Отклонить чат" - "Нет приглашений" - "%1$s (%2$s) пригласил вас" - "Запрос на присоединение отправлен" - "Это одноразовый процесс, спасибо, что подождали." - "Настройка учетной записи." - "Создайте новую беседу или комнату" - "Начните переписку с отправки сообщения." - "Пока нет доступных чатов." - "Избранное" - "Добавить чат в избранное можно в настройках чата. -На данный момент вы можете убрать фильтры, чтобы увидеть другие ваши чаты." - "У вас пока нет избранных чатов" - "Приглашения" - "У вас нет отложенных приглашений." - "Низкий приоритет" - "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты." - "У вас нет чатов для этой подборки" - "Пользователи" - "У вас пока нет личных сообщений" - "Комнаты" - "Вас пока нет ни в одной комнате" - "Непрочитанные" - "Поздравляем! -У вас нет непрочитанных сообщений!" - "Все чаты" - "Пометить как прочитанное" - "Отметить как непрочитанное" - "Просмотреть все комнаты" - "Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям пройдите подтверждение с другим устройством." - "Подтвердите, что это вы" - diff --git a/features/roomlist/impl/src/main/res/values-sk/translations.xml b/features/roomlist/impl/src/main/res/values-sk/translations.xml deleted file mode 100644 index 610d9bb489a..00000000000 --- a/features/roomlist/impl/src/main/res/values-sk/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Odhlásiť sa a aktualizovať" - "Váš server teraz podporuje nový, rýchlejší protokol. Odhláste sa a prihláste sa znova, aby ste mohli aktualizovať. Ak to urobíte teraz, pomôže vám vyhnúť sa nútenému odhláseniu, keď sa starý protokol neskôr odstráni." - "Váš domovský server už nepodporuje starý protokol. Ak chcete pokračovať v používaní aplikácie, odhláste sa a znova sa prihláste." - "Aktualizácia je k dispozícii" - "Vytvorte nový kľúč na obnovenie, ktorý môžete použiť na obnovenie vašej histórie šifrovaných správ v prípade straty prístupu k vašim zariadeniam." - "Nastaviť obnovenie" - "Nastaviť obnovenie" - "Potvrďte svoj kľúč na obnovenie, aby ste zachovali prístup k úložisku kľúčov a histórii správ." - "Zadajte kľúč na obnovenie" - "Zabudli ste svoj kľúč na obnovenie?" - "Vaše úložisko kľúčov nie je synchronizované" - "Aby ste už nikdy nezmeškali dôležitý hovor, zmeňte svoje nastavenia a povoľte upozornenia na celú obrazovku, keď je váš telefón uzamknutý." - "Vylepšite svoj zážitok z hovoru" - "Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?" - "Odmietnuť pozvanie" - "Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?" - "Odmietnuť konverzáciu" - "Žiadne pozvánky" - "%1$s (%2$s) vás pozval/a" - "Žiadosť o pripojenie bola odoslaná" - "Ide o jednorazový proces, ďakujeme za trpezlivosť." - "Nastavenie vášho účtu." - "Vytvorte novú konverzáciu alebo miestnosť" - "Začnite tým, že niekomu pošlete správu." - "Zatiaľ žiadne konverzácie." - "Obľúbené" - "Môžete pridať konverzáciu medzi obľúbené v nastaveniach konverzácie. -Zatiaľ môžete zrušiť výber filtrov, aby ste videli ostatné konverzácie" - "Zatiaľ nemáte obľúbené konverzácie" - "Pozvánky" - "Nemáte žiadne čakajúce pozvánky." - "Nízka priorita" - "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie" - "Nemáte konverzácie pre tento výber" - "Ľudia" - "Zatiaľ nemáte žiadne priame správy" - "Miestnosti" - "Zatiaľ ešte nie ste v žiadnej miestnosti" - "Neprečítané" - "Gratulujeme! -Nemáte žiadne neprečítané správy!" - "Všetky konverzácie" - "Označiť ako prečítané" - "Označiť ako neprečítané" - "Prehliadať všetky miestnosti" - "Vyzerá to tak, že používate nové zariadenie. Overte svoj prístup k zašifrovaným správam pomocou vášho druhého zariadenia." - "Overte, že ste to vy" - diff --git a/features/roomlist/impl/src/main/res/values-sv/translations.xml b/features/roomlist/impl/src/main/res/values-sv/translations.xml deleted file mode 100644 index f2a30a20506..00000000000 --- a/features/roomlist/impl/src/main/res/values-sv/translations.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - "Logga ut och uppgradera" - "Din server stöder nu ett nytt, snabbare protokoll. Logga ut och logga in igen för att uppgradera nu. Om du gör detta nu hjälper du dig att undvika en tvingad utloggning när det gamla protokollet tas bort senare." - "Din hemserver stöder inte längre det gamla protokollet. Logga ut och logga in igen för att fortsätta använda appen." - "Uppgradering tillgänglig" - "Skapa en ny återställningsnyckel som kan användas för att återställa din krypterade meddelandehistorik om du förlorar åtkomst till dina enheter." - "Ställ in återställning" - "Ställ in återställning" - "Bekräfta din återställningsnyckel för att behålla åtkomsten till din nyckellagring och meddelandehistorik." - "Din nyckellagring är inte synkroniserad" - "För att säkerställa att du aldrig missar ett viktigt samtal, ändra dina inställningar för att tillåta helskärmsmeddelanden när telefonen är låst." - "Förbättra din samtalsupplevelse" - "Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?" - "Avböj inbjudan" - "Är du säker på att du vill avböja denna privata chatt med %1$s?" - "Avböj chatt" - "Inga inbjudningar" - "%1$s (%2$s) bjöd in dig" - "Begäran om att gå med skickad" - "Detta är en engångsprocess, tack för att du väntar." - "Konfigurerar ditt konto" - "Skapa en ny konversation eller ett nytt rum" - "Kom igång genom att skicka meddelanden till någon." - "Inga chattar än." - "Favoriter" - "Du kan lägga till en chatt till dina favoriter i chattinställningarna. -För tillfället kan du avmarkera filter för att se dina andra chattar" - "Du har inga favoritchattar än" - "Inbjudningar" - "Du har inga väntande inbjudningar." - "Låg prioritet" - "Du kan avmarkera filter för att se dina andra chattar" - "Du har inga chattar för det här valet" - "Personer" - "Du har inga DM:er än" - "Rum" - "Du är inte i något rum än" - "Olästa" - "Grattis! -Du har inga olästa meddelanden!" - "Alla chattar" - "Markera som läst" - "Markera som oläst" - "Bläddra bland alla rum" - "Det verkar som om du använder en ny enhet. Verifiera med en annan enhet för att komma åt dina krypterade meddelanden." - "Verifiera att det är du" - diff --git a/features/roomlist/impl/src/main/res/values-uk/translations.xml b/features/roomlist/impl/src/main/res/values-uk/translations.xml deleted file mode 100644 index 783bac68c0d..00000000000 --- a/features/roomlist/impl/src/main/res/values-uk/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Вийти та оновити" - "Ваш сервер тепер підтримує новий, швидший протокол. Вийдіть із системи та увійдіть знову, щоб оновити систему зараз. Якщо ви зробите це зараз, це допоможе вам уникнути примусового виходу з системи, коли старий протокол буде видалено пізніше." - "Ваш домашній сервер більше не підтримує старий протокол. Будь ласка, вийдіть і увійдіть знову, щоб продовжити використання програми." - "Доступне оновлення" - "Відновіть свою криптографічну ідентичність та історію повідомлень за допомогою ключа відновлення, якщо ви втратили всі наявні пристрої." - "Налаштувати відновлення" - "Налаштуйте відновлення для захисту свого облікового запису" - "Підтвердіть свій ключ відновлення, щоб мати доступ до сховища ключів та історії повідомлень." - "Введіть ключ відновлення" - "Забули ключ відновлення?" - "Ваше сховище ключів не синхронізовано" - "Щоб ніколи не пропустити важливий виклик, змініть налаштування, щоб увімкнути повноекранні сповіщення, коли телефон заблоковано." - "Покращуйте досвід дзвінків" - "Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?" - "Відхилити запрошення" - "Ви дійсно хочете відмовитися від приватної бесіди з %1$s?" - "Відхилити бесіду" - "Немає запрошень" - "%1$s (%2$s) запрошує вас" - "Запит на приєднання надіслано" - "Це одноразовий процес, дякую за очікування." - "Налаштування облікового запису." - "Створити нову розмову або кімнату" - "Почніть з обміну повідомленнями з кимось." - "Ще немає бесід." - "Обране" - "Ви можете додати бесіду до обраних у налаштуваннях бесіди. -Наразі ви можете зняти фільтри, щоб побачити інші ваші бесіди" - "Ви ще не маєте обраних бесід" - "Запрошення" - "У вас немає запрошень, що очікують на розгляд." - "Низький пріоритет" - "Ви можете зняти фільтри, щоб побачити інші ваші бесіди" - "Ви не маєте бесід для цієї категорії" - "Люди" - "Ви ще не маєте жодної особистої бесіди" - "Кімнати" - "Ви ще не учасник жодної кімнати" - "Непрочитані" - "Вітаємо! -У вас немає непрочитаних повідомлень!" - "Бесіди" - "Позначити прочитаним" - "Позначити непрочитаним" - "Переглянути всі кімнати" - "Схоже, ви використовуєте новий пристрій. Щоб отримати доступ до зашифрованих повідомлень, підтвердьте особу за допомогою іншого пристрою." - "Підтвердьте, що це ви" - diff --git a/features/roomlist/impl/src/main/res/values-uz/translations.xml b/features/roomlist/impl/src/main/res/values-uz/translations.xml deleted file mode 100644 index 0fb88583040..00000000000 --- a/features/roomlist/impl/src/main/res/values-uz/translations.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - "Qayta tiklashni sozlang" - "Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?" - "Taklifni rad etish" - "Haqiqatan ham bu shaxsiy chatni rad qilmoqchimisiz%1$s ?" - "Chatni rad etish" - "Takliflar yo\'q" - "%1$s(%2$s ) sizni taklif qildi" - "Bu bir martalik jarayon, kutganingiz uchun rahmat." - "Hisobingiz sozlanmoqda." - "Yangi suhbat yoki xona yarating" - "Kimgadir xabar yuborishdan boshlang." - "Hozircha chatlar yo‘q." - "Odamlar" - "Suhbatlar" - "Siz yangi qurilmadan foydalanayotganga o‘xshaysiz. Shifrlangan xabarlaringizga kirish uchun boshqa qurilma bilan tasdiqlang." - "Siz ekanligingizni tasdiqlang" - diff --git a/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml deleted file mode 100644 index 82b333e7789..00000000000 --- a/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "登出並升級" - "您的伺服器現在支援新的、更快的協定。立即登出並重新登入以進行升級。現在這樣做將協助您避免在稍後移除舊協定時被強制登出。" - "您的家伺服器不再支援舊協定。請登出並重新登入以繼續使用應用程式。" - "可升級" - "若您遺失了所有現有裝置,則請使用復原金鑰以救援您的密碼學身份與訊息歷史紀錄。" - "設定復原" - "設定備援以保護您的帳號" - "確認您的復原金鑰以維持對金鑰儲存空間與訊息歷史紀錄的存取權。" - "輸入您的復原金鑰" - "忘記了您的復原金鑰?" - "您的金鑰儲存空間並未同步" - "為確保您永遠不會錯過重要通話,請變更設定以允許在手機鎖定時允許全螢幕通知。" - "提升您的通話體驗" - "您確定您想要拒絕加入 %1$s 的邀請嗎?" - "拒絕邀請" - "您確定您要拒絕此與 %1$s 的私人聊天嗎?" - "拒絕聊天" - "沒有邀請" - "%1$s(%2$s)邀請您" - "已傳送加入請求" - "這是一次性的程序,感謝您耐心等候。" - "正在設定您的帳號。" - "建立新的對話或聊天室" - "從向某人傳送訊息開始。" - "尚無聊天室。" - "我的最愛" - "您可以在聊天設定中將聊天新增至收藏。 -目前,您可以取消選取篩選條件以檢視其他聊天" - "您尚無收藏聊天" - "邀請" - "您沒有任何擱置中的邀請。" - "低優先度" - "您可以取消選取篩選條件以檢視其他聊天" - "您並無此選擇的聊天" - "夥伴" - "您尚無任何私人訊息" - "聊天室" - "您尚未進入任何聊天室" - "未讀" - "恭喜! -您沒有任何未讀的訊息!" - "所有聊天室" - "標為已讀" - "標為未讀" - "瀏覽所有聊天室" - "您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。" - "驗證這是您本人" - diff --git a/features/roomlist/impl/src/main/res/values-zh/translations.xml b/features/roomlist/impl/src/main/res/values-zh/translations.xml deleted file mode 100644 index 7c2555d489e..00000000000 --- a/features/roomlist/impl/src/main/res/values-zh/translations.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "登出并升级" - "您的服务器现在支持更快的新协议。现在登出并重新登录以进行升级。现在这样做可以帮助您避免在以后删除旧协议时被强制登出。" - "您的服务器不再支持旧协议。请登出并重新登录以继续使用此应用。" - "有可用升级" - "生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。" - "设置恢复" - "设置恢复" - "确认恢复密钥,以保持对密钥存储和消息历史的访问。" - "输入恢复密钥" - "忘记了恢复密钥?" - "你的密钥存储已不同步" - "为确保您不会错过重要来电,请更改设置以允许锁屏时的全屏通知。" - "提升通话体验" - "您确定要拒绝加入 %1$s 的邀请吗?" - "拒绝邀请" - "您确定要拒绝与 %1$s 开始私聊吗?" - "拒绝聊天" - "没有邀请" - "%1$s (%2$s)邀请了你" - "加入请求已发送" - "这是一个一次性的过程,感谢您的等待。" - "设置您的账户。" - "创建新的对话或聊天室" - "通过向某人发送消息来开始。" - "还没有聊天。" - "收藏夹" - "可以在聊天设置里将聊天添加到收藏夹中。 -现在,可以取消选择过滤器以查看其他对话。" - "您未收藏任何聊天" - "邀请" - "没有待处理的邀请。" - "低优先级" - "您可以取消选择过滤器以查看其他对话" - "您没有关于此选项的聊天" - "用户" - "目前您还没有私信" - "聊天室" - "您尚未进入任何聊天室" - "未读" - "恭喜! -没有任何未读消息!" - "全部聊天" - "标记为已读" - "标记为未读" - "浏览所有聊天室" - "您似乎正在使用新设备。使用另一台设备进行验证以访问您的加密消息。" - "验证是你本人" - diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml deleted file mode 100644 index e10d8da9088..00000000000 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - "Log Out & Upgrade" - "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later." - "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app." - "Upgrade available" - "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices." - "Set up recovery" - "Set up recovery to protect your account" - "Confirm your recovery key to maintain access to your key storage and message history." - "Enter your recovery key" - "Forgot your recovery key?" - "Your key storage is out of sync" - "To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked." - "Enhance your call experience" - "Are you sure you want to decline the invitation to join %1$s?" - "Decline invite" - "Are you sure you want to decline this private chat with %1$s?" - "Decline chat" - "No Invites" - "%1$s (%2$s) invited you" - "Request to join sent" - "This is a one time process, thanks for waiting." - "Setting up your account." - "Create a new conversation or room" - "Get started by messaging someone." - "No chats yet." - "Favourites" - "You can add a chat to your favourites in the chat settings. -For now, you can deselect filters in order to see your other chats" - "You don’t have favourite chats yet" - "Invites" - "You don\'t have any pending invites." - "Low Priority" - "You can deselect filters in order to see your other chats" - "You don’t have chats for this selection" - "People" - "You don’t have any DMs yet" - "Rooms" - "You’re not in any room yet" - "Unreads" - "Congrats! -You don’t have any unread messages!" - "Chats" - "Mark as read" - "Mark as unread" - "Browse all rooms" - "Looks like you’re using a new device. Verify with another device to access your encrypted messages." - "Verify it’s you" - diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt deleted file mode 100644 index 25d57dab7a2..00000000000 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomlist.impl - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureCalledOnceWithParam -import io.element.android.tests.testutils.EnsureNeverCalledWithParam -import io.element.android.tests.testutils.EventsRecorder -import io.element.android.tests.testutils.clickOn -import 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`() { - val eventsRecorder = EventsRecorder() - val contextMenu = aContextMenuShown(hasNewContent = true) - rule.setContent { - RoomListContextMenu( - contextMenu = contextMenu, - eventSink = eventsRecorder, - onRoomSettingsClick = EnsureNeverCalledWithParam(), - ) - } - rule.clickOn(R.string.screen_roomlist_mark_as_read) - eventsRecorder.assertList( - listOf( - RoomListEvents.HideContextMenu, - RoomListEvents.MarkAsRead(contextMenu.roomId), - ) - ) - } - - @Test - fun `clicking on Mark as unread generates expected Events`() { - val eventsRecorder = EventsRecorder() - val contextMenu = aContextMenuShown(hasNewContent = false) - rule.setContent { - RoomListContextMenu( - contextMenu = contextMenu, - eventSink = eventsRecorder, - onRoomSettingsClick = EnsureNeverCalledWithParam(), - ) - } - rule.clickOn(R.string.screen_roomlist_mark_as_unread) - eventsRecorder.assertList( - listOf( - RoomListEvents.HideContextMenu, - RoomListEvents.MarkAsUnread(contextMenu.roomId), - ) - ) - } - - @Test - fun `clicking on Leave dm generates expected Events`() { - val eventsRecorder = EventsRecorder() - val contextMenu = aContextMenuShown(isDm = true) - rule.setContent { - RoomListContextMenu( - contextMenu = contextMenu, - eventSink = eventsRecorder, - onRoomSettingsClick = EnsureNeverCalledWithParam(), - ) - } - rule.clickOn(CommonStrings.action_leave_conversation) - eventsRecorder.assertList( - listOf( - RoomListEvents.HideContextMenu, - RoomListEvents.LeaveRoom(contextMenu.roomId), - ) - ) - } - - @Test - fun `clicking on Leave room generates expected Events`() { - val eventsRecorder = EventsRecorder() - val contextMenu = aContextMenuShown(isDm = false) - rule.setContent { - RoomListContextMenu( - contextMenu = contextMenu, - eventSink = eventsRecorder, - onRoomSettingsClick = EnsureNeverCalledWithParam(), - ) - } - rule.clickOn(CommonStrings.action_leave_room) - eventsRecorder.assertList( - listOf( - RoomListEvents.HideContextMenu, - RoomListEvents.LeaveRoom(contextMenu.roomId), - ) - ) - } - - @Test - fun `clicking on Settings invokes the expected callback and generates expected Event`() { - val eventsRecorder = EventsRecorder() - val contextMenu = aContextMenuShown() - val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - rule.setContent { - RoomListContextMenu( - contextMenu = contextMenu, - eventSink = eventsRecorder, - onRoomSettingsClick = callback, - ) - } - rule.clickOn(CommonStrings.common_settings) - eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) - callback.assertSuccess() - } - - @Test - fun `clicking on Favourites generates expected Event`() { - val eventsRecorder = EventsRecorder() - val contextMenu = aContextMenuShown(isDm = false, isFavorite = false) - val callback = EnsureNeverCalledWithParam() - rule.setContent { - RoomListContextMenu( - contextMenu = contextMenu, - eventSink = eventsRecorder, - onRoomSettingsClick = callback, - ) - } - rule.clickOn(CommonStrings.common_favourite) - eventsRecorder.assertList( - listOf( - RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, true), - ) - ) - } -} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt deleted file mode 100644 index faf3fcfb5c5..00000000000 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomlist.impl - -internal fun RoomListState.contentAsRooms() = contentState as RoomListContentState.Rooms diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt deleted file mode 100644 index caa204328bd..00000000000 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomlist.impl.model - -import com.google.common.truth.Truth.assertThat -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.RoomNotificationMode -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_ROOM_NAME -import kotlinx.collections.immutable.toPersistentList -import org.junit.Test - -class RoomListRoomSummaryTest { - @Test - fun `test default value`() { - val sut = createRoomListRoomSummary( - isMarkedUnread = false, - ) - assertThat(sut.isHighlighted).isFalse() - assertThat(sut.hasNewContent).isFalse() - } - - @Test - fun `test muted room`() { - val sut = createRoomListRoomSummary( - userDefinedNotificationMode = RoomNotificationMode.MUTE, - ) - assertThat(sut.isHighlighted).isFalse() - assertThat(sut.hasNewContent).isFalse() - } - - @Test - fun `test muted room isMarkedUnread set to true`() { - val sut = createRoomListRoomSummary( - isMarkedUnread = true, - userDefinedNotificationMode = RoomNotificationMode.MUTE, - ) - assertThat(sut.isHighlighted).isTrue() - assertThat(sut.hasNewContent).isTrue() - } - - @Test - fun `test muted room with unread message`() { - val sut = createRoomListRoomSummary( - numberOfUnreadNotifications = 1, - userDefinedNotificationMode = RoomNotificationMode.MUTE, - ) - assertThat(sut.isHighlighted).isFalse() - assertThat(sut.hasNewContent).isTrue() - } - - @Test - fun `test isMarkedUnread set to true`() { - val sut = createRoomListRoomSummary( - isMarkedUnread = true, - ) - assertThat(sut.isHighlighted).isTrue() - assertThat(sut.hasNewContent).isTrue() - } - - @Test - fun `when display type is invite then isHighlighted and hasNewContent are true`() { - val sut = createRoomListRoomSummary( - displayType = RoomSummaryDisplayType.INVITE, - ) - assertThat(sut.isHighlighted).isTrue() - assertThat(sut.hasNewContent).isTrue() - } -} - -internal fun createRoomListRoomSummary( - numberOfUnreadMentions: Long = 0, - numberOfUnreadMessages: Long = 0, - numberOfUnreadNotifications: Long = 0, - isMarkedUnread: Boolean = false, - userDefinedNotificationMode: RoomNotificationMode? = null, - isFavorite: Boolean = false, - displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM, - heroes: List = emptyList(), - timestamp: String? = null, -) = RoomListRoomSummary( - id = A_ROOM_ID.value, - roomId = A_ROOM_ID, - name = A_ROOM_NAME, - numberOfUnreadMentions = numberOfUnreadMentions, - numberOfUnreadMessages = numberOfUnreadMessages, - numberOfUnreadNotifications = numberOfUnreadNotifications, - isMarkedUnread = isMarkedUnread, - timestamp = timestamp, - lastMessage = "", - avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem), - displayType = displayType, - userDefinedNotificationMode = userDefinedNotificationMode, - hasRoomCall = false, - isDirect = false, - isFavorite = isFavorite, - canonicalAlias = null, - inviteSender = null, - isDm = false, - heroes = heroes.toPersistentList(), -) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchViewTest.kt deleted file mode 100644 index e901e7703c8..00000000000 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchViewTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.roomlist.impl.search - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.roomlist.impl.R -import io.element.android.features.roomlist.impl.RoomListEvents -import io.element.android.libraries.matrix.api.core.RoomId -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 org.junit.Rule -import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class RoomListSearchViewTest { - @get:Rule val rule = createAndroidComposeRule() - - @Test - fun `clicking on 'Browse all rooms' invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { - rule.setRoomListSearchView( - aRoomListSearchState( - isSearchActive = true, - isRoomDirectorySearchEnabled = true, - eventSink = eventsRecorder, - ), - onRoomDirectorySearchClick = it, - ) - rule.clickOn(R.string.screen_roomlist_room_directory_button_title) - } - } -} - -private fun AndroidComposeTestRule.setRoomListSearchView( - state: RoomListSearchState, - eventSink: (RoomListEvents) -> Unit = EventsRecorder(expectEvents = false), - onRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), - onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(), -) { - setContent { - RoomListSearchView( - state = state, - eventSink = eventSink, - onRoomClick = onRoomClick, - onRoomDirectorySearchClick = onRoomDirectorySearchClick, - ) - } -} diff --git a/features/roommembermoderation/api/build.gradle.kts b/features/roommembermoderation/api/build.gradle.kts new file mode 100644 index 00000000000..aeea031757a --- /dev/null +++ b/features/roommembermoderation/api/build.gradle.kts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.roommembermoderation.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.matrix.api) +} diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt new file mode 100644 index 00000000000..da25ca41eb8 --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt @@ -0,0 +1,15 @@ +/* + * 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.roommembermoderation.api + +import io.element.android.libraries.matrix.api.user.MatrixUser + +interface RoomMemberModerationEvents { + data class ShowActionsForUser(val user: MatrixUser) : RoomMemberModerationEvents + data class ProcessAction(val action: ModerationAction, val targetUser: MatrixUser) : RoomMemberModerationEvents +} diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt new file mode 100644 index 00000000000..afe8ab0f8bf --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt @@ -0,0 +1,21 @@ +/* + * 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.roommembermoderation.api + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.matrix.api.user.MatrixUser + +interface RoomMemberModerationRenderer { + @Composable + fun Render( + state: RoomMemberModerationState, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, + modifier: Modifier, + ) +} diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt new file mode 100644 index 00000000000..368aa283ad1 --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -0,0 +1,26 @@ +/* + * 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.roommembermoderation.api + +interface RoomMemberModerationState { + val canKick: Boolean + val canBan: Boolean + val eventSink: (RoomMemberModerationEvents) -> Unit +} + +data class ModerationActionState( + val action: ModerationAction, + val isEnabled: Boolean, +) + +sealed interface ModerationAction { + data object DisplayProfile : ModerationAction + data object KickUser : ModerationAction + data object BanUser : ModerationAction + data object UnbanUser : ModerationAction +} diff --git a/features/roommembermoderation/impl/build.gradle.kts b/features/roommembermoderation/impl/build.gradle.kts new file mode 100644 index 00000000000..93294988f6a --- /dev/null +++ b/features/roommembermoderation/impl/build.gradle.kts @@ -0,0 +1,47 @@ +import extension.setupAnvil + +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.roommembermoderation.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +setupAnvil() + +dependencies { + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + api(projects.features.roommembermoderation.api) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.services.analytics.compose) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) + testImplementation(projects.services.analytics.test) + testImplementation(libs.test.robolectric) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) + testImplementation(projects.libraries.testtags) +} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt new file mode 100644 index 00000000000..681a1eb7336 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt @@ -0,0 +1,38 @@ +/* + * 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.roommembermoderation.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Modifier +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.user.MatrixUser +import timber.log.Timber +import javax.inject.Inject + +@ContributesBinding(RoomScope::class) +class DefaultRoomMemberModerationRenderer @Inject constructor() : RoomMemberModerationRenderer { + @Composable + override fun Render( + state: RoomMemberModerationState, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, + modifier: Modifier + ) { + if (state is InternalRoomMemberModerationState) { + RoomMemberModerationView(state, onSelectAction, modifier) + } else { + SideEffect { + Timber.d("RoomMemberModerationRenderer: Render called with unsupported state: $state") + } + } + } +} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt new file mode 100644 index 00000000000..902c2bd21ff --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt @@ -0,0 +1,17 @@ +/* + * 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.roommembermoderation.impl + +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents + +sealed interface InternalRoomMemberModerationEvents : RoomMemberModerationEvents { + data class DoKickUser(val reason: String) : InternalRoomMemberModerationEvents + data class DoBanUser(val reason: String) : InternalRoomMemberModerationEvents + data object DoUnbanUser : InternalRoomMemberModerationEvents + data object Reset : InternalRoomMemberModerationEvents +} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt new file mode 100644 index 00000000000..3a650ee59c1 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -0,0 +1,28 @@ +/* + * 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.roommembermoderation.impl + +import io.element.android.features.roommembermoderation.api.ModerationActionState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +data class InternalRoomMemberModerationState( + override val canKick: Boolean, + override val canBan: Boolean, + val selectedUser: MatrixUser?, + val actions: ImmutableList, + val kickUserAsyncAction: AsyncAction, + val banUserAsyncAction: AsyncAction, + val unbanUserAsyncAction: AsyncAction, + override val eventSink: (RoomMemberModerationEvents) -> Unit, +) : RoomMemberModerationState { + val canDisplayActions = actions.isNotEmpty() +} 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 new file mode 100644 index 00000000000..bdebcc3a935 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt @@ -0,0 +1,94 @@ +/* + * 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.roommembermoderation.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.toPersistentList + +class InternalRoomMemberModerationStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ), + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = true), + ), + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ModerationActionState(action = ModerationAction.BanUser, isEnabled = true), + ), + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = true), + ), + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + kickUserAsyncAction = AsyncAction.ConfirmingNoParams, + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + kickUserAsyncAction = AsyncAction.Loading, + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + banUserAsyncAction = AsyncAction.ConfirmingNoParams, + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + banUserAsyncAction = AsyncAction.Loading, + ), + ) +} + +fun anAlice() = MatrixUser( + UserId(value = "@alice:server.org"), + displayName = "Alice", + avatarUrl = null, +) + +fun aRoomMembersModerationState( + canKick: Boolean = false, + canBan: Boolean = false, + selectedUser: MatrixUser? = null, + actions: List = emptyList(), + kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + unbanUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (RoomMemberModerationEvents) -> Unit = {}, +) = InternalRoomMemberModerationState( + canKick = canKick, + canBan = canBan, + selectedUser = selectedUser, + actions = actions.toPersistentList(), + kickUserAsyncAction = kickUserAsyncAction, + banUserAsyncAction = banUserAsyncAction, + unbanUserAsyncAction = unbanUserAsyncAction, + eventSink = eventSink, +) diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt new file mode 100644 index 00000000000..f0861a735e2 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -0,0 +1,213 @@ +/* + * 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.roommembermoderation.impl + +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 +import androidx.compose.runtime.setValue +import im.vector.app.features.analytics.plan.RoomModeration +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +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.coroutine.CoroutineDispatchers +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.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.room.canBanAsState +import io.element.android.libraries.matrix.ui.room.canKickAsState +import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import javax.inject.Inject + +class RoomMemberModerationPresenter @Inject constructor( + private val room: JoinedRoom, + private val dispatchers: CoroutineDispatchers, + private val analyticsService: AnalyticsService, +) : Presenter { + @Composable + override fun present(): RoomMemberModerationState { + val coroutineScope = rememberCoroutineScope() + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() + val canBan = room.canBanAsState(syncUpdateFlow.value) + val canKick = room.canKickAsState(syncUpdateFlow.value) + val currentUserMemberPowerLevel = room.userPowerLevelAsState(syncUpdateFlow.value) + + val kickUserAsyncAction = + remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } + val banUserAsyncAction = + remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } + val unbanUserAsyncAction = + remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } + var selectedUser by remember { + mutableStateOf(null) + } + val moderationActions = remember { mutableStateOf(persistentListOf()) } + + fun handleEvent(event: RoomMemberModerationEvents) { + when (event) { + is RoomMemberModerationEvents.ShowActionsForUser -> { + selectedUser = event.user + val member = room.membersStateFlow.value.roomMembers()?.firstOrNull { + it.userId == event.user.userId + } + moderationActions.value = computeModerationActions( + member = member, + canKick = canKick.value, + canBan = canBan.value, + currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, + ) + } + is RoomMemberModerationEvents.ProcessAction -> { + when (event.action) { + is ModerationAction.DisplayProfile -> Unit + is ModerationAction.KickUser -> { + selectedUser = event.targetUser + kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams + } + is ModerationAction.BanUser -> { + selectedUser = event.targetUser + banUserAsyncAction.value = AsyncAction.ConfirmingNoParams + } + is ModerationAction.UnbanUser -> { + selectedUser = event.targetUser + unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams + } + } + } + is InternalRoomMemberModerationEvents.DoKickUser -> { + selectedUser?.let { + coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction) + } + selectedUser = null + } + is InternalRoomMemberModerationEvents.DoBanUser -> { + selectedUser?.let { + coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction) + } + selectedUser = null + } + is InternalRoomMemberModerationEvents.DoUnbanUser -> { + selectedUser?.let { + coroutineScope.unbanUser(it.userId, unbanUserAsyncAction) + } + selectedUser = null + } + is InternalRoomMemberModerationEvents.Reset -> { + selectedUser = null + kickUserAsyncAction.value = AsyncAction.Uninitialized + banUserAsyncAction.value = AsyncAction.Uninitialized + unbanUserAsyncAction.value = AsyncAction.Uninitialized + } + } + } + + return InternalRoomMemberModerationState( + canKick = canKick.value, + canBan = canBan.value, + selectedUser = selectedUser, + actions = moderationActions.value, + kickUserAsyncAction = kickUserAsyncAction.value, + banUserAsyncAction = banUserAsyncAction.value, + unbanUserAsyncAction = unbanUserAsyncAction.value, + eventSink = { handleEvent(it) }, + ) + } + + private fun computeModerationActions( + member: RoomMember?, + canKick: Boolean, + canBan: Boolean, + currentUserMemberPowerLevel: Long, + ): PersistentList { + return buildList { + add(ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true)) + // Assume the member is a regular user when it's unknown + val targetMemberPowerLevel = member?.powerLevel ?: 0 + val canModerateThisUser = currentUserMemberPowerLevel > targetMemberPowerLevel + // Assume the member is joined when it's unknown + val membership = member?.membership ?: RoomMembershipState.JOIN + if (canKick) { + val isKickEnabled = canModerateThisUser && membership.isActive() + add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = isKickEnabled)) + } + if (canBan) { + if (membership == RoomMembershipState.BAN) { + add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser)) + } else { + add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser)) + } + } + }.toPersistentList() + } + + private fun CoroutineScope.kickUser( + userId: UserId, + reason: String, + kickUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(kickUserAction) { + analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember)) + room.kickUser( + userId = userId, + reason = reason.takeIf { it.isNotBlank() }, + ) + } + + private fun CoroutineScope.banUser( + userId: UserId, + reason: String, + banUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(banUserAction) { + analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember)) + room.banUser( + userId = userId, + reason = reason.takeIf { it.isNotBlank() }, + ) + } + + private fun CoroutineScope.unbanUser( + userId: UserId, + unbanUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(unbanUserAction) { + analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember)) + room.unbanUser(userId = userId) + } + + private fun CoroutineScope.runActionAndWaitForMembershipChange( + action: MutableState>, + block: suspend () -> Result + ) { + launch(dispatchers.io) { + action.runUpdatingState { + val result = block() + if (result.isSuccess) { + room.membersStateFlow.drop(1).take(1) + } + result + } + } + } +} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt new file mode 100644 index 00000000000..d4b7a7b69e8 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -0,0 +1,337 @@ +/* + * 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.roommembermoderation.impl + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +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.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.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState +import io.element.android.libraries.architecture.AsyncAction +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.Avatar +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.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.TextFieldDialog +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.Text +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 +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.launch +import timber.log.Timber + +@Composable +fun RoomMemberModerationView( + state: InternalRoomMemberModerationState, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + val selectedUser = state.selectedUser + if (selectedUser != null && state.canDisplayActions) { + RoomMemberActionsBottomSheet( + user = selectedUser, + actions = state.actions, + onSelectAction = onSelectAction, + onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + ) + } + RoomMemberAsyncActions(state = state) + } +} + +@Composable +private fun RoomMemberAsyncActions( + state: InternalRoomMemberModerationState, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + val selectedUser = state.selectedUser + val asyncIndicatorState = rememberAsyncIndicatorState() + AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState) + + when (val action = state.kickUserAsyncAction) { + is AsyncAction.Confirming -> { + TextFieldDialog( + title = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_title), + submitText = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_action), + onSubmit = { reason -> + state.eventSink(InternalRoomMemberModerationEvents.DoKickUser(reason = reason)) + }, + onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + placeholder = stringResource(id = CommonStrings.common_reason), + label = stringResource(id = CommonStrings.common_reason), + content = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_description), + value = "", + ) + } + is AsyncAction.Loading -> { + LaunchedEffect(action) { + val userDisplayName = selectedUser?.getBestName().orEmpty() + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_removing_user, userDisplayName)) + } + } + } + is AsyncAction.Failure -> { + Timber.e(action.error, "Failed to kick user.") + LaunchedEffect(action) { + asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { + AsyncIndicator.Failure( + text = stringResource(CommonStrings.common_failed), + ) + } + } + } + is AsyncAction.Success -> { + LaunchedEffect(action) { asyncIndicatorState.clear() } + } + else -> Unit + } + + when (val action = state.banUserAsyncAction) { + is AsyncAction.Confirming -> { + TextFieldDialog( + title = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_title), + submitText = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_action), + onSubmit = { reason -> + state.eventSink(InternalRoomMemberModerationEvents.DoBanUser(reason = reason)) + }, + onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + placeholder = stringResource(id = CommonStrings.common_reason), + label = stringResource(id = CommonStrings.common_reason), + content = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_description), + value = "", + ) + } + is AsyncAction.Loading -> { + LaunchedEffect(action) { + val userDisplayName = selectedUser?.getBestName().orEmpty() + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_banning_user, userDisplayName)) + } + } + } + is AsyncAction.Failure -> { + Timber.e(action.error, "Failed to ban user.") + LaunchedEffect(action) { + asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { + AsyncIndicator.Failure( + text = stringResource(CommonStrings.common_failed), + ) + } + } + } + is AsyncAction.Success -> { + LaunchedEffect(action) { asyncIndicatorState.clear() } + } + else -> Unit + } + when (val action = state.unbanUserAsyncAction) { + is AsyncAction.Confirming -> { + ConfirmationDialog( + title = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_title), + content = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_description), + submitText = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_action), + onSubmitClick = { + val userDisplayName = selectedUser?.getBestName().orEmpty() + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_unbanning_user, userDisplayName)) + } + state.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser) + }, + onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + ) + } + is AsyncAction.Failure -> { + Timber.e(action.error, "Failed to unban user.") + LaunchedEffect(action) { + asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { + AsyncIndicator.Failure( + text = stringResource(CommonStrings.common_failed), + ) + } + } + } + is AsyncAction.Success -> { + LaunchedEffect(action) { asyncIndicatorState.clear() } + } + is AsyncAction.Loading, + AsyncAction.Uninitialized -> Unit + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RoomMemberActionsBottomSheet( + user: MatrixUser, + actions: ImmutableList, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, + onDismiss: () -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + modifier = Modifier.systemBarsPadding(), + sheetState = bottomSheetState, + onDismissRequest = { + coroutineScope.launch { + bottomSheetState.hide() + onDismiss() + } + }, + ) { + Column( + modifier = Modifier.padding(vertical = 16.dp) + ) { + Avatar( + avatarData = user.getAvatarData(size = AvatarSize.RoomListManageUser), + avatarType = AvatarType.User, + modifier = Modifier + .padding(bottom = 28.dp) + .align(Alignment.CenterHorizontally) + ) + user.displayName?.let { + Text( + text = it, + style = ElementTheme.typography.fontHeadingLgBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + .fillMaxWidth() + ) + } + Text( + text = user.userId.toString(), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + ) + Spacer(modifier = Modifier.height(32.dp)) + + for (actionState in actions) { + when (val action = actionState.action) { + is ModerationAction.DisplayProfile -> { + ListItem( + style = ListItemStyle.Primary, + headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_member_user_info)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())), + onClick = { + coroutineScope.launch { + onSelectAction(action, user) + bottomSheetState.hide() + } + }, + enabled = actionState.isEnabled + ) + } + is ModerationAction.KickUser -> { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_remove)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())), + style = ListItemStyle.Destructive, + onClick = { + coroutineScope.launch { + bottomSheetState.hide() + onSelectAction(action, user) + } + }, + enabled = actionState.isEnabled + ) + } + is ModerationAction.BanUser -> { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_ban)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), + style = ListItemStyle.Destructive, + onClick = { + coroutineScope.launch { + bottomSheetState.hide() + onSelectAction(action, user) + } + }, + enabled = actionState.isEnabled + ) + } + is ModerationAction.UnbanUser -> { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_unban)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Restart())), + style = ListItemStyle.Destructive, + onClick = { + coroutineScope.launch { + bottomSheetState.hide() + onSelectAction(action, user) + } + }, + enabled = actionState.isEnabled + ) + } + } + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun RoomMemberModerationViewPreview(@PreviewParameter(InternalRoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) { + ElementPreview { + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 64.dp) + ) { + RoomMemberModerationView( + state = state, + onSelectAction = { _, _ -> + }, + ) + } + } +} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt new file mode 100644 index 00000000000..d2a5296b951 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt @@ -0,0 +1,23 @@ +/* + * 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.roommembermoderation.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +import io.element.android.features.roommembermoderation.impl.RoomMemberModerationPresenter +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.RoomScope + +@ContributesTo(RoomScope::class) +@Module +interface RoomMemberModerationModule { + @Binds + fun bindRoomMemberModerationPresenter(presenter: RoomMemberModerationPresenter): Presenter +} diff --git a/features/roommembermoderation/impl/src/main/res/values-be/translations.xml b/features/roommembermoderation/impl/src/main/res/values-be/translations.xml new file mode 100644 index 00000000000..bd744e96e74 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-be/translations.xml @@ -0,0 +1,12 @@ + + + "Выдаліць і заблакіраваць удзельніка" + "Заблакіраваць" + "Яны не змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." + "Вы ўпэўнены, што хочаце заблакіраваць гэтага карыстальніка?" + "Блакіроўка %1$s" + "Прагляд профілю" + "Выдаліць удзельніка з пакоя" + "Выдаліць удзельніка і забараніць далучацца ў будучыні?" + "Выдаленне %1$s…" + diff --git a/features/roommembermoderation/impl/src/main/res/values-bg/translations.xml b/features/roommembermoderation/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 00000000000..53955d5a039 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,4 @@ + + + "Преглед на профила" + diff --git a/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml b/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 00000000000..2c5e51972ea --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,20 @@ + + + "Odebrat a vykázat člena" + "Vykázat" + "Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni." + "Jste si jisti, že chcete vykázat tohoto člena?" + "Vykazování %1$s" + "Odebrat" + "Budou moci znovu vstoupit do této místnosti, pokud budou pozváni." + "Opravdu chcete tohoto člena odebrat?" + "Zobrazit profil" + "Odebrat z místnosti" + "Odebrat člena a zakázat mu připojení v budoucnu?" + "Odstraňování %1$s…" + "Zrušit vykázání z místnosti" + "Zrušit vykázání" + "Pokud by byli pozváni, mohli by se znovu připojit do místnosti" + "Opravdu chcete zrušit vykázání tohoto člena?" + "Rušení vykázání %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml b/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..cfa26c6e9bc --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,20 @@ + + + "Gwahardd o ystafell" + "Atal" + "Fyddan nhw ddim yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad." + "Ydych chi\'n siŵr eich bod am wahardd yr aelod hwn?" + "Yn gwahardd %1$s" + "Tynnu" + "Fyddan nhw yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad." + "Ydych chi\'n siŵr eich bod am ddileu\'r aelod hwn?" + "Gweld proffil" + "Tynnu o\'r ystafell" + "Dileu aelod a\'u gwahardd rhag ymuno yn y dyfodol?" + "Wrthi\'n dileu %1$s…" + "Dad-wahardd o\'r ystafell" + "Dad-wahardd" + "Bydden nhw\'n gallu ymuno â\'r ystafell eto os fydd rhywun yn eu gwahodd" + "Ydych chi\'n siŵr eich bod chi eisiau dadwahardd yr aelod hwn?" + "Dad-wahardd %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-da/translations.xml b/features/roommembermoderation/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..00defeeec60 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,20 @@ + + + "Spær fra rum" + "Spær" + "De vil ikke være i stand til at deltage i dette rum igen, selv om de inviteres." + "Er du stikker på, at du ønsker at spærre dette medlem?" + "Spærrer %1$s" + "Fjern" + "De vil være i stand til at deltage i dette rum igen, hvis de inviteres." + "Er du sikker på, at du vil fjerne dette medlem?" + "Se profil" + "Fjern fra rummet" + "Fjern medlem og udeluk dem fra at deltage i fremtiden?" + "Fjerner %1$s…" + "Fjern brugerens spærring fra rummet" + "Fjern spærring af" + "De ville være i stand til at deltage i rummet igen, hvis de blev inviteret" + "Er du sikker på, at du vil fjerne spærringen af dette medlem?" + "Ophæver spærring af %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-de/translations.xml b/features/roommembermoderation/impl/src/main/res/values-de/translations.xml new file mode 100644 index 00000000000..3ded15cd8e7 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,20 @@ + + + "Mitglied entfernen und sperren" + "Sperren" + "Sie können dem Raum nicht mehr beitreten, selbst wenn sie eingeladen werden." + "Möchten Sie diesen Nutzer wirklich sperren?" + "%1$s wird gesperrt." + "Entfernen" + "Sie können diesen Raum wieder betreten, wenn sie eingeladen werden." + "Möchten Sie dieses Mitglied wirklich entfernen?" + "Nutzerprofil anzeigen" + "Mitglied entfernen" + "Mitglied entfernen und für die Zukunft sperren?" + "%1$s wird entfernt." + "Sperre für diesen Chatroom aufheben" + "Sperre aufheben" + "Sie könnten den Chatroom wieder betreten, wenn sie wieder eingeladen würden." + "Möchten Sie die Sperre dieses Mitglieds wirklich aufheben?" + "Sperre für %1$s aufheben" + diff --git a/features/roommembermoderation/impl/src/main/res/values-el/translations.xml b/features/roommembermoderation/impl/src/main/res/values-el/translations.xml new file mode 100644 index 00000000000..2ed8f03bc44 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-el/translations.xml @@ -0,0 +1,20 @@ + + + "Αφαίρεση και αποκλεισμός μέλους" + "Αποκλεισμός" + "Δεν θα μπορούν να ενταχθούν ξανά σε αυτή την αίθουσα, αν προσκληθούν." + "Θες σίγουρα να αποκλείσεις αυτό το μέλος;" + "Αποκλεισμός %1$s" + "Αφαίρεση" + "Θα μπορούν να συμμετάσχουν ξανά σε αυτή την αίθουσα, εάν προσκληθούν." + "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε αυτό το μέλος;" + "Προβολή προφίλ" + "Αφαίρεση από την αίθουσα" + "Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;" + "Αφαίρεση %1$s…" + "Άρση αποκλεισμού από την αίθουσα" + "Άρση αποκλεισμού" + "Θα μπορούν να συμμετάσχουν και πάλι στην αίθουσα αν προσκληθούν" + "Σίγουρα θες να καταργήσεις τον αποκλεισμό αυτού του μέλους;" + "Άρση αποκλεισμού %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-es/translations.xml b/features/roommembermoderation/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..04049e9e31b --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,20 @@ + + + "Sacar y vetar a un miembro" + "Vetar" + "No podrán volver a unirse a esta sala si son invitados." + "¿Estás seguro de que quieres vetar a este miembro?" + "Vetando a %1$s" + "Echar" + "Podrá volver a unirse a esta sala si se le invita." + "¿Seguro que quieres echar a este miembro?" + "Ver perfil" + "Sacar de la sala" + "¿Sacar al miembro y prohibirle unirse en el futuro?" + "Eliminando %1$s…" + "Eliminar veto en la sala" + "Eliminar veto" + "Podría volver a unirse a la sala si se le invita" + "¿Seguro que quieres levantarle el veto a este miembro?" + "Levantando veto a %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-et/translations.xml b/features/roommembermoderation/impl/src/main/res/values-et/translations.xml new file mode 100644 index 00000000000..ffb1d79f882 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-et/translations.xml @@ -0,0 +1,20 @@ + + + "Eemalda ja sea suhtluskeeld" + "Sea suhtluskeeld" + "Ta ei saa selle jututoaga liituda isegi kutse olemasolul." + "Kas sa oled kindel, et soovid sellele kasutajale seada suhtluskeelu?" + "Seame kasutajale %1$s suhtluskeelu" + "Eemalda" + "Uue kutse saamisel on tal võimalik selle jututoaga uuesti liituda." + "Kas sa oled kindel, et soovid selle osaleja eemaldada?" + "Vaata profiili" + "Eemalda kasutaja jututoast" + "Kas eemaldama kasutaja ja seame talle tulevikuks suhtluskeelu?" + "Eemaldame kasutajat %1$s…" + "Eemalda suhtluskeeld jututoas" + "Eemalda suhtluskeeld" + "Ta võib kutse saamisel liituda jututoaga uuesti" + "Kas oled kindel, et soovid selle liikme suhtluskeelu eemaldada?" + "Eemaldame suhtluskeelu kasutajalt %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml b/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..c49ae2d4d5f --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,11 @@ + + + "Kendu kidea eta ezarri debekua" + "Ezarri debekua" + "Ziur kide honi debekua ezarri nahi diozula?" + "%1$s(r)i debekua ezartzen" + "Ikusi profila" + "Kendu gelatik" + "Kidea kendu eta etorkizunean sartzea debekatu?" + "%1$s kentzen…" + diff --git a/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml b/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml new file mode 100644 index 00000000000..a51b49b2320 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,15 @@ + + + "برداشت و تحریم عضو" + "تحریم" + "در صورت دعوت نمی‌تواند دوباره به اتاق بپیوندد." + "مطمئنید می‌خواهید این عضو را تحریم کنید؟" + "تحریم کردن %1$s" + "برداشتن" + "در صورت دعوت می‌تواند دوباره به اتاق بپیوندد." + "مطمئنید می‌خواهید این عضو را بردارید؟" + "دیدن نمایه" + "برداشتن از اتاق" + "برداشتن عضو و تحریم پیوستن در آینده؟" + "برداشتن %1$s…" + diff --git a/features/roommembermoderation/impl/src/main/res/values-fi/translations.xml b/features/roommembermoderation/impl/src/main/res/values-fi/translations.xml new file mode 100644 index 00000000000..4c93f737802 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-fi/translations.xml @@ -0,0 +1,20 @@ + + + "Poista jäsen huoneesta ja anna porttikielto" + "Anna porttikielto" + "He eivät voi enää liittyä tähän huoneeseen, jos heidät kutsutaan." + "Haluatko varmasti antaa tälle jäsenelle porttikiellon?" + "Annetaan porttikieltoa käyttäjälle %1$s" + "Poista" + "He voivat liittyä tähän huoneeseen uudelleen, jos heidät kutsutaan." + "Haluatko varmasti poistaa tämän jäsenen?" + "Näytä profiili" + "Poista huoneesta" + "Poistetaanko jäsen huoneesta ja kielletäänkö heitä liittymästä tulevaisuudessa?" + "Poistetaan käyttäjää %1$s huoneesta…" + "Poista porttikielto huoneesta" + "Poista porttikielto" + "He voivat liittyä huoneeseen uudelleen, jos heidät kutsutaan" + "Haluatko varmasti poistaa tämän jäsenen porttikiellon?" + "Poistetaan käyttäjän %1$s porttikieltoa" + diff --git a/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml b/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 00000000000..5a09dddf4eb --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,20 @@ + + + "Bannir du salon" + "Bannir" + "Il ne pourra pas rejoindre le salon à nouveau, même si il est invité." + "Êtes-vous certain de vouloir bannir ce membre ?" + "Bannissement de %1$s" + "Retirer" + "Cet utilisateur pourra rejoindre le salon à nouveau si il est invité." + "Voulez-vous vraiment supprimer ce membre ?" + "Voir le profil" + "Retirer le membre du salon" + "Retirer le membre et interdire l’adhésion à l’avenir ?" + "Enlever %1$s…" + "Débannir du salon" + "Débannir" + "L’utilisateur pourra à nouveau rejoindre le salon s’il est invité." + "Êtes-vous sûr de vouloir débannir cet utilisateur?" + "Débannissement de %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml b/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 00000000000..db36f419643 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,20 @@ + + + "Eltávolítás és a tag kitiltása" + "Kitiltás" + "Többé nem csatlakozhat ehhez a szobához, akkor sem, ha meghívják." + "Biztos, hogy kitiltja ezt a tagot?" + "%1$s kitiltása" + "Eltávolítás" + "Ehhez a szobához is csatlakozhat, ha meghívják." + "Biztos, hogy eltávolítja ezt a tagot?" + "Profil megtekintése" + "Eltávolítás a szobából" + "Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?" + "%1$s eltávolítása…" + "Visszaengedés a szobába" + "Kitiltás visszavonása" + "Újra beléphetnek a szobába, ha meghívják őket." + "Biztos, hogy feloldja a felhasználó kitiltását?" + "%1$s kitiltásának feloldása" + diff --git a/features/roommembermoderation/impl/src/main/res/values-in/translations.xml b/features/roommembermoderation/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..778f68402c6 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,20 @@ + + + "Keluarkan dan cekal anggota" + "Cekal" + "Mereka tidak akan dapat bergabung ke ruangan ini lagi jika diundang." + "Apakah Anda yakin ingin mencekal anggota ini?" + "Mencekal %1$s" + "Hapus" + "Pengguna dapat bergabung dalam ruangan ini lagi jika diundang." + "Apakah Anda yakin ingin menghapus anggota ini?" + "Tampilkan profil" + "Keluarkan dari ruangan" + "Keluarkan pengguna dan cekal pengguna bergabung lagi di masa mendatang?" + "Mengeluarkan %1$s…" + "Batalkan cekalan dari ruangan" + "Batalkan pencekalan" + "Mereka akan dapat bergabung dengan ruangan lagi jika diundang" + "Apakah Anda yakin ingin membatalkan pencekalan anggota ini?" + "Membatalkan cekalan %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-it/translations.xml b/features/roommembermoderation/impl/src/main/res/values-it/translations.xml new file mode 100644 index 00000000000..41b8baa4730 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,20 @@ + + + "Rimuovi ed escludi" + "Escludi" + "Non potrà entrare nuovamente in questa stanza se invitato." + "Vuoi davvero escludere questo membro?" + "Esclusione di %1$s" + "Rimuovi" + "Potrà entrare nuovamente in questa stanza se invitato." + "Sei sicuro di voler rimuovere questo membro?" + "Visualizza profilo" + "Rimuovi dalla stanza" + "Rimuovere e vietare l\'accesso in futuro?" + "Rimozione di %1$s…" + "Riammetti nella stanza" + "Togli ban" + "Potranno unirsi di nuovo alla stanza se invitati" + "Sei sicuro di voler sbloccare questo membro?" + "Rimuovendo il ban di %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml new file mode 100644 index 00000000000..a2eb6d4bad1 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml @@ -0,0 +1,12 @@ + + + "წევრის წაშლა და დაბლოკვა" + "დაბლოკვა" + "მოწვევის შემთხვევაში ამ ოთახში კვლავ გაწევრიანებას ვერ შეძლებენ." + "დარწმუნებული ხართ, რომ ამ წევრის დაბლოკვა გსურთ?" + "%1$s-ს დაბლოკვა" + "პროფილის ნახვა" + "ოთახიდან გაგდება" + "გსურთ წევრის გაგდება და მომავალში გაწევრიანების აკრძალვა?" + "%1$s-ს გაგდება…" + diff --git a/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml b/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..323526170b9 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,20 @@ + + + "Fjern og utesteng medlem" + "Utesteng" + "De vil ikke kunne bli med i dette rommet igjen hvis de blir invitert." + "Er du sikker på at du vil utestenge dette medlemmet?" + "Utestenger %1$s" + "Fjern" + "De vil kunne bli med i dette rommet igjen hvis de blir invitert." + "Er du sikker på at du vil fjerne dette medlemmet?" + "Vis profil" + "Fjern fra rommet" + "Fjerne medlem og utestenge fra å bli med i fremtiden?" + "Fjerner %1$s…" + "Fjern utestengelsen fra rommet" + "Opphev utestengelsen" + "De vil kunne bli med i rommet igjen hvis de blir invitert" + "Er du sikker på at du vil oppheve utestengelsen av dette medlemmet?" + "Oppheve utestengelsen av %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml b/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml new file mode 100644 index 00000000000..64317b83bff --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,12 @@ + + + "Lid verwijderen en verbannen" + "Verbannen" + "Ze kunnen niet meer toetreden tot deze kamer als ze worden uitgenodigd." + "Weet je zeker dat je dit lid wilt verbannen?" + "%1$s verbannen" + "Profiel bekijken" + "Verwijderen uit kamer" + "Lid verwijderen en toekomstige deelname verbieden?" + "%1$s wordt verwijderd…" + diff --git a/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml new file mode 100644 index 00000000000..4b0098c34af --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml @@ -0,0 +1,15 @@ + + + "Usuń i zbanuj członka" + "Zbanuj" + "Nie będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni." + "Czy na pewno chcesz zbanować tego członka?" + "Banowanie %1$s" + "Usuń" + "Będą mogli ponownie dołączyć do pokoju, jeśli zostaną zaproszeni." + "Czy na pewno chcesz usunąć tego członka?" + "Wyświetl profil" + "Usuń z pokoju" + "Usunąć członka i zablokować możliwość dołączenia w przyszłości?" + "Usuwanie %1$s…" + diff --git a/features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml b/features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..6d6f24ef8d1 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,20 @@ + + + "Remover e banir membro" + "Banir" + "Eles não poderão entrar nesta sala novamente se forem convidados." + "Tem certeza de que quer banir este membro?" + "Banindo %1$s" + "Remover" + "Eles poderão entrar nesta sala novamente se forem convidados." + "Tem certeza de que deseja remover este membro?" + "Ver perfil" + "Remover da sala" + "Remover membro e banir de entrar novamente no futuro?" + "Removendo %1$s…" + "Desbanir da sala" + "Desbanir" + "Essa pessoa poderia entrar na sala novamente se for convidada" + "Tem certeza que quer desbanir esse membro?" + "Desbanindo %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml b/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml new file mode 100644 index 00000000000..89a0992c98f --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml @@ -0,0 +1,20 @@ + + + "Remover e banir participante" + "Banir" + "Não poderão voltar a entrar nesta sala, mesmo se forem convidados." + "Tens a certeza que queres banir este participante?" + "A banir %1$s" + "Remover" + "Poderão entrar na sala novamente se convidados." + "Tens certeza que queres remover este membro?" + "Ver perfil" + "Remover da sala" + "Remover participante e proibir que entre no futuro?" + "A remover %1$s…" + "Anular banimento da sala" + "Anular banimento" + "Poderão entrar novamente na sala se forem convidados" + "Tens a certeza que queres anular o banimento deste utilizador?" + "A anular banimento de %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 00000000000..27bb9c23bdd --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,12 @@ + + + "Eliminați și interziceți membrul" + "Interzicere" + "Nu se vor putea alătura din nou acestei camere dacă sunt invitați." + "Sunteți sigur că doriți să interziceți acest membru?" + "Se interzice %1$s" + "Vizualizare profil" + "Înlăturați membrul" + "Înlăturați membrul și interziceți-i să se alăture în viitor?" + "Se elimină %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ru/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 00000000000..6d89f42fe15 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,20 @@ + + + "Удалить и заблокировать участника" + "Заблокировать" + "Они не смогут снова присоединиться к этой комнате, если их пригласят." + "Вы уверены, что хотите заблокировать этого участника?" + "Блокировка %1$s" + "Удалить" + "Они снова смогут присоединиться в эту комнату если их пригласят." + "Вы действительно хотите удалить этого участника?" + "Посмотреть профиль" + "Удалить участника из комнаты" + "Удалить участника и запретить присоединяться в будущем?" + "Удаление %1$s…" + "Разблокировать в комнате" + "Разблокировать" + "Они смогут снова войти в комнату, если их пригласят." + "Вы действительно хотите разблокировать этого участника?" + "Разблокировка %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml b/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 00000000000..df5214c17c6 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,20 @@ + + + "Odstrániť a zakázať člena" + "Zakázať" + "Nebudú sa môcť pripojiť k tejto miestnosti znova ani ak budú pozvaní." + "Ste si istý, že chcete zakázať tohto člena?" + "Zakazuje sa %1$s" + "Odstrániť" + "V prípade pozvania sa budú môcť znova pripojiť k tejto miestnosti." + "Ste si istý, že chcete odstrániť tohto člena?" + "Zobraziť profil" + "Odstrániť z miestnosti" + "Odstrániť člena a zakázať vstup v budúcnosti?" + "Odstraňuje sa %1$s…" + "Zrušiť zákaz prístupu do miestnosti" + "Zrušiť zákaz" + "V prípade pozvania by sa mohli opäť pripojiť k miestnosti" + "Naozaj chcete zrušiť zablokovanie tohto člena?" + "Zrušenie zákazu pre %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-sv/translations.xml b/features/roommembermoderation/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 00000000000..9f3aabe1910 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,20 @@ + + + "Ta bort och banna medlem" + "Banna" + "Denne kommer inte att kunna gå med i det här rummet igen om denne bjuds in." + "Är du säker på att du vill banna den här medlemmen?" + "Bannar %1$s" + "Ta bort" + "Denne kommer kunna gå med i rummet igen om denne bjuds in" + "Är du säker på att du vill ta bort den här medlemmen?" + "Visa profil" + "Ta bort från rummet" + "Ta bort medlem och banna från att gå med i framtiden?" + "Tar bort %1$s …" + "Avbanna från rummet" + "Avbanna" + "De skulle kunna gå med i rummet igen om de blev inbjudna" + "Är du säker på att du vill avbanna den här medlemmen?" + "Avbannar %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml b/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..73f17edd028 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,12 @@ + + + "Üyeyi çıkar ve yasakla" + "Yasakla" + "Davet edilseler bile bu odaya tekrar katılamazlar." + "Bu üyeyi yasaklamak istediğinize emin misiniz?" + "Yasaklanıyor %1$s" + "Profili görüntüle" + "Odadan çıkar" + "Üyeyi çıkarın ve gelecekte katılmasını yasaklayın?" + "Kaldırılıyor %1$s…" + diff --git a/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml b/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 00000000000..6c48b0bde00 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,20 @@ + + + "Вилучити й заблокувати учасника" + "Заблокувати" + "Він не зможе приєднатися до цієї кімнати знову, якщо його запросять." + "Ви точно хочете заблокувати цього користувача?" + "Блокування %1$s" + "Вилучити" + "Вони зможуть знову приєднатися до цієї кімнати, якщо їх запросять." + "Ви дійсно хочете вилучити цього учасника?" + "Переглянути профіль" + "Вилучити з кімнати" + "Вилучити учасника та заборонити приєднання в майбутньому?" + "Вилучення %1$s…" + "Розблокувати в кімнаті" + "Розблокувати" + "Вони зможуть знову приєднатися до кімнати, якщо їх запросять" + "Ви впевнені, що хочете розблокувати цього учасника?" + "Розблокування %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..333c86f0564 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,12 @@ + + + "کمرے سے محظور کریں" + "محظور کریں" + "اگر وہ مدعو کیا گیا تو وہ دوبارہ اس کمرے میں شامل نہیں ہوسکیں گے۔" + "کیا آپ کو یقین ہے کہ آپ اس رکن کو محظور کرنا چاہتے ہیں؟" + "%1$s کو محظور کر رہا ہے" + "نمایہ ملاحظہ کریں" + "کمرے سے ہٹائیں" + "رکن کو ہٹائیں اور مستقبل میں شمولیت پر پابندی لگائیں؟" + "%1$s کو ہٹا رہا ہے…" + diff --git a/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml b/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 00000000000..152e2d3a8f5 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,15 @@ + + + "踢出並加入黑名單" + "加入黑名單" + "即使收到邀請,他們仍然無法加入聊天室。" + "您確定要將此成員加入黑名單?" + "正在將 %1$s 加入黑名單" + "移除" + "若收到邀請,他們可以再次加入此聊天室。" + "您真的想要移除此成員嗎?" + "查看個人檔案" + "踢出聊天室" + "移除成員並禁止未來再度加入?" + "正在踢出 %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 new file mode 100644 index 00000000000..20c002aa76c --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml @@ -0,0 +1,15 @@ + + + "移除并封禁成员" + "封禁" + "即使受到邀请,他们也无法再次加入聊天室。" + "您确定要封禁该成员吗?" + "封禁 %1$s" + "移除" + "如果受到邀请,他们可以重新加入聊天室。" + "您确定要移除此成员吗?" + "查看个人资料" + "从聊天室移除" + "删除成员并禁止重新加入?" + "正在移除 %1$s……" + diff --git a/features/roommembermoderation/impl/src/main/res/values/localazy.xml b/features/roommembermoderation/impl/src/main/res/values/localazy.xml new file mode 100644 index 00000000000..16b013d5376 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values/localazy.xml @@ -0,0 +1,20 @@ + + + "Ban from room" + "Ban" + "They won’t be able to join this room again if invited." + "Are you sure you want to ban this member?" + "Banning %1$s" + "Remove" + "They will be able to join this room again if invited." + "Are you sure you want to remove this member?" + "View profile" + "Remove from room" + "Remove member and ban from joining in the future?" + "Removing %1$s…" + "Unban from room" + "Unban" + "They would be able to join the room again if invited" + "Are you sure you want to unban this member?" + "Unbanning %1$s" + diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt new file mode 100644 index 00000000000..3496612c357 --- /dev/null +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt @@ -0,0 +1,360 @@ +/* + * 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.roommembermoderation.impl + +import app.cash.turbine.TurbineTestContext +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +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.RoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.test.A_USER_ID +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.aRoomMember +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class RoomMemberModerationPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + private val targetUser = MatrixUser(userId = A_USER_ID) + + @Test + fun `present - initial state`() = runTest { + val room = aJoinedRoom() + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + assertThat(initialState.canKick).isFalse() + assertThat(initialState.canBan).isFalse() + assertThat(initialState.selectedUser).isNull() + assertThat(initialState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.unbanUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.actions).isEmpty() + } + } + + @Test + fun `present - show actions when canBan=false, canKick=false`() = runTest { + val room = aJoinedRoom( + canBan = false, + canKick = false, + myUserRole = RoomMember.Role.USER, + targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel) + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(1) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ) + } + } + + @Test + fun `present - show actions when canBan=true, canKick=true, userRole=Admin and target member is unknown`() = runTest { + val room = aJoinedRoom( + canBan = true, + canKick = true, + myUserRole = RoomMember.Role.ADMIN, + targetRoomMember = null + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(2) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = true), + ModerationActionState(action = ModerationAction.BanUser, isEnabled = true), + ) + } + } + + @Test + fun `show actions when canBan=true, canKick=true, userRole=Admin and target is User`() = runTest { + val room = aJoinedRoom( + canBan = true, + canKick = true, + myUserRole = RoomMember.Role.ADMIN, + targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel) + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(2) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = true), + ModerationActionState(action = ModerationAction.BanUser, isEnabled = true), + ) + } + } + + @Test + fun `show actions when canBan=true, canKick=true, userRole=Moderator and target is Admin`() = runTest { + val room = aJoinedRoom( + canBan = true, + canKick = true, + myUserRole = RoomMember.Role.MODERATOR, + targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.ADMIN.powerLevel) + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(2) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ModerationActionState(action = ModerationAction.BanUser, isEnabled = false), + ) + } + } + + @Test + fun `show actions when canBan=true, canKick=true, userRole=Moderator and target is Banned`() = runTest { + val room = aJoinedRoom( + canBan = true, + canKick = true, + myUserRole = RoomMember.Role.MODERATOR, + targetRoomMember = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.BAN) + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(2) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = true), + ) + } + } + + @Test + fun `present - process kick action sets confirming state`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.KickUser + ) + ) + skipItems(1) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.kickUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams) + } + } + + @Test + fun `present - process ban action sets confirming state`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.BanUser + ) + ) + skipItems(1) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.banUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams) + } + } + + @Test + fun `present - process unban action sets confirming state`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.UnbanUser + ) + ) + skipItems(1) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.unbanUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams) + } + } + + @Test + fun `present - do kick user with success`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.KickUser + ) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason")) + skipItems(1) + val loadingState = awaitState() + assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + val successState = awaitState() + assertThat(successState.kickUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java) + assertThat(successState.selectedUser).isNull() + } + } + + @Test + fun `present - do ban user with success`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.BanUser + ) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.DoBanUser("Reason")) + skipItems(1) + val loadingState = awaitState() + assertThat(loadingState.banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + val successState = awaitState() + assertThat(successState.banUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java) + assertThat(successState.selectedUser).isNull() + } + } + + @Test + fun `present - do unban user with success`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.UnbanUser + ) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser) + skipItems(1) + val loadingState = awaitState() + assertThat(loadingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + val successState = awaitState() + assertThat(successState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java) + assertThat(successState.selectedUser).isNull() + } + } + + @Test + fun `present - do kick user with failure`() = runTest { + val error = RuntimeException("Test error") + val room = aJoinedRoom( + kickUserResult = Result.failure(error), + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.KickUser + ) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason")) + skipItems(1) + val loadingState = awaitState() + assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + val failureState = awaitState() + assertThat(failureState.kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) + } + } + + @Test + fun `present - reset clears all async actions and selected user`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction(targetUser = targetUser, action = ModerationAction.BanUser) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.Reset) + skipItems(1) + val resetState = awaitState() + assertThat(resetState.selectedUser).isNull() + assertThat(resetState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + private fun aJoinedRoom( + canKick: Boolean = false, + canBan: Boolean = false, + myUserRole: RoomMember.Role = RoomMember.Role.USER, + kickUserResult: Result = Result.success(Unit), + banUserResult: Result = Result.success(Unit), + unBanUserResult: Result = Result.success(Unit), + targetRoomMember: RoomMember? = null, + ): JoinedRoom { + return FakeJoinedRoom( + kickUserResult = { _, _ -> kickUserResult }, + banUserResult = { _, _ -> banUserResult }, + unBanUserResult = { _, _ -> unBanUserResult }, + baseRoom = FakeBaseRoom( + canBanResult = { _ -> Result.success(canBan) }, + canKickResult = { _ -> Result.success(canKick) }, + userRoleResult = { Result.success(myUserRole) }, + ), + ).apply { + val roomMembers = listOfNotNull(targetRoomMember).toPersistentList() + givenRoomMembersState(state = RoomMembersState.Ready(roomMembers)) + } + } + + private fun TestScope.createRoomMemberModerationPresenter( + room: JoinedRoom, + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + analyticsService: AnalyticsService = FakeAnalyticsService(), + ): RoomMemberModerationPresenter { + return RoomMemberModerationPresenter( + room = room, + dispatchers = dispatchers, + analyticsService = analyticsService, + ) + } + + private suspend fun TurbineTestContext.awaitState(): InternalRoomMemberModerationState { + return awaitItem() as InternalRoomMemberModerationState + } +} 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 new file mode 100644 index 00000000000..fc1b8155622 --- /dev/null +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.roommembermoderation.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.testtags.TestTags +import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams +import io.element.android.tests.testutils.pressTag +import io.element.android.tests.testutils.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`() { + val user = anAlice() + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithTwoParams(ModerationAction.DisplayProfile, user) { callback -> + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = user, + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ), + eventSink = eventsRecorder + ), + onSelectAction = callback + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) + } + } + + @Test + fun `clicking on kick user action calls onSelectAction`() { + val user = anAlice() + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithTwoParams(ModerationAction.KickUser, user) { callback -> + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = user, + actions = listOf( + ModerationActionState(action = ModerationAction.KickUser, isEnabled = true), + ), + eventSink = eventsRecorder + ), + onSelectAction = callback + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + // Gives time for bottomsheet to hide + rule.mainClock.advanceTimeBy(1_000) + } + } + + @Test + fun `clicking on ban user action calls onSelectAction`() { + val user = anAlice() + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithTwoParams(ModerationAction.BanUser, user) { callback -> + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = user, + actions = listOf( + ModerationActionState(action = ModerationAction.BanUser, isEnabled = true), + ), + eventSink = eventsRecorder + ), + onSelectAction = callback + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_ban) + // Gives time for bottomsheet to hide + rule.mainClock.advanceTimeBy(1_000) + } + } + + @Test + fun `clicking on unban user action calls onSelectAction`() { + val user = anAlice() + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithTwoParams(ModerationAction.UnbanUser, user) { callback -> + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = user, + actions = listOf( + ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = true), + ), + eventSink = eventsRecorder + ), + onSelectAction = callback + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_unban) + // Gives time for bottomsheet to hide + rule.mainClock.advanceTimeBy(1_000) + } + } + + @Test + fun `clicking submit on kick confirmation dialog sends DoKickUser event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + kickUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogPositive.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoKickUser(reason = "")) + } + + @Test + fun `clicking dismiss on kick confirmation dialog sends Reset event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + kickUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogNegative.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) + } + + @Test + fun `clicking submit on ban confirmation dialog sends DoBanUser event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + banUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogPositive.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoBanUser(reason = "")) + } + + @Test + fun `clicking dismiss on ban confirmation dialog sends Reset event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + banUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogNegative.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) + } + + @Test + fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogPositive.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser) + } + + @Test + fun `clicking dismiss on unban confirmation dialog sends Reset event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogNegative.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) + } + + @Test + fun `disabled actions are not clickable`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ), + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + } +} + +private fun AndroidComposeTestRule.setRoomMemberModerationView( + state: InternalRoomMemberModerationState, + onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(), +) { + setSafeContent { + RoomMemberModerationView( + state = state, + onSelectAction = onSelectAction, + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt index b5337637350..6a46431c350 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt @@ -8,10 +8,22 @@ package io.element.android.features.securebackup.impl.enter import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -25,6 +37,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds @Composable fun SecureBackupEnterRecoveryKeyView( @@ -55,12 +70,30 @@ fun SecureBackupEnterRecoveryKeyView( } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun Content( state: SecureBackupEnterRecoveryKeyState, ) { + val bringIntoViewRequester = remember { BringIntoViewRequester() } + var isFocused by remember { mutableStateOf(false) } + val isImeVisible = WindowInsets.isImeVisible + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(isImeVisible, isFocused) { + // When the keyboard is shown, we want to scroll the text field into view + if (isImeVisible && isFocused) { + coroutineScope.launch { + // Delay to ensure the keyboard is fully shown + delay(100.milliseconds) + bringIntoViewRequester.bringIntoView() + } + } + } RecoveryKeyView( - modifier = Modifier.padding(top = 52.dp, bottom = 32.dp), + modifier = Modifier + .onFocusChanged { isFocused = it.isFocused } + .bringIntoViewRequester(bringIntoViewRequester) + .padding(top = 52.dp, bottom = 32.dp), state = state.recoveryKeyViewState, onClick = null, onChange = { 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 5e97f4193da..6ce66d149d6 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 @@ -9,11 +9,11 @@ package io.element.android.features.securebackup.impl.reset import android.app.Activity import android.os.Parcelable +import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -26,6 +26,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.compound.theme.ElementTheme 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 @@ -35,9 +36,9 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle -import io.element.android.libraries.oidc.api.OidcEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest @@ -50,8 +51,8 @@ class ResetIdentityFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val resetIdentityFlowManager: ResetIdentityFlowManager, - private val coroutineScope: CoroutineScope, - private val oidcEntryPoint: OidcEntryPoint, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, ) : BaseFlowNode( backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap), buildContext = buildContext, @@ -67,12 +68,10 @@ class ResetIdentityFlowNode @AssistedInject constructor( @Parcelize data object ResetPassword : NavTarget - - @Parcelize - data class ResetOidc(val url: String) : NavTarget } private lateinit var activity: Activity + private var darkTheme: Boolean = false private var resetJob: Job? = null override fun onBuilt() { @@ -80,9 +79,9 @@ class ResetIdentityFlowNode @AssistedInject constructor( lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { - // If the custom tab was opened, we need to cancel the reset job + // If the custom tab / Web browser was opened, we need to cancel the reset job // when we come back to the node if the reset wasn't successful - coroutineScope.launch { + sessionCoroutineScope.launch { cancelResetJob() resetIdentityFlowManager.whenResetIsDone { @@ -93,7 +92,7 @@ class ResetIdentityFlowNode @AssistedInject constructor( override fun onDestroy(owner: LifecycleOwner) { // Make sure we cancel the reset job when the node is destroyed, just in case - coroutineScope.launch { cancelResetJob() } + sessionCoroutineScope.launch { cancelResetJob() } } }) } @@ -103,7 +102,7 @@ class ResetIdentityFlowNode @AssistedInject constructor( is NavTarget.Root -> { val callback = object : ResetIdentityRootNode.Callback { override fun onContinue() { - coroutineScope.startReset() + sessionCoroutineScope.startReset() } } createNode(buildContext, listOf(callback)) @@ -115,9 +114,6 @@ class ResetIdentityFlowNode @AssistedInject constructor( listOf(ResetIdentityPasswordNode.Inputs(handle)) ) } - is NavTarget.ResetOidc -> { - oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.url) - } } } @@ -135,11 +131,7 @@ class ResetIdentityFlowNode @AssistedInject constructor( Timber.d("No reset handle return, the reset is done.") } is IdentityOidcResetHandle -> { - if (oidcEntryPoint.canUseCustomTab()) { - activity.openUrlInChromeCustomTab(null, false, handle.url) - } else { - backstack.push(NavTarget.ResetOidc(handle.url)) - } + activity.openUrlInChromeCustomTab(null, darkTheme, handle.url) resetJob = launch { handle.resetOidc() } } is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) @@ -160,14 +152,14 @@ class ResetIdentityFlowNode @AssistedInject constructor( override fun View(modifier: Modifier) { // Workaround to get the current activity if (!this::activity.isInitialized) { - activity = LocalContext.current as Activity + activity = requireNotNull(LocalActivity.current) } - + darkTheme = !ElementTheme.isLightTheme val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState() if (startResetState.isLoading()) { ProgressDialog( properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true), - onDismissRequest = { coroutineScope.launch { cancelResetJob() } } + onDismissRequest = { sessionCoroutineScope.launch { cancelResetJob() } } ) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt index 2c118c30494..998b46046ce 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt @@ -33,6 +33,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.Icon import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TextFieldValidity import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -99,7 +100,7 @@ private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Bool Icon(imageVector = image, description) } }, - isError = hasError, + validity = if (hasError) TextFieldValidity.Invalid else TextFieldValidity.None, supportingText = if (hasError) { stringResource(R.string.screen_reset_encryption_password_error) } else { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt index ecd9cd522f4..fc05b6d3167 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt @@ -22,12 +22,13 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -39,7 +40,6 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.tools.RecoveryKeyVisualTransformation -import io.element.android.libraries.designsystem.modifiers.autofill import io.element.android.libraries.designsystem.modifiers.clickableIfNotNull import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -168,7 +168,6 @@ private fun RecoveryKeyWithCopy( } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun RecoveryKeyFormContent( state: RecoveryKeyViewState, @@ -186,10 +185,9 @@ private fun RecoveryKeyFormContent( modifier = Modifier .fillMaxWidth() .testTag(TestTags.recoveryKey) - .autofill( - autofillTypes = listOf(AutofillType.Password), - onFill = { onChange(it) }, - ), + .semantics { + contentType = ContentType.Password + }, minLines = 2, value = state.formattedRecoveryKey.orEmpty(), onValueChange = onChange, diff --git a/features/securebackup/impl/src/main/res/values-be/translations.xml b/features/securebackup/impl/src/main/res/values-be/translations.xml index 3a57f36dede..781ca5af964 100644 --- a/features/securebackup/impl/src/main/res/values-be/translations.xml +++ b/features/securebackup/impl/src/main/res/values-be/translations.xml @@ -2,11 +2,11 @@ "Выключыць рэзервовае капіраванне" "Уключыце рэзервовае капіраванне" - "Рэзервовае капіраванне гарантуе, што вы не страціце сваю гісторыю паведамленняў. %1$s." - "Рэзервовая копія" + "Надзейна захоўвайце вашу крыптаграфічную ідэнтыфікацыю і ключы паведамленняў на серверы. Гэта дазволіць вам праглядаць гісторыю паведамленняў на любых новых прыладах.%1$s ." + "Сховішча ключоў" "Змяніць ключ аднаўлення" "Увядзіце ключ аднаўлення" - "Ваша рэзервовая копія чата зараз не сінхранізавана." + "Ваша сховішча ключоў зараз не сінхранізавана." "Наладзьце аднаўленне" "Атрымайце доступ да зашыфраваных паведамленняў, калі вы страціце ўсе свае прылады або выйдзеце з сістэмы %1$s усюды." "Адкрыйце %1$s на настольнай прыладзе" @@ -31,12 +31,12 @@ "Вы ўпэўнены, што хочаце адключыць рэзервовае капіраванне?" "Атрымайце новы ключ аднаўлення, калі вы страцілі існуючы. Пасля змены ключа аднаўлення ваш стары больш не будзе працаваць." "Стварыць новы ключ аднаўлення" - "Пераканайцеся, што вы можаце захаваць ключ аднаўлення ў бяспечным месцы" + "Ні з кім не дзяліцеся гэтым!" "Ключ аднаўлення зменены" "Змяніць ключ аднаўлення?" "Стварыць новы ключ аднаўлення" "Пераканайцеся, што ніхто не бачыць гэты экран!" - "Паўтарыце спробу, каб пацвердзіць доступ да рэзервовай копіі чата." + "Паўтарыце спробу, каб пацвердзіць доступ да сховішча ключоў." "Няправільны ключ аднаўлення" "Калі ў вас ёсць ключ аднаўлення або парольная фраза, гэта таксама будзе працаваць." "Увесці…" @@ -45,14 +45,14 @@ "Ключ аднаўлення скапіраваны" "Стварэнне…" "Захаваць ключ аднаўлення" - "Запішыце ключ аднаўлення ў бяспечным месцы або захавайце яго ў менеджары пароляў." + "Запішыце гэты ключ аднаўлення ў бяспечным месцы, напрыклад, у менеджэры пароляў, у зашыфраванай нататцы або ў фізічным сейфе." "Націсніце, каб скапіяваць ключ аднаўлення" "Захавайце ключ аднаўлення" "Пасля гэтага кроку вы не зможаце атрымаць доступ да новага ключа аднаўлення." "Вы захавалі свой ключ аднаўлення?" "Рэзервовая копія чата абаронена ключом аднаўлення. Калі пасля настройкі вам спатрэбіцца новы ключ аднаўлення, вы можаце стварыць яго нанова, выбраўшы \"Змяніць ключ аднаўлення\"." "Стварыце ключ аднаўлення" - "Пераканайцеся, што вы можаце захаваць ключ аднаўлення ў бяспечным месцы" + "Ні з кім не дзяліцеся гэтым!" "Наладка аднаўлення прайшла паспяхова" "Наладзьце аднаўленне" "Так, скінуць зараз" diff --git a/features/securebackup/impl/src/main/res/values-bg/translations.xml b/features/securebackup/impl/src/main/res/values-bg/translations.xml index 7dfb012c34f..fe84ddc4a54 100644 --- a/features/securebackup/impl/src/main/res/values-bg/translations.xml +++ b/features/securebackup/impl/src/main/res/values-bg/translations.xml @@ -1,21 +1,32 @@ - "Изключване на резервните копия" + "Изтриване на хранилището за ключове" "Включване на резервните копия" - "Резервното копие гарантира, че няма да загубите хронологията на съобщенията си. %1$s." - "Резервно копие" + "Съхранявайте сигурно криптографската си самоличност и ключовете за съобщения на сървъра. Това ще ви позволи да преглеждате историята на съобщенията си на всички нови устройства.%1$s ." + "Съхранение на ключове" + "За да настроите възстановяването, трябва да включите съхранението на ключове." + "Разрешаване на съхранението на ключове" "Промяна на ключа за възстановяване" + "Възстановете криптографската си самоличност и историята на съобщенията с ключ за възстановяване, ако сте загубили всичките си съществуващи устройства." "Въвеждане на ключ за възстановяване" - "Резервното копие на чатовете ви в момента не е синхронизирано." + "Хранилището ви за ключове в момента не е синхронизирано." "Изключване" + "Изтриването на хранилището за ключове ще премахне вашата криптографска самоличност и ключове за съобщения от сървъра и ще изключи следните функции за сигурност:" + "Сигурни ли сте, че искате да изключите хранилището на ключове и да го изтриете?" + "Вземете нов ключ за възстановяване, ако сте загубили съществуващия си. След като промените ключа си за възстановяване, старият ви вече няма да работи." "Генериране на нов ключ за възстановяване" + "Не споделяйте това с никого!" "Промяна на ключа за възстановяване?" "Уверете се, че никой не може да види този екран!" + "Моля, опитайте отново, за да потвърдите достъпа до хранилището за ключове." "Неправилен ключ за възстановяване" - "Въведете 48-символния код." + "Ако имате ключ за сигурност или фраза за сигурност, това също ще работи." "Въведете…" "Ключът за възстановяване е потвърден" + "Въведете ключа си за възстановяване" "Копиран ключ за възстановяване" "Запазване на ключа за възстановяване" + "Вашето хранилище за ключове е защитено с ключ за възстановяване. Ако имате нужда от нов ключ за възстановяване след настройката, можете да го създадете отново, като изберете „Промяна на ключа за възстановяване“." + "Не споделяйте това с никого!" "Въведете…" diff --git a/features/securebackup/impl/src/main/res/values-cs/translations.xml b/features/securebackup/impl/src/main/res/values-cs/translations.xml index 3ba3bd98eec..f03a7d68d62 100644 --- a/features/securebackup/impl/src/main/res/values-cs/translations.xml +++ b/features/securebackup/impl/src/main/res/values-cs/translations.xml @@ -62,9 +62,9 @@ "Nastavení obnovy" "Ano, resetovat nyní" "Tento proces je nevratný." - "Opravdu chcete obnovit šifrování?" + "Opravdu chcete obnovit svou identitu?" "Došlo k neznámé chybě. Zkontrolujte, zda je heslo k účtu správné a zkuste to znovu." "Zadejte…" - "Potvrďte, že chcete obnovit šifrování." + "Potvrďte, že chcete obnovit svou identitu." "Pro pokračování zadejte heslo k účtu" diff --git a/features/securebackup/impl/src/main/res/values-cy/translations.xml b/features/securebackup/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..fdff84c0b9f --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,70 @@ + + + "Dileu storfa allweddi" + "Trowch y copi wrth gefn ymlaen" + "Cadwch eich hunaniaeth cryptograffig a\'ch allweddi neges yn ddiogel ar y gweinydd. Bydd hyn yn caniatáu ichi weld hanes eich neges ar unrhyw ddyfeisiau newydd. %1$s." + "Storio allweddi" + "Rhaid troi storio allweddi ymlaen i osod adferiad." + "Llwythwch allweddi i fyny o\'r ddyfais hon" + "Caniatáu storio allweddi" + "Newid yr allwedd adfer" + "Adferwch eich hunaniaeth cryptograffig a hanes negeseuon gydag allwedd adfer os ydych chi wedi colli\'ch holl ddyfeisiau presennol." + "Rhowch eich allwedd adfer" + "Nid yw eich storfa allweddi wedi\'i chydweddu ar hyn o bryd." + "Gosod adfer" + "Cael mynediad i\'ch negeseuon wedi\'u hamgryptio os byddwch yn colli\'ch holl ddyfeisiau neu\'n cael eich allgofnodi o %1$s ym mhobman." + "Agor %1$s mewn dyfais bwrdd gwaith" + "Mewngofnodwch i\'ch cyfrif eto" + "Pan fydd gofyn i chi ddilysu\'ch dyfais, dewiswch %1$s" + "“Ailosod y cyfan“" + "Dilynwch y cyfarwyddiadau i greu allwedd adfer newydd" + "Cadwch eich allwedd adfer newydd mewn rheolwr cyfrinair neu mewn nodyn wedi\'i amgryptio" + "Ailosodwch yr amgryptio ar gyfer eich cyfrif gan ddefnyddio dyfais arall" + "Parhau i ailosod" + "Bydd manylion eich cyfrif, eich cysylltiadau, eich dewisiadau a\'ch rhestr sgwrsio yn cael eu cadw" + "Byddwch yn colli unrhyw hanes neges sydd wedi\'i gadw dim ond ar y gweinydd" + "Bydd angen i chi wirio\'ch holl ddyfeisiau a chysylltiadau presennol eto" + "Ailosodwch eich hunaniaeth dim ond os nad oes gennych fynediad i ddyfais arall sydd wedi\'i mewngofnodi a\'ch bod wedi colli\'ch allwedd adfer." + "Methu cadarnhau? Bydd angen i chi ailosod eich hunaniaeth." + "Diffodd" + "Byddwch yn colli eich negeseuon wedi\'u hamgryptio os ydych wedi\'ch allgofnodi o bob dyfais." + "Ydych chi\'n siŵr eich bod am ddiffodd copi wrth gefn?" + "Bydd dileu storfa allweddi\'n tynnu eich hunaniaeth cryptograffig a\'ch allweddi neges o\'r gweinydd ac yn diffodd y nodweddion diogelwch canlynol:" + "Bydd gennych chi ddim hanes negeseuon wedi\'i amgryptio ar ddyfeisiau newydd" + "Byddwch yn colli mynediad i\'ch negeseuon wedi\'u hamgryptio os ydych wedi\'ch allgofnodi o %1$s ym mhobman" + "Ydych chi\'n siŵr eich bod am ddiffodd storfa allweddi a\'i dileu?" + "Cael allwedd adfer newydd os ydych chi wedi colli\'ch un presennol. Ar ôl newid eich allwedd adfer, fydd eich hen un ddim yn gweithio mwyach." + "Cynhyrchu allwedd adfer newydd" + "Peidiwch â rhannu hwn gyda neb!" + "Newidwyd yr allwedd adfer" + "Newid yr allwedd adfer?" + "Creu allwedd adfer newydd" + "Gwnewch yn siŵr nad oes neb yn gallu gweld y sgrin hon!" + "Ceisiwch eto i gadarnhau mynediad i\'ch storfa allweddi." + "Allwedd adfer anghywir" + "Os oes gennych allwedd ddiogelwch neu ymadrodd diogelwch, bydd hyn yn gweithio hefyd." + "Rhowch…" + "Wedi colli eich allwedd adfer?" + "Wedi cadarnhau\'r allwedd adfer" + "Rhowch eich allwedd adfer" + "Allwedd adfer wedi\'i chopïo" + "Yn cynhyrchu…" + "Cadw allwedd adfer" + "Ysgrifennwch yr allwedd adfer hon yn rhywle diogel, fel rheolwr cyfrinair, nodyn wedi\'i amgryptio, neu mewn man dan glo." + "Tapiwch i gopïo\'r allwedd adfer" + "Cadwch eich allwedd adfer yn rhywle diogel" + "Fyddwch chi ddim yn gallu cyrchu\'ch allwedd adfer newydd ar ôl y cam hwn." + "Ydych chi wedi cadw\'ch allwedd adfer?" + "Mae eich storfa allweddi wedi\'i diogelu gan allwedd adfer. Os oes angen allwedd adfer newydd arnoch ar ôl gosod, gallwch ei hail-greu trwy ddewis \'Newid allwedd adfer\'." + "Cynhyrchwch eich allwedd adfer" + "Peidiwch â rhannu hwn gyda neb!" + "Llwyddiant wrth osod adferiad" + "Gosod adfer" + "Iawn, ailosod nawr" + "Does dim posib dadwneud y broses hon." + "Ydych chi\'n siŵr eich bod am ailosod eich hunaniaeth?" + "Digwyddodd gwall anhysbys. Gwiriwch fod cyfrinair eich cyfrif yn gywir a cheisio eto." + "Rhowch…" + "Cadarnhewch eich bod am ailosod eich hunaniaeth." + "Rhowch eich cyfrinair cyfrif i barhau" + diff --git a/features/securebackup/impl/src/main/res/values-da/translations.xml b/features/securebackup/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..b4a385eea2a --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,70 @@ + + + "Slet nøglelager" + "Aktivér sikkerhedskopiering" + "Gem din kryptografiske identitet og meddelelsesnøgler sikkert på serveren. Dette giver dig mulighed for at se din meddelelseshistorik på alle nye enheder. %1$s." + "Nøgleopbevaring" + "Nøglelagring skal være slået til for at konfigurere gendannelse." + "Upload nøgler fra denne enhed" + "Tillad lagring af nøgler" + "Skift gendannelsesnøgle" + "Gendan din kryptografiske identitet og beskedhistorik med en gendannelsesnøgle, hvis du har mistet alle dine eksisterende enheder." + "Indtast gendannelsesnøgle" + "Din nøglelagring er i øjeblikket ikke synkroniseret." + "Opsæt gendannelse" + "Få adgang til dine krypterede meddelelser, hvis du mister alle dine enheder eller er logget ud af %1$s overalt." + "Åbn %1$s på en stationær enhed" + "Log ind på din konto igen" + "Når du bliver bedt om at bekræfte din enhed, skal du vælge %1$s" + "\"Nulstil alle\"" + "Følg instruktionerne for at oprette en ny gendannelsesnøgle" + "Gem din nye gendannelsesnøgle i en adgangskodeadministrator eller i en krypteret note" + "Nulstil krypteringen for din konto ved hjælp af en anden enhed" + "Fortsæt nulstilling" + "Dine kontodetaljer, kontakter, personlige indstilliger og samtaler vil blive gemt" + "Du mister al beskedhistorik, der kun er gemt på serveren." + "Du bliver nødt til at verificere alle dine eksisterende enheder og kontakter påny" + "Nulstil kun din identitet, hvis du ikke har adgang til en anden enhed, der er logget ind, og du har mistet din gendannelsesnøgle." + "Kan du ikke bekræfte? Du skal nulstille din identitet." + "Slå fra" + "Du mister dine krypterede meddelelser, hvis du er logget ud af alle enheder." + "Er du sikker på, at du vil slå sikkerhedskopiering fra?" + "Hvis du sletter nøglelageret, fjernes din kryptografiske identitet og meddelelsesnøgler fra serveren og følgende sikkerhedsfunktioner deaktiveres:" + "Du vil ikke kunne se historikken for krypterede beskeder på nye enheder" + "Du mister adgangen til dine krypterede meddelelser, hvis du er logget ud %1$s overalt" + "Er du sikker på, at du vil deaktivere nøglelagring og slette lageret?" + "Få en ny gendannelsesnøgle, hvis du har mistet din eksisterende. Når du har ændret din gendannelsesnøgle, fungerer din gamle ikke længere." + "Generer en ny gendannelsesnøgle" + "Del ikke dette med nogen!" + "Gendannelsesnøgle ændret" + "Skift gendannelsesnøgle?" + "Opret ny gendannelsesnøgle" + "Sørg for, at ingen kan se denne skærm!" + "Prøv igen for at bekræfte adgangen til dit nøglelager." + "Forkert gendannelsesnøgle" + "Hvis du har en sikkerhedsnøgle eller sikkerhedssætning, kan en af dem også bruges." + "Indtast…" + "Mistet din gendannelsesnøgle?" + "Gendannelsesnøgle bekræftet" + "Indtast din gendannelsesnøgle" + "Kopieret gendannelsesnøgle" + "Genererer…" + "Gem gendannelsesnøgle" + "Skriv denne gendannelsesnøgle et sikkert sted, som en adgangskodeadministrator, en krypteret note eller på papir, som du lægger i et fysisk pengeskab." + "Tryk for at kopiere gendannelsesnøglen" + "Gem din gendannelsesnøgle et sikkert sted" + "Du vil ikke kunne få adgang til din nye gendannelsesnøgle efter dette trin." + "Har du gemt din gendannelsesnøgle?" + "Din nøglelager er beskyttet af en gendannelsesnøgle. Hvis du har brug for en ny gendannelsesnøgle efter installationen, kan du oprette den ved at vælge \'Skift gendannelsesnøgle\'." + "Generer din gendannelsesnøgle" + "Del ikke dette med nogen!" + "Opsætning af gendannelse lykkedes" + "Opsæt gendannelse" + "Ja, nulstil nu" + "Denne proces er irreversibel." + "Er du sikker på, at du ønsker at nulstille din identitet?" + "Der opstod en ukendt fejl. Kontroller, at adgangskoden til din konto er korrekt, og prøv igen." + "Indtast…" + "Bekræft, at du ønsker at nulstille din identitet." + "Indtast adgangskoden til din konto for at fortsætte" + 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 29e43e2e2f0..3c6dd95b8e6 100644 --- a/features/securebackup/impl/src/main/res/values-de/translations.xml +++ b/features/securebackup/impl/src/main/res/values-de/translations.xml @@ -12,7 +12,7 @@ "Wiederherstellungsschlüssel eingeben" "Dein Schlüssel ist derzeit nicht synchronisiert." "Wiederherstellung einrichten" - "Erhalte Zugriff auf deine verschlüsselten Nachrichten, wenn du alle deine Geräte verlierst oder von %1$s überall abgemeldet bist." + "Erhalten Sie Zugriff auf Ihre verschlüsselten Nachrichten, wenn Sie all Ihre Geräte verlieren oder von %1$s überall abgemeldet sind." "Öffne " "%1$s" @@ -20,7 +20,7 @@ "Desktop-Gerät" "Melde dich erneut bei deinem Konto an" - "Wenn du aufgefordert wirst dein Gerät zu verifizieren, wähle \"%1$s\"." + "Wenn Sie aufgefordert werden, Ihr Gerät zu verifizieren, wählen Sie \"%1$s\"" "Alles zurücksetzen" "Folge den Anweisungen, um einen neuen Wiederherstellungsschlüssel zu erstellen" @@ -35,18 +35,18 @@ "Zurücksetzen fortsetzen" "Deine Kontodaten, Kontakte, Einstellungen und die Liste der Chats bleiben erhalten" - "Du verlierst alle deine bisherigen Nachrichten sofern sie nicht auf einem anderen Gerät vorliegen" - "Du musst alle deine bestehenden Geräte und Kontakte erneut verifizieren." + "Sie verlieren jeglichen Nachrichtenverlauf, der nur auf dem Server gespeichert ist" + "Sie müssen dann alle Ihre vorhandenen Geräte und Kontakte erneut verifizieren" "Setzen Sie Ihre Identität nur dann zurück, wenn Sie keinen Zugriff auf ein anderes Ihrer angemeldeten Geräte und auch Ihren Wiederherstellungsschlüssel verloren haben." - "Sie können es nicht bestätigen? Dann müssen Sie Ihre Identität zurücksetzen." + "Bestätigung unmöglich? Dann müssen Sie Ihre 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?" - "Wenn du das Backup deaktivierst, wird dein aktuelles Schlüssel Backup für die Verschlüsselung entfernt und andere Sicherheitsfunktionen werden deaktiviert. Das bedeutet:" - "Keinen Nachrichtenverlauf für verschlüsselte Nachrichten auf neuen Geräten ." - "Verlust des Zugriffs auf Deine verschlüsselten Nachrichten, wenn Du Dich überall von %1$s abmeldest" - "Bist du sicher, dass du das Backup deaktivieren willst?" - "Hier kannst Du einen neuen Wiederherstellungsschlüssel erstellen. Nachdem Du einen neuen Wiederherstellungsschlüssel erstellt hast, funktioniert dein alter Schlüssel nicht mehr." + "Sie verlieren Ihre verschlüsselten Nachrichten, wenn Sie von allen Ihren Geräten abgemeldet sind." + "Sind Sie sicher, dass Sie das Backup deaktivieren möchten?" + "Mit dem Löschen des Schlüsselspeichers werden Ihre kryptografische Identität und Ihre Nachrichtenschlüssel vom Server entfernt und die folgenden Sicherheitsfunktionen werden deaktiviert:" + "Keinen Nachrichtenverlauf für verschlüsselte Nachrichten auf neuen Geräten." + "Sie verlieren den Zugriff auf Ihre verschlüsselten Nachrichten, wenn Sie von %1$s überall abgemeldet sind" + "Möchten Sie die Schlüsselspeicherung wirklich deaktivieren und entfernen?" + "Falls Sie Ihren alten Wiederherstellungsschlüssel verloren haben, erstellen Sie einen neuen. Danach funktioniert Ihr alter Schlüssel nicht mehr." "Wiederherstellungsschlüssel erstellen" "Geben Sie dies an niemanden weiter!" "Wiederherstellungsschlüssel geändert" @@ -61,7 +61,7 @@ "Falscher Wiederherstellungsschlüssel" "Dies funktioniert auch mit einem Sicherheitsschlüssel oder Sicherheitsphrase." "Eingeben…" - "Hast du deinen Wiederherstellungschlüssel vergessen?" + "Wiederherstellungschlüssel vergessen?" "Wiederherstellungsschlüssel bestätigt" "Geben Sie Ihren Wiederherstellungsschlüssel ein" "Wiederherstellungsschlüssel kopiert" @@ -70,18 +70,18 @@ "Schreiben Sie Ihren Wiederherstellungsschlüssel in eine verschlüsselte Datei, oder in einem Passwort-Manager oder in einem Safe. " "Tippe, um den Wiederherstellungsschlüssel zu kopieren" "Speichere deinen Wiederherstellungsschlüssel" - "Nach diesem Schritt kannst du nicht mehr auf deinen neuen Wiederherstellungsschlüssel zugreifen." - "Hast du deinen Wiederherstellungsschlüssel gespeichert?" - "Dein Chat-Backup ist durch einen Wiederherstellungsschlüssel geschützt. Wenn du nach der Einrichtung einen neuen Wiederherstellungsschlüssel brauchst, kannst du ihn über die Option \"Wiederherstellungsschlüssel ändern\" neu erstellen." + "Nach diesem Schritt können Sie nicht mehr auf Ihren neuen Wiederherstellungsschlüssel zugreifen." + "Haben Sie Ihren Wiederherstellungsschlüssel gespeichert?" + "Ihr Schlüsselbackup ist durch einen Wiederherstellungsschlüssel geschützt. Wenn Sie nach der Installation einen neuen Wiederherstellungsschlüssel benötigen, können Sie einen neuen kreiern, indem Sie „Wiederherstellungsschlüssel erstellen“ auswählen." "Wiederherstellungsschlüssel erstellen" "Geben Sie dies an niemanden weiter!" "Einrichtung der Wiederherstellung erfolgreich" "Wiederherstellung einrichten" "Ja, zurücksetzen" "Das Zurücksetzen kann nicht rückgängig gemacht werden." - "Bist du sicher, dass du deine Identität zurücksetzen möchtest?" + "Möchten Sie Ihre kryptografische Identität wirklich zurücksetzen?" "Es ist ein unbekannter Fehler aufgetreten. Bitte überprüfe das Passwort deines Kontos und versuche es erneut." "Eingeben…" - "Bestätige, dass du deine Identität zurücksetzen möchtest." + "Bestätigen Sie, dass Sie Ihre Identität zurücksetzen möchten." "Gib dein Passwort ein, um fortzufahren" diff --git a/features/securebackup/impl/src/main/res/values-es/translations.xml b/features/securebackup/impl/src/main/res/values-es/translations.xml index 64a98d6f94a..da6f4f23916 100644 --- a/features/securebackup/impl/src/main/res/values-es/translations.xml +++ b/features/securebackup/impl/src/main/res/values-es/translations.xml @@ -1,44 +1,70 @@ - "Desactivar copia de seguridad" + "Borrar almacén de claves" "Activar copia de seguridad" - "La copia de seguridad garantiza que no pierdas tu historial de mensajes. %1$s." - "Copia de seguridad" + "Almacena tu identidad criptográfica y las claves de tus mensajes de forma segura en el servidor. Esto te permitirá ver tu historial de mensajes en cualquier dispositivo nuevo. %1$s." + "Almacenamiento de claves" + "El almacenamiento de claves debe estar activado para configurar la recuperación." + "Subir claves desde este dispositivo" + "Permitir almacenamiento de claves" "Cambiar la clave de recuperación" - "Introduzca la clave de recuperación" - "La copia de seguridad de tus chats no está sincronizada ahora mismo." + "Recupera tu identidad criptográfica y tu historial de mensajes con una clave de recuperación si has perdido todos tus dispositivos actuales." + "Introduce la clave de recuperación" + "Tu almacén de claves no está sincronizado actualmente." "Configurar la recuperación" "Accede a tus mensajes cifrados si pierdes todos tus dispositivos o cierras sesión de %1$s en cualquier lugar." + "Abre %1$s en un dispositivo de escritorio" + "Vuelve a iniciar sesión en tu cuenta" + "Cuando se te pida que verifiques tu dispositivo, selecciona %1$s" + "«Restablecer todo»" + "Sigue las instrucciones para crear una nueva clave de recuperación" + "Guarda tu nueva clave de recuperación en un administrador de contraseñas o en una nota cifrada" + "Restablece el cifrado de tu cuenta usando otro dispositivo" + "Continuar con el restablecimiento" + "Se conservarán los detalles de tu cuenta, tus contactos, tus preferencias y tu lista de chats" + "Perderás cualquier historial de mensajes que solo esté almacenado en el servidor" + "Tendrás que verificar de nuevo todos tus dispositivos y contactos existentes" + "Restablece tu identidad solo si no tienes acceso a otro dispositivo en el que hayas iniciado sesión y has perdido tu clave de recuperación." + "¿No puedes confirmar? Tendrás que restablecer tu identidad." "Desactivar" "Perderás tus mensajes cifrados si cierras sesión en todos los dispositivos." "¿Estás seguro de que quieres desactivar la copia de seguridad?" - "Desactivar la copia de seguridad eliminará la copia de seguridad de tu clave de encriptación actual y desactivará otras funciones de seguridad. En este caso:" + "Al borrar el almacén de claves, se eliminarán del servidor tu identidad criptográfica y claves de los mensajes, y se desactivarán las siguientes funciones de seguridad:" "No tendrás un historial de mensajes cifrados en nuevos dispositivos" - "Perderás el acceso a tus mensajes cifrados si cierras sesión %1$s en todas partes" - "¿Estás seguro de que quieres desactivar la copia de seguridad?" + "Perderás el acceso a tus mensajes cifrados si cierras sesión en %1$s en todas partes" + "¿Estás seguro de que quieres desactivar el almacenamiento de claves y borrarlo?" "Obtén una nueva clave de recuperación si has perdido la que tenías. Después de cambiar la clave de recuperación, la anterior dejará de funcionar." "Generar una nueva clave de recuperación" - "Asegúrate de que puedes guardar tu clave de recuperación en algún lugar seguro" + "¡No la compartas con nadie!" "Clave de recuperación cambiada" "¿Cambiar la clave de recuperación?" - "Introduce tu clave de recuperación para confirmar el acceso a la copia de seguridad de tú chat." - "Por favor, inténtalo de nuevo para confirmar el acceso a tu copia de seguridad del chat." + "Crear nueva clave de recuperación" + "¡Asegúrate de que nadie pueda ver esta pantalla!" + "Inténtalo de nuevo para confirmar el acceso a tu almacén de claves." "Clave de recuperación incorrecta" - "Introduce el código de 48 caracteres." - "Ingresar…" + "Si tienes una clave o frase de seguridad, también funcionará." + "Introducir…" + "¿Perdiste tu clave de recuperación?" "Clave de recuperación confirmada" + "Introduce tu clave de recuperación" "Clave de recuperación copiada" "Generando…" "Guardar clave de recuperación" - "Anota tu clave de recuperación en un lugar seguro o guárdala en un administrador de contraseñas." + "Anota esta clave de recuperación en un lugar seguro, como un administrador de contraseñas, una nota cifrada o una caja fuerte física." "Pulsa para copiar la clave de recuperación" "Guardar tu clave de recuperación" "No podrás acceder a tu nueva clave de recuperación después de este paso." "¿Has guardado tu clave de recuperación?" - "La copia de seguridad del chat está protegida por una clave de recuperación. Si necesitas una nueva clave de recuperación después de la configuración, puedes volver a crearla seleccionando «Cambiar clave de recuperación»." + "Tu almacén de claves está protegido por una clave de recuperación. Si necesitas una nueva clave de recuperación después de la configuración, puedes volver a crearla seleccionando «Cambiar la clave de recuperación»." "Generar tu clave de recuperación" - "Asegúrate de que puedes guardar tu clave de recuperación en algún lugar seguro" + "¡No la compartas con nadie!" "Configuración de recuperación terminada" "Configurar la recuperación" - "Ingresar…" + "Sí, restablecer ahora" + "Este proceso es irreversible." + "¿Estás seguro de que quieres restablecer tu identidad?" + "Se ha producido un error desconocido. Comprueba que la contraseña de tu cuenta sea correcta y vuelve a intentarlo." + "Introducir…" + "Confirma que quieres restablecer tu identidad." + "Introduce la contraseña de tu cuenta para continuar" diff --git a/features/securebackup/impl/src/main/res/values-eu/translations.xml b/features/securebackup/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..3cb643a04c1 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,58 @@ + + + "Ezabatu gakoen biltegia" + "Aktibatu babeskopia" + "Gorde zure identitate-kriptografikoa eta mezuen gakoak modu seguruan zerbitzarian. Horrek zure mezuen historia edozein gailu berritan ikusteko aukera emango dizu. %1$s." + "Gakoen biltegiratzea" + "Aldatu berreskuratze-gakoa" + "Sartu berreskuratze-gakoa" + "Zure giltzen-biltegiratzea gaur-gaurkoz sinkronizatu gabe dago." + "Konfiguratu berreskurapena" + "Ireki %1$s mahaigaineko gailu batean" + "Hasi saioa berriro zure kontuan" + "Gailua egiaztatzeko eskatzen zaizunean, hautatu %1$s" + "\"Berrezarri guztia\"" + "Jarraitu argibideak berreskuratze-gako berri bat sortzeko" + "Gorde berreskuratze-gako berria pasahitz-kudeatzaile batean edo enkriptatutako ohar batean" + "Berrezarri zure kontuaren enkriptazioa beste gailu bat erabiliz" + "Jarraitu berrezarpenarekin" + "Ezin duzu baieztatu? Zure identitatea berrezarri beharko duzu." + "Desaktibatu" + "Enkriptatutako mezuak galduko dituzu gailu guztietan saioa amaitzen baduzu." + "Ziur babeskopia desaktibatu nahi duzula?" + "Gakoen biltegiratzea ezabatuz gero, zure identitate kriptografikoa eta mezuen gakoak zerbitzaritik kenduko dira eta honako segurtasun ezaugarriak desaktibatu egingo dira:" + "Aurrerantzean ez duzu mezuen historia enkriptatuta izango gailu berrietan" + "Gailu guztietan amaitzen baduzu %1$s saioa, enkriptatutako mezuetarako sarbidea galduko duzu" + "Ziur gakoen biltegia desaktibatu eta ezabatu nahi duzula?" + "Lortu berreskuratze-gako berri bat lehendik duzuna galdu baduzu. Berreskuratze-gakoa aldatu ondoren, zaharrak ez du funtzionatuko." + "Sortu berreskuratze-gako berri bat" + "Ez partekatu inorekin!" + "Berreskuratze-gakoa aldatu da" + "Berreskuratze-gakoa aldatu?" + "Sortu berreskuratze-gako berria" + "Egiaztatu inork ezin duela pantaila hau ikusi!" + "Berreskuratze-gako okerra" + "Segurtasun-gako edo -esaldi bat baduzu, honek ere balio du." + "Sartu…" + "Berreskuratze-gakoa galdu duzu?" + "Berreskuratze-gakoa berretsi da" + "Sartu zure berreskuratze-gakoa" + "Berreskuratze-gakoa kopiatu da" + "Sortzen…" + "Gorde berreskuratze-gakoa" + "Idatzi berreskuratze-gako hau leku seguru batean, esate baterako, pasahitzen kudeatzaile, enkriptatutako ohar, edo kutxa gotor fisiko batean." + "Sakatu berreskuratze-gakoa kopiatzeko" + "Gorde berreskuratze-gakoa leku seguru batean" + "Ezingo duzu berreskuratze-gako berria atzitu urrats honen ondoren." + "Gorde al duzu berreskuratze-gakoa?" + "Sortu berreskuratze-gakoa" + "Ez partekatu inorekin!" + "Berreskuratzea ondo konfiguratu da" + "Konfiguratu berreskurapena" + "Bai, berrezarri orain" + "Ezin da desegin." + "Ziur zure identitatea berrezarri nahi duzula?" + "Sartu…" + "Berretsi zure identitatea berrezarri nahi duzula." + "Idatzi kontuaren pasahitza aurrera egiteko" + 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 a0d71400c8d..095dcbc139a 100644 --- a/features/securebackup/impl/src/main/res/values-fa/translations.xml +++ b/features/securebackup/impl/src/main/res/values-fa/translations.xml @@ -2,12 +2,14 @@ "خاموش کردن پشتیبان" "روشن کردن پشتیبان" - "پشتیبان‌ها اطمینان می‌دهند که تاریخچهٔ پیام‌هایتان را از دست نمی‌دهید. %1$s." - "پشتیبان گیری" + "ذخیرهٔ کلیدهای پیام‌ها و هویت رمزنگاریتان به صورت امن روی کارساز. این کار می‌گذارد تاریخچهٔ پیام‌هایتان را روی هر افزارهٔ جدیدی ببینید. %1$s." + "ذخیره‌ساز کلید" + "بارگذاری کلیدها از ین افزاره" "تغییر کلید بازیابی" "ورود کلید بازیابی" - "پشتیبان گپتان از هم‌گام بودن در آمده." + "ذخیره‌ساز کلیدتان از هم‌گام بودن در آمده." "برپایی بازیابی" + "اگر همه دستگاه‌هایتان را گم کردید یا از سیستم خارج شدید، به پیام‌های رمزگذاری‌شده‌تان دسترسی پیدا کنید%1$s همه جا." "گشودن %1$s در افزارهٔ میزکار" "ورود دوباره به حسابتان" "گزینش %1$s هنگام درخواست تأیید افزاره‌تان" @@ -22,30 +24,37 @@ "فقط اگر به افزاره‌ای وارد شده از پیش دسترسی ندارید و کلید بازیابیتان را گم کرده‌اید بازنشانی کنید." "نمی‌توانید تأیید کنید؟ لازم است هویتتان را بازنشانی کنید." "خاموش کردن" + "اگر از سیستم همه دستگاه ها خارج شده باشید، پیام های رمزگذاری شده خود را از دست خواهید داد." + "آیا مطمئن هستید که می خواهید پشتیبان گیری را خاموش کنید؟" + "حذف فضای ذخیره سازی کلید، هویت رمزنگاری و کلیدهای پیام شما را از سرور حذف می کند و ویژگی های امنیتی زیر را خاموش می کند:" + "سابقه پیام رمزگذاری شده در دستگاه های جدید نخواهید داشت" + "اگر از %1$s در همه جا خارج شده باشید، دسترسی به پیام های رمزگذاری شده خود را از دست خواهید داد" + "آیا مطمئن هستید که می خواهید فضای ذخیره سازی کلید را خاموش کرده و آن را حذف کنید؟" "گرفتن کلید بازیابی جدید در صورت فراموشی کلید کنونی. پس از تغییر دادن کلید بازیابیتان، کلید پیشین دیگر کار نخواهد کرد." "تولید کلید بازیابی جدید" - "اطمینان از امکان نگه داری کلید بازیابیتان در جایی امن" + "با کسی هم‌رسانیش نکنید!" "کلید بازیابی تغییر کرد" "تغییر کلید بازیابی؟" "ایجاد کلید بازیابی جدید" "اطمینان از این که کسی نمی‌تواند این صفحه را ببیند!" - "لطفاً برای تأیید دسترسی به پشتیبان گپتان دوباره تلاش کنید." + "لطفاً برای دسترسی به ذخیره‌ساز کلیدتان دوباره تلاش کنید." "کلید بازیابی اشتباه" "کلید امنیتی یا عبارت امنیتی نیز باید کار کنند." "ورود…" "گم کردن کلید بازیابیتان؟" "کلید بازیابی تأیید شد" + "ورود کلید بازیابیتان" "کلید بازیابی رونوشت شد" "تولید کردن…" "ذخیرهٔ کلید بازیابی" - "نوشتن کلید بازیابیتان در جایی امن یا ذخیره‌اش در مدیر گذرواژه." + "این کلید بازیابی را در جایی امن چون مدیر گذرواژه، یادداشت رمز شده یا گاوصندوق فیزیکی بنویسید." "زدن برای رونوشت از کلید بازیابی" "ذخیرهٔ کلید بازیابیتان" "پس از این برپایی قادر به دسترسی به کلید بازیابی جدیدتان نخواهید بود." "کلید بازیابیتان را ذخیره کرده‌اید؟" "پشتیبان گپتان با کلید بازیابی محافظت می‌شود. اگر پس از برپایی نیاز به کلید بازیابی جدیدی داشتید می‌توانید با گزینش «دگرگونی کلید بازیابی» دوباره ایجادش کنید." "تولید کلید بازیابیتان" - "اطمینان از امکان نگه داری کلید بازیابیتان در جایی امن" + "با کسی هم‌رسانیش نکنید!" "برپایی بازیابی موفّق بود" "برپایی بازیابی" "بله. اکنون بازنشانی شود" diff --git a/features/securebackup/impl/src/main/res/values-hu/translations.xml b/features/securebackup/impl/src/main/res/values-hu/translations.xml index f6552348643..92212a40861 100644 --- a/features/securebackup/impl/src/main/res/values-hu/translations.xml +++ b/features/securebackup/impl/src/main/res/values-hu/translations.xml @@ -25,7 +25,7 @@ "Elveszíti meglévő üzenetelőzményeit" "Újból ellenőriznie kell az összes meglévő eszközét és csevegőpartnerét" "Csak akkor állítsa vissza a személyazonosságát, ha nem fér hozzá másik bejelentkezett eszközhöz, és elvesztette a helyreállítási kulcsot." - "Állítsa vissza a személyazonosságát, ha más módon nem tudja megerősíteni" + "Állítsa alaphelyzetbe a személyazonosságát, ha más módon nem tudja megerősíteni" "Kikapcsolás" "Ha kijelentkezik az összes eszközéről, akkor elveszti a titkosított üzeneteit." "Biztos, hogy kikapcsolja a biztonsági mentéseket?" diff --git a/features/securebackup/impl/src/main/res/values-ka/translations.xml b/features/securebackup/impl/src/main/res/values-ka/translations.xml index 61e4c51c1ea..3e17cb41a68 100644 --- a/features/securebackup/impl/src/main/res/values-ka/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ka/translations.xml @@ -5,9 +5,13 @@ "სარეზერვო ასლი უზრუნველყოფს იმას, რომ თქვენ შეტყობინებების ისტორიას არ დაკარგავთ. %1$s" "გასაღების საცავი" "აღდგენის გასაღების შეცვლა" + "შეიყვანეთ აღდგენის გასაღები" "თქვენი ჩატის სარეზერვო ასლი ამჟამად არ არის სინქრონიზებული." "აღდგენის დაყენება" "მიიღეთ წვდომა თქვენს დაშიფრულ შეტყობინებებზე, თუ დაკარგავთ თქვენს ყველა მოწყობილობას ან გამოხვალთ სისტემიდან %1$s-დან ყველგან." + "გახსენით %1$s კომპიუტერზე" + "შედით თქვენს ანგარიშში კიდევ ერთხელ" + "გაანულეთ თქვენი ანგარიშის დაშიფვრა სხვა მოწყობილობის დახმარებით" "გამორთვა" "თქვენ დაკარგავთ დაშიფრულ შეტყობინებებს, თუ ყველა მოწყობილობიდან გამოხვალთ." "დარწმუნებული ხართ, რომ გსურთ გამორთოთ სარეზერვო ასლი?" @@ -17,11 +21,11 @@ "დარწმუნებული ხართ, რომ გსურთ გამორთოთ სარეზერვო ასლი?" "მიიღეთ ახალი აღდგენის გასაღები, თუ დაკარგეთ არსებული. აღდგენის გასაღების შეცვლის შემდეგ, ძველი აღარ იმუშავებს." "ახალი აღდგენის გასაღების შექმნა" - "დარწმუნდით, რომ შეგიძლიათ შეინახოთ თქვენი აღდგენის გასაღები სადმე უსაფრთხო ადგილას" + "არავის გაუზიაროთ!" "აღდგენის გასაღები შეიცვალა" "გსურთ აღდგენის გასაღების შეცვლა?" "დარწმუნდით, რომ ვერავინ ხედავს ამ ეკრანს!" - "გთხოვთ, სცადოთ ხელახლა, რათა თქვენი ჩეთის სარეზერვო ასლაზე წვდომა დაადასტუროთ" + "გთხოვთ, სცადოთ წვდომის დადასტურება გასაღებების დამგროვებელთან კვლავ." "აღდგენის არასწორი გასაღები" "თუ თქვენ გაქვთ უსაფრთხოების გასაღები ან უსაფრთხოების ფრაზა, ეს ასევე იმუშავებს." "შეყვანა" @@ -29,14 +33,14 @@ "დაკოპირებულია აღდგენის გასაღები" "გენერირება…" "აღდგენის გასაღების შენახვა" - "ჩაწერეთ თქვენი აღდგენის გასაღები სადმე უსაფრთხო ადგილას ან შეინახეთ პაროლის მენეჯერში." + "ჩაწერეთ ეს აღდგენის გასაღები სადმე უსაფრთხო ადგილას, მაგალითად პაროლების მენეჯერში, დაშიფრულ შენიშვნაში ან ფიზიკურ სეიფში." "აღდგენის გასაღების დასაკოპირებლად, დააწკაპუნეთ" "შეინახეთ აღდგენის გასაღები" "თქვენ ვერ შეძლებთ წვდომას თქვენი ახალი აღდგენის გასაღებზე ამ ნაბიჯის შემდეგ." "შეინახეთ თქვენი აღდგენის გასაღები?" "თქვენი ჩატის სარეზერვო ასლი დაცულია აღდგენის გასაღებით. თუ დაყენების შემდეგ გჭირდებათ ახალი აღდგენის გასაღები, შეგიძლიათ ხელახლა შექმნათ „აღდგენის გასაღების შეცვლის“ არჩევით." "შექმენით აღდგენის გასაღები" - "დარწმუნდით, რომ შეგიძლიათ შეინახოთ თქვენი აღდგენის გასაღები სადმე უსაფრთხო ადგილას" + "არავის გაუზიაროთ!" "აღდგენის დაყენება წარმატებით დასრულდა" "აღდგენის დაყენება" "შეყვანა" diff --git a/features/securebackup/impl/src/main/res/values-nb/translations.xml b/features/securebackup/impl/src/main/res/values-nb/translations.xml index c95629df732..518e23e0709 100644 --- a/features/securebackup/impl/src/main/res/values-nb/translations.xml +++ b/features/securebackup/impl/src/main/res/values-nb/translations.xml @@ -1,6 +1,25 @@ + "Slett nøkkellagring" + "Slå på sikkerhetskopiering" + "Lagre din kryptografiske identitet og meldingsnøkler sikkert på serveren. Dette gjør at du kan se meldingshistorikken din på alle nye enheter. %1$s." + "Nøkkellagring" + "Nøkkellagring må være slått på for å konfigurere gjenoppretting." + "Last opp nøkler fra denne enheten" + "Tillat nøkkellagring" + "Endre gjenopprettingsnøkkel" + "Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter." "Skriv inn gjenopprettingsnøkkel" + "Nøkkellagringen din er for øyeblikket ikke synkronisert." + "Konfigurer gjenoppretting" + "Få tilgang til de krypterte meldingene dine hvis du mister alle enhetene dine eller blir logget ut av %1$s overalt." + "Åpne %1$s på en datamaskin" + "Logg på kontoen din igjen" + "Når du blir bedt om å verifisere din enhet, velger du %1$s" + "Tilbakestill alt" + "Følg instruksjonene for å opprette en ny gjenopprettingsnøkkel" + "Du bør lagre din nye gjenopprettingsnøkkel i en passordbehandler eller i et kryptert notat" + "Tilbakestill krypteringen for kontoen din ved hjelp av en annen enhet" "Fortsett tilbakestillingen" "Dine kontodetaljer, kontakter, innstillinger og chatteliste vil bli beholdt" "Du mister all meldingshistorikk som bare er lagret på serveren" @@ -8,5 +27,44 @@ "Tilbakestill identiteten din bare hvis du ikke har tilgang til en annen pålogget enhet og du har mistet gjenopprettingsnøkkelen." "Kan du ikke bekrefte? Du må tilbakestille identiteten din." "Slå av" + "Du mister de krypterte meldingene dine hvis du er logget ut av alle enheter." + "Er du sikker på at du vil slå av sikkerhetskopiering?" + "Sletting av nøkkellagring vil fjerne din kryptografiske identitet og meldingsnøkler fra serveren og deaktivere følgende sikkerhetsfunksjoner:" + "Du vil ikke ha kryptert meldingshistorikk på nye enheter" + "Du mister tilgangen til de krypterte meldingene dine hvis du er logget ut av %1$s overalt" + "Er du sikker på at du vil slå av nøkkellagring og slette den?" + "Få en ny gjenopprettingsnøkkel hvis du har mistet den eksisterende. Etter at du har endret gjenopprettingsnøkkelen, vil den gamle ikke lenger fungere." + "Generer en ny gjenopprettingsnøkkel" + "Ikke del dette med noen!" + "Gjenopprettingsnøkkel endret" + "Endre gjenopprettingsnøkkelen?" + "Opprett ny gjenopprettingsnøkkel" + "Sørg for at ingen kan se denne skjermen!" + "Prøv igjen for å verifisere tilgangen til nøkkellageret ditt." + "Feil gjenopprettingsnøkkel" + "Hvis du har en sikkerhetsnøkkel eller sikkerhetsfrase, vil dette også fungere." + "Angi…" + "Har du mistet gjenopprettingsnøkkelen?" + "Gjenopprettingsnøkkel bekreftet" "Skriv inn gjenopprettingsnøkkelen din" + "Kopiert gjenopprettingsnøkkel" + "Genererer…" + "Lagre gjenopprettingsnøkkelen" + "Skriv ned denne gjenopprettingsnøkkelen på et trygt sted, for eksempel i en passordbehandler, på en kryptert lapp eller i en fysisk safe." + "Trykk for å kopiere gjenopprettingsnøkkelen" + "Lagre gjenopprettingsnøkkelen på et trygt sted" + "Du vil ikke ha tilgang til den nye gjenopprettingsnøkkelen etter dette trinnet." + "Har du lagret gjenopprettingsnøkkelen din?" + "Nøkkellagringen din er beskyttet av en gjenopprettingsnøkkel. Hvis du trenger en ny gjenopprettingsnøkkel etter konfigureringen, kan du opprette den på nytt ved å velge «Endre gjenopprettingsnøkkel»." + "Generer gjenopprettingsnøkkelen din" + "Ikke del dette med noen!" + "Gjenopprettingsoppsett vellykket" + "Konfigurer gjenoppretting" + "Ja, tilbakestill nå" + "Denne prosessen kan ikke angres." + "Er du sikker på at du vil tilbakestille identiteten din?" + "En ukjent feil har oppstått. Vennligst sjekk at passordet ditt er riktig og prøv igjen." + "Angi…" + "Bekreft at du vil tilbakestille identiteten din." + "Skriv inn passordet til kontoen din for å fortsette" diff --git a/features/securebackup/impl/src/main/res/values-nl/translations.xml b/features/securebackup/impl/src/main/res/values-nl/translations.xml index 30f8362ed6a..047f06d3fea 100644 --- a/features/securebackup/impl/src/main/res/values-nl/translations.xml +++ b/features/securebackup/impl/src/main/res/values-nl/translations.xml @@ -3,10 +3,10 @@ "Back-up uitschakelen" "Back-up inschakelen" "Sla je cryptografische identiteit en berichtsleutels veilig op de server op. Zo kun je je berichtgeschiedenis bekijken op nieuwe apparaten. %1$s." - "Back-up" + "Sleutelopslag" "Herstelsleutel wijzigen" "Voer herstelsleutel in" - "Je chatback-up is momenteel niet gesynchroniseerd." + "Je sleutelopslag is momenteel niet gesynchroniseerd." "Herstelmogelijkheid instellen" "Krijg toegang tot je versleutelde berichten als je al je apparaten kwijtraakt of overal uit %1$s bent uitgelogd." "Open %1$s op een desktopapparaat" @@ -36,7 +36,7 @@ "Herstelsleutel wijzigen?" "Maak een nieuwe herstelsleutel" "Zorg ervoor dat niemand dit scherm kan zien!" - "Probeer het opnieuw om toegang tot je chatback-up te bevestigen." + "Probeer het opnieuw om toegang tot je sleutelopslag te bevestigen." "Onjuiste herstelsleutel" "Als je een beveiligingssleutel of beveiligingszin hebt, werkt dit ook." "Voer in…" diff --git a/features/securebackup/impl/src/main/res/values-pt-rBR/translations.xml b/features/securebackup/impl/src/main/res/values-pt-rBR/translations.xml index 240698bd565..524db970946 100644 --- a/features/securebackup/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/securebackup/impl/src/main/res/values-pt-rBR/translations.xml @@ -2,13 +2,30 @@ "Desativar o backup" "Ativar o backup" - "Armazene sua identidade criptográfica e chaves de mensagem com segurança no servidor. Isso permitirá que você visualize seu histórico de mensagens em quaisquer novos dispositivos.%1$s ." + "Armazene sua identidade criptográfica e chaves de mensagem com segurança no servidor. Isso permitirá que você visualize seu histórico de mensagens em quaisquer novos dispositivos.%1$s." "Armazenamento de chaves" + "O armazenamento de chaves deve ser ativado para configurar a recuperação." + "Carregar chaves a partir deste dispositivo" + "Permita o armazenamento de chaves" "Alterar chave de recuperação" + "Recupere sua identidade criptográfica e o histórico de mensagens com uma chave de recuperação, caso você tenha perdido todos os dispositivos existentes." "Insira a chave de recuperação" "Seu armazenamento de chaves está fora de sincronia no momento." "Configurar a recuperação" "Tenha acesso às suas mensagens criptografadas se você perder todos os seus dispositivos ou for desconectado do %1$s em qualquer lugar." + "Abrir %1$s em um dispositivo desktop" + "Inicie sessão na sua conta novamente" + "Quando solicitado a verificar seu dispositivo, selecione %1$s" + "\"Redefinir tudo\"" + "Siga as instruções para criar uma nova chave de recuperação" + "Salve sua nova chave de recuperação em um gerenciador de senhas ou em uma nota criptografada" + "Redefinir a criptografia da sua conta usando outro dispositivo" + "Continuar redefinindo" + "Os detalhes da sua conta, contatos, preferências e lista de bate-papo serão mantidos" + "Você perderá qualquer histórico de mensagens armazenado somente no servidor" + "Você precisará verificar todos os seus dispositivos e contatos existentes novamente." + "Redefina sua identidade somente se você não tiver acesso a outro dispositivo conectado e se tiver perdido sua chave de recuperação." + "Não consegue confirmar? Você precisará redefinir sua identidade." "Desligar" "Você perderá suas mensagens criptografadas se estiver desconectado de todos os dispositivos." "Tem certeza de que deseja desativar o backup?" @@ -21,12 +38,15 @@ "Não compartilhe isso com ninguém!" "Chave de recuperação alterada" "Alterar chave de recuperação?" + "Crie uma nova chave de recuperação" "Certifique-se de que ninguém possa ver essa tela!" + "Tente novamente para confirmar o acesso ao seu armazenamento de chaves." "Chave de recuperação incorreta" "Se você tiver uma chave de segurança ou frase de segurança, isso também funcionará." "Inserir…" "Perdeu sua chave de recuperação?" "Chave de recuperação confirmada" + "Digite sua chave de recuperação" "Chave de recuperação copiada" "Gerando…" "Salvar chave de recuperação" @@ -40,5 +60,11 @@ "Não compartilhe isso com ninguém!" "Configuração de recuperação bem-sucedida" "Configurar a recuperação" + "Sim, redefinir agora" + "Esse processo é irreversível." + "Você tem certeza de que deseja redefinir sua identidade?" + "Ocorreu um erro desconhecido. Verifique se a senha da sua conta está correta e tente novamente." "Inserir…" + "Confirme que você deseja redefinir sua identidade." + "Digite a senha de sua conta para continuar" 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 9b209c5ec67..1f3161d34aa 100644 --- a/features/securebackup/impl/src/main/res/values-ru/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ru/translations.xml @@ -2,7 +2,7 @@ "Удалить хранилище ключей" "Включить резервное копирование" - "Сохраните вашу криптографическую идентификацию и ключи сообщений в безопасности на сервере. Это позволит вам просматривать историю сообщений на любых новых устройствах.%1$s ." + "Сохраните вашу криптографическую идентификацию и ключи сообщений в безопасности на сервере. Это позволит вам просматривать историю сообщений на любых новых устройствах.%1$s." "Хранилище ключей" "Для настройки восстановления необходимо включить хранилище ключей." "Загрузить ключи с этого устройства" diff --git a/features/securebackup/impl/src/main/res/values-sv/translations.xml b/features/securebackup/impl/src/main/res/values-sv/translations.xml index 785e023bc53..501182b7aa3 100644 --- a/features/securebackup/impl/src/main/res/values-sv/translations.xml +++ b/features/securebackup/impl/src/main/res/values-sv/translations.xml @@ -4,7 +4,11 @@ "Slå på säkerhetskopiering" "Lagra din kryptografiska identitet och dina meddelandenycklar säkert på servern. Detta gör att du kan se din meddelandehistorik på alla nya enheter. %1$s." "Nyckellagring" + "Nyckellagring måste vara aktiverat för att konfigurera återställning." + "Ladda upp nycklar från den här enheten" + "Tillåt lagring av nycklar" "Byt återställningsnyckel" + "Återställ din kryptografiska identitet och meddelandehistorik med en återställningsnyckel om du har tappat bort alla dina befintliga enheter." "Ange återställningsnyckel" "Din nyckellagring är för närvarande osynkroniserad." "Ställ in återställning" @@ -42,6 +46,7 @@ "Ange …" "Blivit av med din återställningsnyckel?" "Återställningsnyckel bekräftad" + "Ange din återställningsnyckel" "Kopierade återställningsnyckel" "Genererar …" "Spara återställningsnyckeln" diff --git a/features/securebackup/impl/src/main/res/values-tr/translations.xml b/features/securebackup/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..b3a2acc0e90 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,70 @@ + + + "Anahtar depolamasını sil" + "Yedeklemeyi aç" + "Kriptografik kimliğinizi ve mesaj anahtarlarınızı sunucuda güvenli bir şekilde saklayın. Bu, mesaj geçmişinizi herhangi bir yeni cihazda görüntülemenize olanak tanır. %1$s." + "Anahtar Depolama" + "Kurtarmayı ayarlamak için anahtar depolama alanı açık olmalıdır." + "Anahtarları bu cihazdan yükle" + "Anahtar depolamaya izin ver" + "Kurtarma anahtarını değiştir" + "Mevcut tüm cihazlarınızı kaybettiyseniz, kurtarma anahtarıyla şifreleme kimliğinizi ve mesaj geçmişinizi kurtarın." + "Kurtarma anahtarını girin" + "Anahtar depolama alanınız şu anda senkronize değil." + "Kurtarmayı ayarlayın" + "Tüm cihazlarınızı kaybettiğinizde veya %1$s adresinden çıkış yaptığınızda şifrelenmiş mesajlarınıza her yerden erişin." + "Masaüstü cihazında aç %1$s" + "Hesabınızda tekrar oturum açın" + "Cihazınızı doğrulamanız istendiğinde %1$s öğesini seçin" + "\"Tümünü sıfırla\"" + "Yeni bir kurtarma anahtarı oluşturmak için talimatları izleyin" + "Yeni kurtarma anahtarınızı bir parola yöneticisine veya şifreli bir nota kaydedin" + "Hesabınızın şifrelemesini başka bir cihaz kullanarak sıfırlayın" + "Sıfırlamaya devam et" + "Hesap bilgileriniz, kişileriniz, tercihleriniz ve sohbet listeniz saklanacaktır" + "Yalnızca sunucuda depolanan mesaj geçmişlerini kaybedeceksiniz" + "Mevcut tüm cihazlarınızı ve kişilerinizi tekrar doğrulamanız gerekecek" + "Kimliğinizi yalnızca oturum açtığınız başka bir cihaza erişiminiz yoksa ve kurtarma anahtarınızı kaybettiyseniz sıfırlayın." + "Onaylayamıyor musunuz? Kimliğinizi sıfırlamanız gerekecek." + "Kapat" + "Tüm cihazlardan çıkış yaparsanız şifrelenmiş mesajlarınızı kaybedersiniz." + "Yedeklemeyi kapatmak istediğinizden emin misiniz?" + "Anahtar depolamanın silinmesi kriptografik kimliğinizi ve mesaj anahtarlarınızı sunucudan kaldıracak ve aşağıdaki güvenlik özelliklerini kapatacaktır:" + "Yeni cihazlarda şifrelenmiş mesaj geçmişine sahip olmayacaksınız" + "%1$s adresinden her yerde oturumunuzu kapatırsanız şifrelenmiş mesajlarınıza erişiminizi kaybedersiniz" + "Anahtar depolamayı kapatmak ve silmek istediğinizden emin misiniz?" + "Mevcut kurtarma anahtarınızı kaybettiyseniz yeni bir kurtarma anahtarı alın. Kurtarma anahtarınızı değiştirdikten sonra eski anahtarınız artık çalışmayacaktır." + "Yeni bir kurtarma anahtarı oluştur" + "Bunu kimseyle paylaşmayın!" + "Kurtarma anahtarı değiştirildi" + "Kurtarma anahtarını değiştir?" + "Yeni kurtarma anahtarı oluştur" + "Bu ekranı kimsenin göremediğinden emin olun!" + "Anahtar depolama alanınıza erişimi onaylamak için lütfen tekrar deneyin." + "Yanlış kurtarma anahtarı" + "Bir güvenlik anahtarınız veya güvenlik ifadeniz varsa, bu da işe yarayacaktır." + "Gir…" + "Kurtarma anahtarınızı mı kaybettiniz?" + "Kurtarma anahtarı onaylandı" + "Kurtarma anahtarınızı girin" + "Kurtarma anahtarı kopyalandı" + "Oluşturuluyor…" + "Kurtarma anahtarını kaydet" + "Bu kurtarma anahtarını şifre yöneticisi, şifreli not veya fiziksel kasa gibi güvenli bir yere kaydedin." + "Kurtarma anahtarını kopyalamak için dokunun" + "Kurtarma anahtarınızı güvenli bir yere kaydedin" + "Bu adımdan sonra yeni kurtarma anahtarınıza erişemeyeceksiniz." + "Kurtarma anahtarınızı kaydettiniz mi?" + "Anahtar depolama alanınız bir kurtarma anahtarıyla korunmaktadır. Kurulumdan sonra yeni bir kurtarma anahtarına ihtiyacınız olursa, \"Kurtarma anahtarını değiştir\"i seçerek yeniden oluşturabilirsiniz." + "Kurtarma anahtarınızı oluşturun" + "Bunu kimseyle paylaşmayın!" + "Kurtarma kurulumu başarılı" + "Kurtarmayı ayarlayın" + "Evet, şimdi sıfırla" + "Bu işlem geri alınamaz." + "Kimliğinizi sıfırlamak istediğinizden emin misiniz?" + "Bilinmeyen bir hata oluştu. Lütfen hesap şifrenizin doğru olup olmadığını kontrol edin ve tekrar deneyin." + "Gir…" + "Kimliğinizi sıfırlamak istediğinizi onaylayın." + "Devam etmek için hesap şifrenizi girin" + diff --git a/features/securebackup/impl/src/main/res/values-ur/translations.xml b/features/securebackup/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..80b55d88780 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,63 @@ + + + "کلید کا ذخیرہ حذف کریں" + "پشتارہ چالو کریں" + "خادم پر اپنی تشفیری شناخت اور پیغام کی کلیدیں محفوظ طریقے سے ذخیرہ کریں۔ اس سے آپ کسی بھی نئے آلات پر اپنے پیغام کی سرگزشت دیکھ سکیں گے۔ %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/reset/ResetIdentityFlowManagerTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt index 57a9fb62126..a26d6fabef8 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt @@ -17,7 +17,6 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.lambda.lambdaRecorder -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle @@ -132,11 +131,10 @@ class ResetIdentityFlowManagerTest { private fun TestScope.createFlowManager( encryptionService: FakeEncryptionService = FakeEncryptionService(), client: FakeMatrixClient = FakeMatrixClient(encryptionService = encryptionService), - sessionCoroutineScope: CoroutineScope = this, sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), ) = ResetIdentityFlowManager( matrixClient = client, - sessionCoroutineScope = sessionCoroutineScope, + sessionCoroutineScope = this, sessionVerificationService = sessionVerificationService, ) } diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateTest.kt new file mode 100644 index 00000000000..1e4f58a84a6 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateTest.kt @@ -0,0 +1,63 @@ +/* + * 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.securebackup.impl.root + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import org.junit.Test + +class SecureBackupRootStateTest { + @Test + fun `isKeyStorageEnabled should be true for all these backup states`() { + listOf( + BackupState.CREATING, + BackupState.ENABLING, + BackupState.RESUMING, + BackupState.DOWNLOADING, + BackupState.ENABLED, + ).forEach { backupState -> + assertThat(aSecureBackupRootState(backupState = backupState).isKeyStorageEnabled).isTrue() + } + } + + @Test + fun `isKeyStorageEnabled should be false for all these backup states`() { + listOf( + BackupState.WAITING_FOR_SYNC, + BackupState.DISABLING, + ).forEach { backupState -> + assertThat(aSecureBackupRootState(backupState = backupState).isKeyStorageEnabled).isFalse() + } + } + + @Test + fun `isKeyStorageEnabled should have value depending on doesBackupExistOnServer when state is UNKNOWN`() { + assertThat( + aSecureBackupRootState( + backupState = BackupState.UNKNOWN, + doesBackupExistOnServer = AsyncData.Success(true), + ).isKeyStorageEnabled + ).isTrue() + + listOf( + AsyncData.Uninitialized, + AsyncData.Loading(), + AsyncData.Failure(AN_EXCEPTION), + AsyncData.Success(false), + ).forEach { doesBackupExistOnServer -> + assertThat( + aSecureBackupRootState( + backupState = BackupState.UNKNOWN, + doesBackupExistOnServer = doesBackupExistOnServer, + ).isKeyStorageEnabled + ).isFalse() + } + } +} diff --git a/features/share/impl/build.gradle.kts b/features/share/impl/build.gradle.kts index ae0cad80dff..c059a249820 100644 --- a/features/share/impl/build.gradle.kts +++ b/features/share/impl/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(projects.libraries.roomselect.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) + implementation(projects.services.appnavstate.api) api(libs.statemachine) api(projects.features.share.api) diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareIntentHandler.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareIntentHandler.kt index 479ca773b56..b2b1dd36e6d 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareIntentHandler.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareIntentHandler.kt @@ -89,7 +89,7 @@ class DefaultShareIntentHandler @Inject constructor( * Use this function to retrieve files which are shared from another application or internally * by using android.intent.action.SEND or android.intent.action.SEND_MULTIPLE actions. */ - private fun getIncomingUris(intent: Intent, type: String): List { + private fun getIncomingUris(intent: Intent, fallbackMimeType: String): List { val uriList = mutableListOf() if (intent.action == Intent.ACTION_SEND) { IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java) @@ -115,9 +115,12 @@ class DefaultShareIntentHandler @Inject constructor( } } return uriList.map { uri -> + // The value in fallbackMimeType can be wrong, especially if several uris were received + // in the same intent (i.e. 'image/*'). We need to check the mime type of each uri. + val mimeType = context.contentResolver.getType(uri) ?: fallbackMimeType ShareIntentHandler.UriToShare( uri = uri, - mimeType = type + mimeType = mimeType, ) } } diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt index 1642578863b..6dac12a6e28 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt @@ -18,21 +18,27 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import io.element.android.services.appnavstate.api.ActiveRoomsHolder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException class SharePresenter @AssistedInject constructor( @Assisted private val intent: Intent, - private val appCoroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val shareIntentHandler: ShareIntentHandler, private val matrixClient: MatrixClient, private val mediaPreProcessor: MediaPreProcessor, private val sessionPreferencesStore: SessionPreferencesStore, + private val activeRoomsHolder: ActiveRoomsHolder, ) : Presenter { @AssistedFactory interface Factory { @@ -42,7 +48,7 @@ class SharePresenter @AssistedInject constructor( private val shareActionState: MutableState>> = mutableStateOf(AsyncAction.Uninitialized) fun onRoomSelected(roomIds: List) { - appCoroutineScope.share(intent, roomIds) + sessionCoroutineScope.share(intent, roomIds) } @Composable @@ -59,6 +65,12 @@ class SharePresenter @AssistedInject constructor( ) } + private suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? { + return activeRoomsHolder.getActiveRoom(matrixClient.sessionId) + ?.takeIf { it.roomId == roomId } + ?: matrixClient.getJoinedRoom(roomId) + } + private fun CoroutineScope.share( intent: Intent, roomIds: List, @@ -72,7 +84,7 @@ class SharePresenter @AssistedInject constructor( } else { roomIds .map { roomId -> - val room = matrixClient.getRoom(roomId) ?: return@map false + val room = getJoinedRoom(roomId) ?: return@map false val mediaSender = MediaSender( preProcessor = mediaPreProcessor, room = room, @@ -80,12 +92,26 @@ class SharePresenter @AssistedInject constructor( ) filesToShare .map { fileToShare -> - mediaSender.sendMedia( + val result = mediaSender.sendMedia( uri = fileToShare.uri, mimeType = fileToShare.mimeType, - ).isSuccess + ) + // If the coroutine was cancelled, destroy the room and rethrow the exception + val cancellationException = result.exceptionOrNull() as? CancellationException + if (cancellationException != null) { + if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) { + room.destroy() + } + throw cancellationException + } + result.isSuccess + } + .all { isSuccess -> isSuccess } + .also { + if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) { + room.destroy() + } } - .all { it } } .all { it } } @@ -93,7 +119,7 @@ class SharePresenter @AssistedInject constructor( onPlainText = { text -> roomIds .map { roomId -> - matrixClient.getRoom(roomId)?.sendMessage( + getJoinedRoom(roomId)?.liveTimeline?.sendMessage( body = text, htmlBody = null, intentionalMentions = emptyList(), diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt index 62664cdd0d2..5afc622fb9c 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt @@ -24,7 +24,7 @@ open class ShareStateProvider : PreviewParameterProvider { ) ), aShareState( - shareAction = AsyncAction.Failure(Throwable("error")), + shareAction = AsyncAction.Failure(RuntimeException("error")), ), ) } diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt index 866170daa3f..979d6aa61c9 100644 --- a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt +++ b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt @@ -18,14 +18,17 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore +import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.test.TestScope @@ -89,11 +92,13 @@ class SharePresenterTest { @Test fun `present - send text ok`() = runTest { - val matrixRoom = FakeMatrixRoom( - sendMessageResult = { _, _, _ -> Result.success(Unit) }, + val joinedRoom = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendMessageLambda = { _, _, _ -> Result.success(Unit) } + }, ) val matrixClient = FakeMatrixClient().apply { - givenGetRoomResult(A_ROOM_ID, matrixRoom) + givenGetRoomResult(A_ROOM_ID, joinedRoom) } val presenter = createSharePresenter( matrixClient = matrixClient, @@ -116,14 +121,17 @@ class SharePresenterTest { @Test fun `present - send media ok`() = runTest { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } - val matrixRoom = FakeMatrixRoom( - sendFileResult = sendFileResult, + val joinedRoom = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val matrixClient = FakeMatrixClient().apply { - givenGetRoomResult(A_ROOM_ID, matrixRoom) + givenGetRoomResult(A_ROOM_ID, joinedRoom) } val presenter = createSharePresenter( matrixClient = matrixClient, @@ -156,15 +164,17 @@ class SharePresenterTest { intent: Intent = Intent(), shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(), matrixClient: MatrixClient = FakeMatrixClient(), - mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor() + mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(), + activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(), ): SharePresenter { return SharePresenter( intent = intent, - appCoroutineScope = this, + sessionCoroutineScope = this, shareIntentHandler = shareIntentHandler, matrixClient = matrixClient, mediaPreProcessor = mediaPreProcessor, - InMemorySessionPreferencesStore(), + sessionPreferencesStore = InMemorySessionPreferencesStore(), + activeRoomsHolder = activeRoomsHolder, ) } } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt index dd7c24ab359..773954c3e0e 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt @@ -34,7 +34,9 @@ class SignedOutPresenter @AssistedInject constructor( @Composable override fun present(): SignedOutState { - val sessions by sessionStore.sessionsFlow().collectAsState(initial = emptyList()) + val sessions by remember { + sessionStore.sessionsFlow() + }.collectAsState(initial = emptyList()) val signedOutSession by remember { derivedStateOf { sessions.firstOrNull { it.userId == sessionId } } } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt index 85c9934cdcd..969da07f40e 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.runtime.Composable import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier @@ -63,7 +61,7 @@ private fun SignedOutHeader(state: SignedOutState) { modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = stringResource(id = R.string.screen_signed_out_title), subTitle = stringResource(id = R.string.screen_signed_out_subtitle, state.appName), - iconStyle = BigIcon.Style.Default(Icons.Filled.AccountCircle), + iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()), ) } diff --git a/features/signedout/impl/src/main/res/values-cy/translations.xml b/features/signedout/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..520a22d8fcd --- /dev/null +++ b/features/signedout/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,8 @@ + + + "Rydych chi wedi newid eich cyfrinair yn ystod sesiwn arall" + "Rydych chi wedi dileu\'r sesiwn o sesiwn arall" + "Mae gweinyddwr eich gweinydd wedi annilysu eich mynediad" + "Efallai eich bod wedi cael eich allgofnodi am un o\'r rhesymau sy\'n cael eu rhestru isod. Mewngofnodwch eto i barhau i ddefnyddio %s." + "Rydych wedi\'ch allgofnodi" + diff --git a/features/signedout/impl/src/main/res/values-da/translations.xml b/features/signedout/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..3bfc9f49965 --- /dev/null +++ b/features/signedout/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,8 @@ + + + "Du har ændret din adgangskode på en anden session" + "Du har slettet sessionen fra en anden session" + "Din serveradministrator har lukket for din adgang" + "Du er muligvis blevet logget ud på grund af en af nedenstående årsager. Log ind igen for at fortsætte med at bruge%s." + "Du er logget ud" + diff --git a/features/signedout/impl/src/main/res/values-de/translations.xml b/features/signedout/impl/src/main/res/values-de/translations.xml index 08e0305f8e0..4b631a70202 100644 --- a/features/signedout/impl/src/main/res/values-de/translations.xml +++ b/features/signedout/impl/src/main/res/values-de/translations.xml @@ -1,8 +1,8 @@ - "Du hast dein Passwort in einer anderen Sitzung geändert" - "Du hast die Sitzung aus einer anderen Sitzung gelöscht" + "Sie haben Ihr Passwort in einer anderen Sitzung geändert" + "Sie haben diese Sitzung aus einer anderen Sitzung gelöscht" "Der Administrator deines Servers hat deinen Zugang ungültig gemacht" - "Möglicherweise wurdest du aus einem der unten aufgeführten Gründe abgemeldet. Bitte melde dich erneut an, um %s weiter zu nutzen." - "Du bist abgemeldet" + "Möglicherweise wurden Sie aus einem der unten aufgeführten Gründe abgemeldet. Bitte melden Sie erneut an, um %s weiter zu nutzen." + "Sie sind abgemeldet" diff --git a/features/signedout/impl/src/main/res/values-es/translations.xml b/features/signedout/impl/src/main/res/values-es/translations.xml index b4ec370c956..2044f70efea 100644 --- a/features/signedout/impl/src/main/res/values-es/translations.xml +++ b/features/signedout/impl/src/main/res/values-es/translations.xml @@ -3,6 +3,6 @@ "Has cambiado tu contraseña en otra sesión" "Has eliminado la sesión desde otra sesión" "El administrador de tu servidor ha invalidado su acceso" - "Es posible que haya cerrado sesión por uno de los motivos que se enumeran a continuación. Por favor inicia sesión nuevamente para continuar usando %s ." + "Es posible que haya cerrado sesión por uno de los motivos que se enumeran a continuación. Por favor inicia sesión nuevamente para continuar usando %s." "Has cerrado sesión" diff --git a/features/signedout/impl/src/main/res/values-eu/translations.xml b/features/signedout/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..678a775fadc --- /dev/null +++ b/features/signedout/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,8 @@ + + + "Pasahitza beste saio batean aldatu duzu" + "Saioa beste saio batetik ezabatu duzu" + "Zerbitzariaren administratzaileak zure sarbidea baliogabetu du" + "Litekeena da saioa amaitu izana ondorengo arrazoietako bat dela eta. Hasi saioa berriro %s erabiltzen jarraitzeko." + "Saioa amaitu duzu" + diff --git a/features/signedout/impl/src/main/res/values-fa/translations.xml b/features/signedout/impl/src/main/res/values-fa/translations.xml index bd5680df004..a2336114f07 100644 --- a/features/signedout/impl/src/main/res/values-fa/translations.xml +++ b/features/signedout/impl/src/main/res/values-fa/translations.xml @@ -2,5 +2,7 @@ "گذرواژه‌تان را در نشستی دیگر تغییر داده‌اید" "این نشست را از نشستی دیگر حذف کرده‌اید" + "مدیر سرور شما دسترسی شما را لغو کرده است." + "ممکن است به یکی از دلایل ذکر شده در زیر از سیستم خارج شده باشید. لطفا برای ادامه استفاده از %s دوباره وارد شوید." "خارج شده‌اید" diff --git a/features/signedout/impl/src/main/res/values-nb/translations.xml b/features/signedout/impl/src/main/res/values-nb/translations.xml index fa9893b5ba1..58aa25916e1 100644 --- a/features/signedout/impl/src/main/res/values-nb/translations.xml +++ b/features/signedout/impl/src/main/res/values-nb/translations.xml @@ -1,4 +1,8 @@ + "Du har endret passordet ditt i en annen sesjon" + "Du har slettet sesjonen fra en annen sesjon" + "Serveradministratoren har ugyldiggjort tilgangen din" + "Du kan ha blitt logget ut av en av årsakene som er oppført nedenfor. Logg på igjen for å fortsette å bruke %s." "Du er logget ut" diff --git a/features/signedout/impl/src/main/res/values-pt/translations.xml b/features/signedout/impl/src/main/res/values-pt/translations.xml index d7e6ad89484..95f0c980b5c 100644 --- a/features/signedout/impl/src/main/res/values-pt/translations.xml +++ b/features/signedout/impl/src/main/res/values-pt/translations.xml @@ -1,7 +1,7 @@ "Alteraste a tua senha noutra sessão" - "Apagaste esta sessão a partir de outra" + "Eliminaste esta sessão a partir de outra" "O administrador do teu servidor invalidou o teu acesso" "A tua sessão pode ter sido terminada por um dos motivos indicados abaixo. Inicia sessão novamente para continuares a utilizar a %s." "Não tens sessão iniciada" diff --git a/features/signedout/impl/src/main/res/values-tr/translations.xml b/features/signedout/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..0da098f8229 --- /dev/null +++ b/features/signedout/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,8 @@ + + + "Başka bir oturumda şifrenizi değiştirdiniz" + "Oturumu başka bir oturumdan sildiniz" + "Sunucunuzun yöneticisi erişiminizi geçersiz kıldı" + "Aşağıda listelenen nedenlerden biri nedeniyle oturumunuz kapatılmış olabilir. %s kullanmaya devam etmek için lütfen tekrar oturum açın." + "Oturumunuz kapatıldı" + diff --git a/features/signedout/impl/src/main/res/values-ur/translations.xml b/features/signedout/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..3227c69f23e --- /dev/null +++ b/features/signedout/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,8 @@ + + + "آپ نے دوسرے جلسے میں اپنا لفظِ عبور تبدیل کر لیا ہے" + "آپ نے جلسہ کو دوسرے جلسے سے حذف کر دیا ہے" + "آپ کے خادم کے منتظم نے آپکی رسائی کو باطل کردیا" + "ہوسکتا ہے کہ آپ کو ذیل میں درج وجوہات میں سے کسی ایک کی وجہ سے خارج کیا گیا ہو۔ برائے مہربانی %s کا استعمال جاری رکھنے کے لئے دوبارہ داخل ہوں۔" + "آپ خارج ہوگئے ہیں" + diff --git a/features/signedout/impl/src/main/res/values-uz/translations.xml b/features/signedout/impl/src/main/res/values-uz/translations.xml index bb386ee54f1..3d25b051796 100644 --- a/features/signedout/impl/src/main/res/values-uz/translations.xml +++ b/features/signedout/impl/src/main/res/values-uz/translations.xml @@ -3,6 +3,6 @@ "Siz boshqa seansda parolingizni o\'zgartirdingiz" "Siz seansni boshqa seansdan o\'chirib tashladingiz" "Serveringiz administratori ruxsatingizni bekor qildi" - "Siz quyida sanab o‘tilgan sabablardan biri tufayli tizimdan chiqqan bo‘lishingiz mumkin. Foydalanishni davom ettirish uchun qayta kiring%s ." + "Siz quyida sanab o‘tilgan sabablardan biri tufayli tizimdan chiqqan bo‘lishingiz mumkin. Foydalanishni davom ettirish uchun qayta kiring%s." "Hisobingizdan chiqdingiz" diff --git a/features/userprofile/api/build.gradle.kts b/features/userprofile/api/build.gradle.kts index b2c1068556a..8bdaa8d77e2 100644 --- a/features/userprofile/api/build.gradle.kts +++ b/features/userprofile/api/build.gradle.kts @@ -16,5 +16,6 @@ android { dependencies { implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) implementation(projects.libraries.matrix.api) } diff --git a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt index 77917e31062..4a5f6bb415e 100644 --- a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt @@ -14,4 +14,6 @@ sealed interface UserProfileEvents { data class UnblockUser(val needsConfirmation: Boolean = false) : UserProfileEvents data object ClearBlockUserError : UserProfileEvents data object ClearConfirmationDialog : UserProfileEvents + data object WithdrawVerification : UserProfileEvents + data class CopyToClipboard(val text: String) : UserProfileEvents } 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 5d3ae9c1320..b9f08a27f32 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 @@ -9,6 +9,7 @@ package io.element.android.features.userprofile.api import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -16,13 +17,14 @@ data class UserProfileState( val userId: UserId, val userName: String?, val avatarUrl: String?, - val isVerified: AsyncData, + val verificationState: UserProfileVerificationState, val isBlocked: AsyncData, val startDmActionState: AsyncAction, val displayConfirmationDialog: ConfirmationDialog?, val isCurrentUser: Boolean, val dmRoomId: RoomId?, val canCall: Boolean, + val snackbarMessage: SnackbarMessage?, val eventSink: (UserProfileEvents) -> Unit ) { enum class ConfirmationDialog { @@ -30,3 +32,10 @@ data class UserProfileState( Unblock } } + +enum class UserProfileVerificationState { + UNKNOWN, + VERIFIED, + UNVERIFIED, + VERIFICATION_VIOLATION, +} diff --git a/features/userprofile/impl/build.gradle.kts b/features/userprofile/impl/build.gradle.kts index 2c94082d0c1..4242efcb985 100644 --- a/features/userprofile/impl/build.gradle.kts +++ b/features/userprofile/impl/build.gradle.kts @@ -33,6 +33,8 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.mediaviewer.api) implementation(projects.features.call.api) + implementation(projects.features.enterprise.api) + implementation(projects.features.verifysession.api) api(projects.features.userprofile.api) api(projects.features.userprofile.shared) implementation(libs.coil.compose) @@ -48,6 +50,7 @@ dependencies { testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.createroom.test) + testImplementation(projects.features.enterprise.test) testImplementation(projects.tests.testutils) testImplementation(libs.androidx.compose.ui.test.junit) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) 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 489d82480cb..e67a00a8b1b 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 @@ -25,6 +25,7 @@ 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 import io.element.android.features.userprofile.shared.UserProfileNodeHelper +import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode @@ -32,7 +33,9 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope 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.user.CurrentSessionIdHolder +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import kotlinx.parcelize.Parcelize @@ -43,6 +46,7 @@ class UserProfileFlowNode @AssistedInject constructor( private val elementCallEntryPoint: ElementCallEntryPoint, private val sessionIdHolder: CurrentSessionIdHolder, private val mediaViewerEntryPoint: MediaViewerEntryPoint, + private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -57,8 +61,13 @@ class UserProfileFlowNode @AssistedInject constructor( @Parcelize data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget + + @Parcelize + data class VerifyUser(val userId: UserId) : NavTarget } + private val inputs = inputs() + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { @@ -74,8 +83,12 @@ class UserProfileFlowNode @AssistedInject constructor( override fun onStartCall(dmRoomId: RoomId) { elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = dmRoomId)) } + + override fun onVerifyUser(userId: UserId) { + backstack.push(NavTarget.VerifyUser(userId)) + } } - val params = UserProfileNode.UserProfileInputs(userId = inputs().userId) + val params = UserProfileNode.UserProfileInputs(userId = inputs.userId) createNode(buildContext, listOf(callback, params)) } is NavTarget.AvatarPreview -> { @@ -96,6 +109,15 @@ class UserProfileFlowNode @AssistedInject constructor( .callback(callback) .build() } + is NavTarget.VerifyUser -> { + val params = OutgoingVerificationEntryPoint.Params( + showDeviceVerifiedScreen = false, + verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId) + ) + outgoingVerificationEntryPoint.nodeBuilder(this, buildContext) + .params(params) + .build() + } } } diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt index 1b99e8409bf..f10fd48a04e 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt @@ -42,7 +42,7 @@ class UserProfileNode @AssistedInject constructor( private val inputs = inputs() private val callback = inputs() - private val presenter = presenterFactory.create(inputs.userId) + private val presenter = presenterFactory.create(userId = inputs.userId) private val userProfileNodeHelper = UserProfileNodeHelper(inputs.userId) init { @@ -75,6 +75,7 @@ class UserProfileNode @AssistedInject constructor( onOpenDm = ::onStartDM, onStartCall = callback::onStartCall, openAvatarPreview = callback::openAvatarPreview, + onVerifyClick = callback::onVerifyUser, ) } } 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 1695ea886ce..4de79b8375e 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 @@ -21,13 +21,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.createroom.api.StartDMAction +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileState.ConfirmationDialog +import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -44,6 +45,7 @@ class UserProfilePresenter @AssistedInject constructor( @Assisted private val userId: UserId, private val client: MatrixClient, private val startDMAction: StartDMAction, + private val sessionEnterpriseService: SessionEnterpriseService, ) : Presenter { @AssistedFactory interface Factory { @@ -53,17 +55,27 @@ class UserProfilePresenter @AssistedInject constructor( @Composable private fun getDmRoomId(): State { return produceState(initialValue = null) { - value = client.findDM(userId) + value = client.findDM(userId).getOrNull() } } @Composable private fun getCanCall(roomId: RoomId?): State { - return produceState(initialValue = false, roomId) { - value = if (client.isMe(userId)) { - false - } else { - roomId?.let { client.getRoom(it)?.canUserJoinCall(client.sessionId)?.getOrNull() == true }.orFalse() + val isElementCallAvailable by produceState(initialValue = false, roomId) { + value = sessionEnterpriseService.isElementCallAvailable() + } + + return produceState(initialValue = false, isElementCallAvailable, roomId) { + value = when { + isElementCallAvailable.not() -> false + client.isMe(userId) -> false + else -> + roomId + ?.let { client.getRoom(it) } + ?.use { room -> + room.canUserJoinCall(client.sessionId).getOrNull() + } + .orFalse() } } } @@ -73,10 +85,8 @@ class UserProfilePresenter @AssistedInject constructor( val coroutineScope = rememberCoroutineScope() val isCurrentUser = remember { client.isMe(userId) } var confirmationDialog by remember { mutableStateOf(null) } - var userProfile by remember { mutableStateOf(null) } val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val isBlocked: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } - val isVerified: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } val dmRoomId by getDmRoomId() val canCall by getCanCall(dmRoomId) LaunchedEffect(Unit) { @@ -86,14 +96,7 @@ class UserProfilePresenter @AssistedInject constructor( .onEach { isBlocked.value = AsyncData.Success(it) } .launchIn(this) } - LaunchedEffect(Unit) { - userProfile = client.getProfile(userId).getOrNull() - } - LaunchedEffect(Unit) { - suspend { - client.encryptionService().isUserVerified(userId).getOrThrow() - }.runCatchingUpdatingState(isVerified) - } + val userProfile by produceState(null) { value = client.getProfile(userId).getOrNull() } fun handleEvents(event: UserProfileEvents) { when (event) { @@ -119,12 +122,19 @@ class UserProfilePresenter @AssistedInject constructor( } UserProfileEvents.StartDM -> { coroutineScope.launch { - startDMAction.execute(userId, startDmActionState) + startDMAction.execute( + matrixUser = userProfile ?: MatrixUser(userId), + createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming, + actionState = startDmActionState, + ) } } UserProfileEvents.ClearStartDMState -> { startDmActionState.value = AsyncAction.Uninitialized } + // Do nothing for other event as they are handled by the RoomMemberDetailsPresenter if needed + UserProfileEvents.WithdrawVerification, + is UserProfileEvents.CopyToClipboard -> Unit } } @@ -133,12 +143,13 @@ class UserProfilePresenter @AssistedInject constructor( userName = userProfile?.displayName, avatarUrl = userProfile?.avatarUrl, isBlocked = isBlocked.value, - isVerified = isVerified.value, + verificationState = UserProfileVerificationState.UNKNOWN, startDmActionState = startDmActionState.value, displayConfirmationDialog = confirmationDialog, isCurrentUser = isCurrentUser, dmRoomId = dmRoomId, canCall = canCall, + snackbarMessage = null, eventSink = ::handleEvents ) } 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 80bfd81a8af..2ce7f99c2c2 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 @@ -7,31 +7,45 @@ package io.element.android.features.userprofile.impl +import androidx.compose.runtime.MutableState +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine +import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.createroom.test.FakeStartDMAction +import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState +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.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.user.MatrixUser 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_THROWABLE import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.awaitLastSequentialItem +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.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -56,7 +70,7 @@ class UserProfilePresenterTest { assertThat(initialState.userName).isEqualTo(matrixUser.displayName) assertThat(initialState.avatarUrl).isEqualTo(matrixUser.avatarUrl) assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(false)) - assertThat(initialState.isVerified.dataOrNull()).isFalse() + assertThat(initialState.verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN) assertThat(initialState.dmRoomId).isEqualTo(A_ROOM_ID) assertThat(initialState.canCall).isFalse() } @@ -66,6 +80,8 @@ class UserProfilePresenterTest { fun `present - canCall is true when all the conditions are met`() { testCanCall( expectedResult = true, + skipItems = 3, + checkThatRoomIsDestroyed = true, ) } @@ -101,29 +117,44 @@ class UserProfilePresenterTest { ) } + @Test + fun `present - canCall is false when call is not available`() { + testCanCall( + isElementCallAvailable = false, + expectedResult = false, + ) + } + private fun testCanCall( + isElementCallAvailable: Boolean = true, canUserJoinCallResult: Result = Result.success(true), dmRoom: RoomId? = A_ROOM_ID, canFindRoom: Boolean = true, expectedResult: Boolean, + skipItems: Int = 1, + checkThatRoomIsDestroyed: Boolean = false, ) = runTest { - val room = FakeMatrixRoom( + val room = FakeBaseRoom( canUserJoinCallResult = { canUserJoinCallResult }, ) val client = createFakeMatrixClient().apply { if (canFindRoom) { givenGetRoomResult(A_ROOM_ID, room) } - givenFindDmResult(dmRoom) + givenFindDmResult(Result.success(dmRoom)) } val presenter = createUserProfilePresenter( userId = A_USER_ID_2, client = client, + isElementCallAvailable = isElementCallAvailable, ) presenter.test { - val initialState = awaitLastSequentialItem() + val initialState = awaitFirstItem(skipItems) assertThat(initialState.canCall).isEqualTo(expectedResult) } + if (checkThatRoomIsDestroyed) { + room.assertDestroyed() + } } @Test @@ -160,7 +191,8 @@ class UserProfilePresenterTest { @Test fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { - val client = createFakeMatrixClient() + val ignoredUsersFlow = MutableStateFlow(persistentListOf()) + val client = createFakeMatrixClient(ignoredUsersFlow = ignoredUsersFlow) val presenter = createUserProfilePresenter( client = client, userId = A_USER_ID @@ -169,27 +201,28 @@ class UserProfilePresenterTest { val initialState = awaitFirstItem() initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() - client.emitIgnoreUserList(listOf(A_USER_ID)) + ignoredUsersFlow.emit(persistentListOf(A_USER_ID)) assertThat(awaitItem().isBlocked.dataOrNull()).isTrue() initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() - client.emitIgnoreUserList(listOf()) + ignoredUsersFlow.emit(persistentListOf()) assertThat(awaitItem().isBlocked.dataOrNull()).isFalse() } } @Test fun `present - BlockUser with error`() = runTest { - val matrixClient = createFakeMatrixClient() - matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE)) + val matrixClient = createFakeMatrixClient( + ignoreUserResult = { Result.failure(AN_EXCEPTION) } + ) val presenter = createUserProfilePresenter(client = matrixClient) presenter.test { - val initialState = awaitFirstItem() + val initialState = awaitFirstItem(count = 2) initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() val errorState = awaitItem() - assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE) + assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(AN_EXCEPTION) // Clear error initialState.eventSink(UserProfileEvents.ClearBlockUserError) assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false)) @@ -198,15 +231,16 @@ class UserProfilePresenterTest { @Test fun `present - UnblockUser with error`() = runTest { - val matrixClient = createFakeMatrixClient() - matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE)) + val matrixClient = createFakeMatrixClient( + unIgnoreUserResult = { Result.failure(AN_EXCEPTION) } + ) val presenter = createUserProfilePresenter(client = matrixClient) presenter.test { - val initialState = awaitFirstItem() + val initialState = awaitFirstItem(count = 2) initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() val errorState = awaitItem() - assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE) + assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(AN_EXCEPTION) // Clear error initialState.eventSink(UserProfileEvents.ClearBlockUserError) assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true)) @@ -229,72 +263,156 @@ class UserProfilePresenterTest { } @Test - fun `present - start DM action complete scenario`() = runTest { - val startDMAction = FakeStartDMAction() + fun `present - start DM action failure scenario`() = runTest { + val startDMFailureResult = AsyncAction.Failure(AN_EXCEPTION) + val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> + actionState.value = startDMFailureResult + } + val startDMAction = FakeStartDMAction(executeResult = executeResult) val presenter = createUserProfilePresenter(startDMAction = startDMAction) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { val initialState = awaitFirstItem() assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) - val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID) - val startDMFailureResult = AsyncAction.Failure(A_THROWABLE) - - // Failure - startDMAction.givenExecuteResult(startDMFailureResult) + val matrixUser = MatrixUser(UserId("@alice:server.org")) initialState.eventSink(UserProfileEvents.StartDM) - assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java) awaitItem().also { state -> assertThat(state.startDmActionState).isEqualTo(startDMFailureResult) + executeResult.assertions().isCalledOnce().with( + value(matrixUser), + value(false), + any(), + ) state.eventSink(UserProfileEvents.ClearStartDMState) } - - // Success - startDMAction.givenExecuteResult(startDMSuccessResult) awaitItem().also { state -> - assertThat(state.startDmActionState).isEqualTo(AsyncAction.Uninitialized) - state.eventSink(UserProfileEvents.StartDM) + assertThat(state.startDmActionState.isUninitialized()).isTrue() } - assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java) + } + } + + @Test + fun `present - start DM action success scenario`() = runTest { + val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID) + val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> + actionState.value = startDMSuccessResult + } + val startDMAction = FakeStartDMAction(executeResult = executeResult) + val presenter = createUserProfilePresenter(startDMAction = startDMAction) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) + val matrixUser = MatrixUser(UserId("@alice:server.org")) + initialState.eventSink(UserProfileEvents.StartDM) awaitItem().also { state -> assertThat(state.startDmActionState).isEqualTo(startDMSuccessResult) + executeResult.assertions().isCalledOnce().with( + value(matrixUser), + value(false), + any(), + ) } } } @Test - fun `present - when user is verified, the value in the state is true`() = runTest { - val client = createFakeMatrixClient(isUserVerified = true) - val presenter = createUserProfilePresenter( - client = client, - ) - presenter.test { - assertThat(awaitItem().isVerified.isUninitialized()).isTrue() - assertThat(awaitItem().isVerified.isLoading()).isTrue() - assertThat(awaitItem().isVerified.dataOrNull()).isTrue() + fun `present - start DM action confirmation scenario - cancel`() = runTest { + val matrixUser = MatrixUser(UserId("@alice:server.org")) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> + actionState.value = startDMConfirmationResult + } + val startDMAction = FakeStartDMAction(executeResult = executeResult) + val presenter = createUserProfilePresenter(startDMAction = startDMAction) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) + initialState.eventSink(UserProfileEvents.StartDM) + val confirmingState = awaitItem() + assertThat(confirmingState.startDmActionState).isEqualTo(startDMConfirmationResult) + executeResult.assertions().isCalledOnce().with( + value(matrixUser), + value(false), + any(), + ) + // Cancelling should not create the DM + confirmingState.eventSink(UserProfileEvents.ClearStartDMState) + val finalState = awaitItem() + assertThat(finalState.startDmActionState.isUninitialized()).isTrue() + executeResult.assertions().isCalledExactly(1) + } + } + + @Test + fun `present - start DM action confirmation scenario - confirm`() = runTest { + val matrixUser = MatrixUser(UserId("@alice:server.org")) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> + actionState.value = startDMConfirmationResult + } + val startDMAction = FakeStartDMAction(executeResult = executeResult) + val presenter = createUserProfilePresenter(startDMAction = startDMAction) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) + initialState.eventSink(UserProfileEvents.StartDM) + val confirmingState = awaitItem() + assertThat(confirmingState.startDmActionState).isEqualTo(startDMConfirmationResult) + executeResult.assertions().isCalledOnce().with( + value(matrixUser), + value(false), + any(), + ) + // Start DM again should invoke the action with createIfDmDoesNotExist = true + confirmingState.eventSink(UserProfileEvents.StartDM) + executeResult.assertions().isCalledExactly(2).withSequence( + listOf(value(matrixUser), value(false), any()), + listOf(value(matrixUser), value(true), any()), + ) } } - private suspend fun ReceiveTurbine.awaitFirstItem(): T { - skipItems(2) + private suspend fun ReceiveTurbine.awaitFirstItem(count: Int = 1): T { + skipItems(count) return awaitItem() } private fun createFakeMatrixClient( - isUserVerified: Boolean = false, + isUserVerified: Boolean = true, + userIdentityState: IdentityState? = null, + ignoreUserResult: (UserId) -> Result = { Result.success(Unit) }, + unIgnoreUserResult: (UserId) -> Result = { Result.success(Unit) }, + ignoredUsersFlow: StateFlow> = MutableStateFlow(persistentListOf()) ) = FakeMatrixClient( encryptionService = FakeEncryptionService( - isUserVerifiedResult = { Result.success(isUserVerified) } + isUserVerifiedResult = { Result.success(isUserVerified) }, + getUserIdentityResult = { Result.success(userIdentityState) } ), + ignoreUserResult = ignoreUserResult, + unIgnoreUserResult = unIgnoreUserResult, + ignoredUsersFlow = ignoredUsersFlow, ) private fun createUserProfilePresenter( client: MatrixClient = createFakeMatrixClient(), userId: UserId = UserId("@alice:server.org"), - startDMAction: StartDMAction = FakeStartDMAction() + startDMAction: StartDMAction = FakeStartDMAction(), + isElementCallAvailable: Boolean = true, ): UserProfilePresenter { return UserProfilePresenter( userId = userId, client = client, - startDMAction = startDMAction + startDMAction = startDMAction, + sessionEnterpriseService = FakeSessionEnterpriseService( + isElementCallAvailableResult = { isElementCallAvailable }, + ), ) } } 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 1a59e43fc6c..415a6e32825 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 @@ -13,24 +13,31 @@ 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.material3.MaterialTheme +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.draw.clip 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.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.libraries.architecture.AsyncData +import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule 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.modifiers.niceClickable import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +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 import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.testtags.TestTags @@ -43,8 +50,10 @@ fun UserProfileHeaderSection( avatarUrl: String?, userId: UserId, userName: String?, - isUserVerified: AsyncData, + verificationState: UserProfileVerificationState, openAvatarPreview: (url: String) -> Unit, + onUserIdClick: () -> Unit, + withdrawVerificationClick: () -> Unit, modifier: Modifier = Modifier ) { Column( @@ -55,14 +64,26 @@ fun UserProfileHeaderSection( ) { Avatar( avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader), + avatarType = AvatarType.User, + contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_user_avatar) }, modifier = Modifier - .clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) } + .clip(CircleShape) + .clickable( + enabled = avatarUrl != null, + onClickLabel = stringResource(CommonStrings.action_view), + ) { + openAvatarPreview(avatarUrl!!) + } .testTag(TestTags.memberDetailAvatar) ) Spacer(modifier = Modifier.height(24.dp)) if (userName != null) { Text( - modifier = Modifier.clipToBounds(), + modifier = Modifier + .clipToBounds() + .semantics { + heading() + }, text = userName, style = ElementTheme.typography.fontHeadingLgBold, textAlign = TextAlign.Center, @@ -70,21 +91,43 @@ fun UserProfileHeaderSection( Spacer(modifier = Modifier.height(6.dp)) } Text( + modifier = Modifier.niceClickable { onUserIdClick() }, text = userId.value, style = ElementTheme.typography.fontBodyLgRegular, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, textAlign = TextAlign.Center, ) - if (isUserVerified.dataOrNull() == true) { - MatrixBadgeRowMolecule( - data = listOf( - MatrixBadgeAtom.MatrixBadgeData( - text = stringResource(CommonStrings.common_verified), - icon = CompoundIcons.Verified(), - type = MatrixBadgeAtom.Type.Positive, - ) - ).toImmutableList(), - ) + when (verificationState) { + UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit + UserProfileVerificationState.VERIFIED -> { + MatrixBadgeRowMolecule( + data = listOf( + MatrixBadgeAtom.MatrixBadgeData( + text = stringResource(CommonStrings.common_verified), + icon = CompoundIcons.Verified(), + type = MatrixBadgeAtom.Type.Positive, + ) + ).toImmutableList(), + ) + } + UserProfileVerificationState.VERIFICATION_VIOLATION -> { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(CommonStrings.crypto_identity_change_profile_pin_violation, userName ?: userId.value), + color = ElementTheme.colors.textCriticalPrimary, + style = ElementTheme.typography.fontBodyMdMedium, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + size = ButtonSize.MediumLowPadding, + text = stringResource(CommonStrings.crypto_identity_change_withdraw_verification_action), + onClick = withdrawVerificationClick, + ) + } } Spacer(Modifier.height(40.dp)) } @@ -97,7 +140,23 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview { avatarUrl = null, userId = UserId("@alice:example.com"), userName = "Alice", - isUserVerified = AsyncData.Success(true), + verificationState = UserProfileVerificationState.VERIFIED, + openAvatarPreview = {}, + onUserIdClick = {}, + withdrawVerificationClick = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun UserProfileHeaderSectionWithVerificationViolationPreview() = ElementPreview { + UserProfileHeaderSection( + avatarUrl = null, + userId = UserId("@alice:example.com"), + userName = "Alice", + verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION, openAvatarPreview = {}, + onUserIdClick = {}, + withdrawVerificationClick = {}, ) } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt index deb72a5baba..61f46697690 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt @@ -24,6 +24,7 @@ class UserProfileNodeHelper( fun openAvatarPreview(username: String, avatarUrl: String) fun onStartDM(roomId: RoomId) fun onStartCall(dmRoomId: RoomId) + fun onVerifyUser(userId: UserId) } fun onShareUser( 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 01a36077fb9..7a5cc532399 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 @@ -8,25 +8,30 @@ package io.element.android.features.userprofile.shared import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState +import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.components.aMatrixUser open class UserProfileStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aUserProfileState(), aUserProfileState(userName = null), - aUserProfileState(isBlocked = AsyncData.Success(true), isVerified = AsyncData.Success(true)), + aUserProfileState(isBlocked = AsyncData.Success(true), verificationState = UserProfileVerificationState.VERIFIED), aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block), aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock), - aUserProfileState(isBlocked = AsyncData.Loading(true), isVerified = AsyncData.Loading()), + aUserProfileState(isBlocked = AsyncData.Loading(true), verificationState = UserProfileVerificationState.UNKNOWN), aUserProfileState(startDmActionState = AsyncAction.Loading), aUserProfileState(canCall = true), - // Add other states here + aUserProfileState(startDmActionState = ConfirmingStartDmWithMatrixUser(aMatrixUser())), + aUserProfileState(verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION), ) } @@ -35,23 +40,25 @@ fun aUserProfileState( userName: String? = "Daniel", avatarUrl: String? = null, isBlocked: AsyncData = AsyncData.Success(false), - isVerified: AsyncData = AsyncData.Success(false), + verificationState: UserProfileVerificationState = UserProfileVerificationState.UNVERIFIED, startDmActionState: AsyncAction = AsyncAction.Uninitialized, displayConfirmationDialog: UserProfileState.ConfirmationDialog? = null, isCurrentUser: Boolean = false, dmRoomId: RoomId? = null, canCall: Boolean = false, + snackbarMessage: SnackbarMessage? = null, eventSink: (UserProfileEvents) -> Unit = {}, ) = UserProfileState( userId = userId, userName = userName, avatarUrl = avatarUrl, isBlocked = isBlocked, - isVerified = isVerified, + verificationState = verificationState, startDmActionState = startDmActionState, displayConfirmationDialog = displayConfirmationDialog, isCurrentUser = isCurrentUser, dmRoomId = dmRoomId, canCall = canCall, + snackbarMessage = snackbarMessage, 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 6fb3e5ccb78..a43478e466f 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 @@ -21,8 +21,10 @@ import androidx.compose.ui.res.stringResource 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.createroom.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState +import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs import io.element.android.features.userprofile.shared.blockuser.BlockUserSection import io.element.android.libraries.designsystem.components.async.AsyncActionView @@ -36,7 +38,11 @@ import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost +import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet import io.element.android.libraries.ui.strings.CommonStrings @OptIn(ExperimentalMaterial3Api::class) @@ -48,28 +54,35 @@ fun UserProfileView( onStartCall: (RoomId) -> Unit, goBack: () -> Unit, openAvatarPreview: (username: String, url: String) -> Unit, + onVerifyClick: (UserId) -> Unit, modifier: Modifier = Modifier, ) { + val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) Scaffold( modifier = modifier, topBar = { TopAppBar(title = { }, navigationIcon = { BackButton(onClick = goBack) }) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, ) { padding -> Column( modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - .verticalScroll(rememberScrollState()) + .padding(padding) + .consumeWindowInsets(padding) + .verticalScroll(rememberScrollState()) ) { UserProfileHeaderSection( avatarUrl = state.avatarUrl, userId = state.userId, userName = state.userName, - isUserVerified = state.isVerified, + verificationState = state.verificationState, openAvatarPreview = { avatarUrl -> openAvatarPreview(state.userName ?: state.userId.value, avatarUrl) }, + onUserIdClick = { + state.eventSink(UserProfileEvents.CopyToClipboard(state.userId.value)) + }, + withdrawVerificationClick = { state.eventSink(UserProfileEvents.WithdrawVerification) }, ) UserProfileMainActionsSection( isCurrentUser = state.isCurrentUser, @@ -80,7 +93,7 @@ fun UserProfileView( ) Spacer(modifier = Modifier.height(26.dp)) if (!state.isCurrentUser) { - VerifyUserSection(state) + VerifyUserSection(state, onVerifyClick = { onVerifyClick(state.userId) }) BlockUserSection(state) BlockUserDialogs(state) } @@ -95,20 +108,34 @@ fun UserProfileView( errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) }, onRetry = { state.eventSink(UserProfileEvents.StartDM) }, onErrorDismiss = { state.eventSink(UserProfileEvents.ClearStartDMState) }, + confirmationDialog = { data -> + if (data is ConfirmingStartDmWithMatrixUser) { + CreateDmConfirmationBottomSheet( + matrixUser = data.matrixUser, + onSendInvite = { + state.eventSink(UserProfileEvents.StartDM) + }, + onDismiss = { + state.eventSink(UserProfileEvents.ClearStartDMState) + }, + ) + } + }, ) } } } @Composable -private fun VerifyUserSection(state: UserProfileState) { - if (state.isVerified.dataOrNull() == false) { +private fun VerifyUserSection( + state: UserProfileState, + onVerifyClick: () -> Unit, +) { + if (state.verificationState == UserProfileVerificationState.UNVERIFIED) { ListItem( - headlineContent = { Text(stringResource(CommonStrings.common_verify_identity)) }, - supportingContent = { Text(stringResource(R.string.screen_room_member_details_verify_button_subtitle)) }, + headlineContent = { Text(stringResource(CommonStrings.common_verify_user)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())), - enabled = false, - onClick = { }, + onClick = onVerifyClick, ) } } @@ -124,6 +151,7 @@ internal fun UserProfileViewPreview( goBack = {}, onOpenDm = {}, onStartCall = {}, - openAvatarPreview = { _, _ -> } + openAvatarPreview = { _, _ -> }, + onVerifyClick = {}, ) } diff --git a/features/userprofile/shared/src/main/res/values-bg/translations.xml b/features/userprofile/shared/src/main/res/values-bg/translations.xml index 67c73e3ea82..677034496c4 100644 --- a/features/userprofile/shared/src/main/res/values-bg/translations.xml +++ b/features/userprofile/shared/src/main/res/values-bg/translations.xml @@ -6,6 +6,8 @@ "Отблокиране на потребителя" "Блокиране" "Блокиране на потребителя" + "Профил" "Отблокиране" "Отблокиране на потребителя" + "Потвърждаване на %1$s" diff --git a/features/userprofile/shared/src/main/res/values-cy/translations.xml b/features/userprofile/shared/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..5fe1fa7cc7d --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-cy/translations.xml @@ -0,0 +1,19 @@ + + + "Rhwystro" + "Fydd defnyddwyr sydd wedi\'u rhwystro ddim yn gallu anfon negeseuon atoch a bydd eu holl negeseuon yn cael eu cuddio. Gallwch eu dadrwystro unrhyw bryd." + "Rhwystro defnyddiwr" + "Dad-rwystro" + "Byddwch yn gallu gweld pob neges oddi wrthyn nhw eto." + "Dadrwystro defnyddiwr" + "Rhwystro" + "Fydd defnyddwyr sydd wedi\'u rhwystro ddim yn gallu anfon negeseuon atoch a bydd eu holl negeseuon yn cael eu cuddio. Gallwch eu dadrwystro unrhyw bryd." + "Rhwystro defnyddiwr" + "Proffil" + "Dad-rwystro" + "Byddwch yn gallu gweld pob neges oddi wrthyn nhw eto." + "Dadrwystro defnyddiwr" + "Defnyddiwch yr ap gwe i ddilysu\'r defnyddiwr hwn." + "Dilysu %1$s" + "Digwyddodd gwall wrth geisio cychwyn sgwrs" + diff --git a/features/userprofile/shared/src/main/res/values-da/translations.xml b/features/userprofile/shared/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..7af21ebea9c --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-da/translations.xml @@ -0,0 +1,19 @@ + + + "Bloker" + "Blokerede brugere vil ikke være i stand til at sende dig beskeder, og alle deres beskeder vil blive skjult. Du kan fjerne blokeringen af dem når som helst." + "Bloker bruger" + "Fjern blokering" + "Du vil være i stand til at se alle beskeder fra dem igen." + "Fjern blokering af bruger" + "Bloker" + "Blokerede brugere vil ikke være i stand til at sende dig beskeder, og alle deres beskeder vil blive skjult. Du kan fjerne blokeringen af dem når som helst." + "Bloker bruger" + "Profil" + "Fjern blokering" + "Du vil være i stand til at se alle beskeder fra dem igen." + "Fjern blokering af bruger" + "Brug webappen til at verificere denne bruger." + "Verificér %1$s" + "Der opstod en fejl under forsøget på at starte en samtale" + diff --git a/features/userprofile/shared/src/main/res/values-de/translations.xml b/features/userprofile/shared/src/main/res/values-de/translations.xml index f96af58bae5..b1c791af37a 100644 --- a/features/userprofile/shared/src/main/res/values-de/translations.xml +++ b/features/userprofile/shared/src/main/res/values-de/translations.xml @@ -1,19 +1,19 @@ "Blockieren" - "Blockierte Benutzer können Dir keine Nachrichten senden und alle ihre alten Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden." - "Benutzer blockieren" + "Blockierte Nutzer können Ihnen keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden." + "Nutzer blockieren" "Blockierung aufheben" "Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt." "Blockierung aufheben" "Blockieren" - "Blockierte Benutzer können Dir keine Nachrichten senden und alle ihre alten Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden." - "Benutzer blockieren" + "Blockierte Nutzer können Ihnen keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden." + "Nutzer blockieren" "Profil" "Blockierung aufheben" "Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt." "Blockierung aufheben" - "Verwenden Sie die Web-App, um diesen Benutzer zu verifizieren." + "Verwenden Sie die Web-App, um diesen Nutzer zu verifizieren." "Überprüfen Sie %1$s" "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten" diff --git a/features/userprofile/shared/src/main/res/values-es/translations.xml b/features/userprofile/shared/src/main/res/values-es/translations.xml index 7d9a03375e4..c65165c04f3 100644 --- a/features/userprofile/shared/src/main/res/values-es/translations.xml +++ b/features/userprofile/shared/src/main/res/values-es/translations.xml @@ -9,8 +9,11 @@ "Bloquear" "Los usuarios bloqueados no podrán enviarte mensajes y todos sus mensajes se ocultarán. Puedes desbloquearlos cuando quieras." "Bloquear usuario" + "Perfil" "Desbloquear" "Podrás ver todos sus mensajes de nuevo." "Desbloquear usuario" + "Utiliza la aplicación web para verificar a este usuario." + "Verificar a %1$s" "Se ha producido un error al intentar iniciar un chat" diff --git a/features/userprofile/shared/src/main/res/values-eu/translations.xml b/features/userprofile/shared/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..42050a53ae7 --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-eu/translations.xml @@ -0,0 +1,19 @@ + + + "Blokeatu" + "Blokeatutako erabiltzaileek ezingo dizute mezurik bidali eta beren mezuak ezkutatuko dira. Edozein unetan desblokeatu ditzakezu." + "Blokeatu erabiltzailea" + "Desblokeatu" + "Beraien mezu guztiak berriro ikusteko aukera izango duzu." + "Desblokeatu erabiltzailea" + "Blokeatu" + "Blokeatutako erabiltzaileek ezingo dizute mezurik bidali eta beren mezuak ezkutatuko dira. Edozein unetan desblokeatu ditzakezu." + "Blokeatu erabiltzailea" + "Profila" + "Desblokeatu" + "Beraien mezu guztiak berriro ikusteko aukera izango duzu." + "Desblokeatu erabiltzailea" + "Erabili web-aplikazioa erabiltzaile hau egiaztatzeko." + "Egiaztatu %1$s" + "Errorea gertatu da txata hasten saiatzean" + diff --git a/features/userprofile/shared/src/main/res/values-fa/translations.xml b/features/userprofile/shared/src/main/res/values-fa/translations.xml index 8ce64548de1..65664a007a7 100644 --- a/features/userprofile/shared/src/main/res/values-fa/translations.xml +++ b/features/userprofile/shared/src/main/res/values-fa/translations.xml @@ -1,14 +1,17 @@ "بلوک" + "کاربران مسدود شده نمی‌توانند برای شما پیام ارسال کنند و تمام پیام‌های آنها پنهان خواهد شد. می‌توانید هر زمان که بخواهید آنها را از حالت مسدود خارج کنید." "انسداد کاربر" "رفع انسداد" "قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید." "رفع انسداد کاربر" "بلوک" + "کاربران مسدود شده نمی‌توانند برای شما پیام ارسال کنند و تمام پیام‌های آنها پنهان خواهد شد. می‌توانید هر زمان که بخواهید آنها را از حالت مسدود خارج کنید." "انسداد کاربر" "نمایه" "رفع انسداد" "قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید." "رفع انسداد کاربر" + "هنگام تلاش برای شروع چت خطایی روی داد" diff --git a/features/userprofile/shared/src/main/res/values-in/translations.xml b/features/userprofile/shared/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..e368bc9692f --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-in/translations.xml @@ -0,0 +1,19 @@ + + + "Blokir" + "Pengguna yang diblokir tidak akan dapat mengirim Anda pesan dan semua pesan mereka akan disembunyikan. Anda dapat membuka blokirnya kapan saja." + "Blokir pengguna" + "Buka blokir" + "Anda akan dapat melihat semua pesan dari mereka lagi." + "Buka blokir pengguna" + "Blokir" + "Pengguna yang diblokir tidak akan dapat mengirim Anda pesan dan semua pesan mereka akan disembunyikan. Anda dapat membuka blokirnya kapan saja." + "Blokir pengguna" + "Profil" + "Buka blokir" + "Anda akan dapat melihat semua pesan dari mereka lagi." + "Buka blokir pengguna" + "Gunakan aplikasi web untuk memverifikasi pengguna ini." + "Verifikasi %1$s" + "Terjadi kesalahan saat mencoba memulai obrolan" + diff --git a/features/userprofile/shared/src/main/res/values-ka/translations.xml b/features/userprofile/shared/src/main/res/values-ka/translations.xml index f0b863e2015..ca90f0ea348 100644 --- a/features/userprofile/shared/src/main/res/values-ka/translations.xml +++ b/features/userprofile/shared/src/main/res/values-ka/translations.xml @@ -9,6 +9,7 @@ "დაბლოკვა" "დაბლოკილი მომხმარებლები ვერ შეძლებენ თქვენთვის შეტყობინების გაგზავნას და ყველა მათი შეტყობინება თქვენთვის დამალული იქნება. თქვენ მათი განბლოკვა ნებისმეირ დროს შეგიძლიათ." "მომხმარებლის დაბლოკვა" + "პროფილი" "განბლოკვა" "თქვენ კვლავ შეძლებთ მათგან ყველა შეტყობინების ნახვას." "Მომხმარებლის განბლოკვა" diff --git a/features/userprofile/shared/src/main/res/values-lt/translations.xml b/features/userprofile/shared/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..327d8ffc2b2 --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-lt/translations.xml @@ -0,0 +1,16 @@ + + + "Blokuoti" + "Užblokuoti vartotojai negalės siųsti Jums žinučių, ir visos jų žinutės bus paslėptos. Galėsite juos atblokuoti bet kuriuo metu." + "Blokuoti vartotoją" + "Atblokuoti" + "Vėl galėsite matyti visas iš jų gautas žinutes." + "Atblokuoti vartotoją" + "Blokuoti" + "Užblokuoti vartotojai negalės siųsti Jums žinučių, ir visos jų žinutės bus paslėptos. Galėsite juos atblokuoti bet kuriuo metu." + "Blokuoti vartotoją" + "Atblokuoti" + "Vėl galėsite matyti visas iš jų gautas žinutes." + "Atblokuoti vartotoją" + "Bandant pradėti pokalbį įvyko klaida" + diff --git a/features/userprofile/shared/src/main/res/values-nl/translations.xml b/features/userprofile/shared/src/main/res/values-nl/translations.xml index 7acdb58bdb0..ce8bebad414 100644 --- a/features/userprofile/shared/src/main/res/values-nl/translations.xml +++ b/features/userprofile/shared/src/main/res/values-nl/translations.xml @@ -13,5 +13,6 @@ "Deblokkeren" "Je zult alle berichten van hen weer kunnen zien." "Gebruiker deblokkeren" + "Verifieer %1$s" "Er is een fout opgetreden bij het starten van een chat" diff --git a/features/userprofile/shared/src/main/res/values-pt-rBR/translations.xml b/features/userprofile/shared/src/main/res/values-pt-rBR/translations.xml index 8d64033f62d..e379aeb2ec0 100644 --- a/features/userprofile/shared/src/main/res/values-pt-rBR/translations.xml +++ b/features/userprofile/shared/src/main/res/values-pt-rBR/translations.xml @@ -9,8 +9,11 @@ "Bloquear" "Usuários bloqueados não poderão enviar mensagens para você e todas as mensagens deles serão ocultadas. Você pode desbloqueá-los a qualquer momento." "Bloquear usuário" + "Perfil" "Desbloquear" "Você poderá ver todas as mensagens deles novamente." "Desbloquear usuário" + "Use o aplicativo da Web para verificar este usuário." + "Verificar %1$s" "Ocorreu um erro ao tentar iniciar um chat" diff --git a/features/userprofile/shared/src/main/res/values-tr/translations.xml b/features/userprofile/shared/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..9f80d33836a --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-tr/translations.xml @@ -0,0 +1,19 @@ + + + "Engelle" + "Engellenen kullanıcılar size mesaj gönderemez ve tüm mesajları gizlenir. İstediğiniz zaman engellemelerini kaldırabilirsiniz." + "Kullanıcıyı engelle" + "Engellemeyi kaldır" + "Onlardan gelen tüm mesajları tekrar görebileceksiniz." + "Kullanıcının engelini kaldır" + "Engelle" + "Engellenen kullanıcılar size mesaj gönderemez ve tüm mesajları gizlenir. İstediğiniz zaman engellemelerini kaldırabilirsiniz." + "Kullanıcıyı engelle" + "Profil" + "Engellemeyi kaldır" + "Onlardan gelen tüm mesajları tekrar görebileceksiniz." + "Kullanıcının engelini kaldır" + "Bu kullanıcıyı doğrulamak için web uygulamasını kullan." + "Doğrula %1$s" + "Sohbet başlatmaya çalışırken bir hata oluştu" + diff --git a/features/userprofile/shared/src/main/res/values-ur/translations.xml b/features/userprofile/shared/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..244b5b85462 --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-ur/translations.xml @@ -0,0 +1,17 @@ + + + "مسدود کریں" + "مسدود کردہ صارفین آپ کو پیغامات نہیں بھیج سکیں گے اور انکے تمام پیغامات چھپ جائیں گے۔ آپ انھیں کسی بھی وقت غیر مسدود کر سکتے ہیں۔" + "صارف کو مسدود کریں" + "غیر مسدود کریں" + "آپ انکی جانب سے تمام پیغامات دوبارہ دیکھ سکیں گے۔" + "صارف کو غیر مسدود کریں" + "مسدود کریں" + "مسدود کردہ صارفین آپ کو پیغامات نہیں بھیج سکیں گے اور انکے تمام پیغامات چھپ جائیں گے۔ آپ انھیں کسی بھی وقت غیر مسدود کر سکتے ہیں۔" + "صارف کو مسدود کریں" + "نمایہ" + "غیر مسدود کریں" + "آپ انکی جانب سے تمام پیغامات دوبارہ دیکھ سکیں گے۔" + "صارف کو غیر مسدود کریں" + "گفتگو شروع کرنے کی کوشش کرتے وقت ایک خرابی واقع ہوگئی" + 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 6c81acd00a2..0e614e53d72 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 @@ -15,13 +15,16 @@ import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState +import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.shared.R import io.element.android.features.userprofile.shared.UserProfileView import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings @@ -193,6 +196,17 @@ class UserProfileViewTest { rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } + + @Test + fun `on verify user clicked - the right callback is called`() = runTest { + ensureCalledOnceWithParam(A_USER_ID) { callback -> + rule.setUserProfileView( + state = aUserProfileState(userId = A_USER_ID, verificationState = UserProfileVerificationState.UNVERIFIED), + onVerifyClick = callback, + ) + rule.clickOn(CommonStrings.common_verify_user) + } + } } private fun AndroidComposeTestRule.setUserProfileView( @@ -202,6 +216,7 @@ private fun AndroidComposeTestRule.setUserP onShareUser: () -> Unit = EnsureNeverCalled(), onDmStarted: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onStartCall: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onVerifyClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), goBack: () -> Unit = EnsureNeverCalled(), openAvatarPreview: (String, String) -> Unit = EnsureNeverCalledWithTwoParams(), ) { @@ -213,6 +228,7 @@ private fun AndroidComposeTestRule.setUserP onStartCall = onStartCall, goBack = goBack, openAvatarPreview = openAvatarPreview, + onVerifyClick = onVerifyClick, ) } } diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt index 2d2a789b3a8..9d90f33e8dc 100644 --- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt +++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt @@ -12,11 +12,11 @@ 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.architecture.NodeInputs -import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.verification.VerificationRequest interface IncomingVerificationEntryPoint : FeatureEntryPoint { data class Params( - val sessionVerificationRequestDetails: SessionVerificationRequestDetails, + val verificationRequest: VerificationRequest.Incoming, ) : NodeInputs fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/OutgoingVerificationEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/OutgoingVerificationEntryPoint.kt new file mode 100644 index 00000000000..60536504d2d --- /dev/null +++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/OutgoingVerificationEntryPoint.kt @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package io.element.android.features.verifysession.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.architecture.NodeInputs +import io.element.android.libraries.matrix.api.verification.VerificationRequest + +interface OutgoingVerificationEntryPoint : FeatureEntryPoint { + data class Params( + val showDeviceVerifiedScreen: Boolean, + val verificationRequest: VerificationRequest.Outgoing, + ) : NodeInputs + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun params(params: Params): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onLearnMoreAboutEncryption() + fun onBack() + fun onDone() + } +} diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt deleted file mode 100644 index 3bae35a3cf4..00000000000 --- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.verifysession.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.architecture.NodeInputs - -interface VerifySessionEntryPoint : FeatureEntryPoint { - data class Params(val showDeviceVerifiedScreen: Boolean) : NodeInputs - - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder - - interface NodeBuilder { - fun callback(callback: Callback): NodeBuilder - fun params(params: Params): NodeBuilder - fun build(): Node - } - - interface Callback : Plugin { - fun onEnterRecoveryKey() - fun onResetKey() - fun onDone() - } -} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt index f884f64510a..802eadb13a8 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt @@ -28,7 +28,7 @@ class IncomingVerificationNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins), IncomingVerificationNavigator { private val presenter = presenterFactory.create( - sessionVerificationRequestDetails = inputs().sessionVerificationRequestDetails, + verificationRequest = inputs().verificationRequest, navigator = this, ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt index b3cec7def2a..66a2b2b2cdb 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt @@ -10,10 +10,12 @@ package io.element.android.features.verifysession.impl.incoming import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import com.freeletics.flowredux.compose.rememberStateAndDispatch import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -22,20 +24,24 @@ import io.element.android.features.verifysession.impl.incoming.IncomingVerificat import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.api.verification.VerificationRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import io.element.android.features.verifysession.impl.incoming.IncomingVerificationStateMachine.Event as StateMachineEvent import io.element.android.features.verifysession.impl.incoming.IncomingVerificationStateMachine.State as StateMachineState class IncomingVerificationPresenter @AssistedInject constructor( - @Assisted private val sessionVerificationRequestDetails: SessionVerificationRequestDetails, + @Assisted private val verificationRequest: VerificationRequest.Incoming, @Assisted private val navigator: IncomingVerificationNavigator, + @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val sessionVerificationService: SessionVerificationService, private val stateMachine: IncomingVerificationStateMachine, private val dateFormatter: DateFormatter, @@ -43,49 +49,63 @@ class IncomingVerificationPresenter @AssistedInject constructor( @AssistedFactory interface Factory { fun create( - sessionVerificationRequestDetails: SessionVerificationRequestDetails, + verificationRequest: VerificationRequest.Incoming, navigator: IncomingVerificationNavigator, ): IncomingVerificationPresenter } @Composable override fun present(): IncomingVerificationState { - LaunchedEffect(Unit) { - // Force reset, just in case the service was left in a broken state - sessionVerificationService.reset( - cancelAnyPendingVerificationAttempt = false - ) - // Acknowledge the request right now - sessionVerificationService.acknowledgeVerificationRequest(sessionVerificationRequestDetails) - } + val coroutineScope = rememberCoroutineScope() + val stateAndDispatch = stateMachine.rememberStateAndDispatch() + + DisposableEffect(Unit) { + coroutineScope.launch { + // Force reset, just in case the service was left in a broken state + sessionVerificationService.reset( + cancelAnyPendingVerificationAttempt = false + ) + + // Start this after observing state machine + observeVerificationService() + + // Acknowledge the request right now + sessionVerificationService.acknowledgeVerificationRequest(verificationRequest) + } + + onDispose { + sessionCoroutineScope.launch { + val currentState = stateAndDispatch.state.value + sessionVerificationService.reset( + cancelAnyPendingVerificationAttempt = currentState?.isPending() == true, + ) + } + } + } + val formattedSignInTime = remember { dateFormatter.format( - timestamp = sessionVerificationRequestDetails.firstSeenTimestamp, + timestamp = verificationRequest.details.firstSeenTimestamp, mode = DateFormatterMode.TimeOrDate, ) } val step by remember { derivedStateOf { stateAndDispatch.state.value.toVerificationStep( - sessionVerificationRequestDetails = sessionVerificationRequestDetails, + sessionVerificationRequestDetails = verificationRequest.details, formattedSignInTime = formattedSignInTime, ) } } LaunchedEffect(stateAndDispatch.state.value) { - if ((stateAndDispatch.state.value as? IncomingVerificationStateMachine.State.Initial)?.isCancelled == true) { + if ((stateAndDispatch.state.value as? StateMachineState.Initial)?.isCancelled == true) { // The verification was canceled before it was started, maybe because another session accepted it navigator.onFinish() } } - // Start this after observing state machine - LaunchedEffect(Unit) { - observeVerificationService() - } - fun handleEvents(event: IncomingVerificationViewEvents) { Timber.d("Verification user action: ${event::class.simpleName}") when (event) { @@ -119,6 +139,7 @@ class IncomingVerificationPresenter @AssistedInject constructor( return IncomingVerificationState( step = step, + request = verificationRequest, eventSink = ::handleEvents, ) } @@ -129,36 +150,36 @@ class IncomingVerificationPresenter @AssistedInject constructor( ): Step = when (val machineState = this) { is StateMachineState.Initial, - IncomingVerificationStateMachine.State.AcceptingIncomingVerification, - IncomingVerificationStateMachine.State.RejectingIncomingVerification, + StateMachineState.AcceptingIncomingVerification, + StateMachineState.RejectingIncomingVerification, null -> { Step.Initial( - deviceDisplayName = sessionVerificationRequestDetails.displayName ?: sessionVerificationRequestDetails.deviceId.value, + deviceDisplayName = sessionVerificationRequestDetails.senderProfile.displayName ?: sessionVerificationRequestDetails.deviceId.value, deviceId = sessionVerificationRequestDetails.deviceId, formattedSignInTime = formattedSignInTime, - isWaiting = machineState == IncomingVerificationStateMachine.State.AcceptingIncomingVerification || - machineState == IncomingVerificationStateMachine.State.RejectingIncomingVerification, + isWaiting = machineState == StateMachineState.AcceptingIncomingVerification || + machineState == StateMachineState.RejectingIncomingVerification, ) } - is IncomingVerificationStateMachine.State.ChallengeReceived -> + is StateMachineState.ChallengeReceived -> Step.Verifying( data = machineState.data, isWaiting = false, ) - IncomingVerificationStateMachine.State.Completed -> Step.Completed - IncomingVerificationStateMachine.State.Canceling, - IncomingVerificationStateMachine.State.Failure -> Step.Failure - is IncomingVerificationStateMachine.State.AcceptingChallenge -> + StateMachineState.Completed -> Step.Completed + StateMachineState.Canceling, + StateMachineState.Failure -> Step.Failure + is StateMachineState.AcceptingChallenge -> Step.Verifying( data = machineState.data, isWaiting = true, ) - is IncomingVerificationStateMachine.State.RejectingChallenge -> + is StateMachineState.RejectingChallenge -> Step.Verifying( data = machineState.data, isWaiting = true, ) - IncomingVerificationStateMachine.State.Canceled -> Step.Canceled + StateMachineState.Canceled -> Step.Canceled } private fun CoroutineScope.observeVerificationService() { @@ -170,10 +191,10 @@ class IncomingVerificationPresenter @AssistedInject constructor( VerificationFlowState.DidAcceptVerificationRequest, VerificationFlowState.DidStartSasVerification -> Unit is VerificationFlowState.DidReceiveVerificationData -> { - stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data)) + stateMachine.dispatch(StateMachineEvent.DidReceiveChallenge(verificationAttemptState.data)) } VerificationFlowState.DidFinish -> { - stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidAcceptChallenge) + stateMachine.dispatch(StateMachineEvent.DidAcceptChallenge) } VerificationFlowState.DidCancel -> { // Can happen when: @@ -181,10 +202,10 @@ class IncomingVerificationPresenter @AssistedInject constructor( // - another session has accepted the incoming verification request // - the user reject the challenge from this application (I think this is an error). In this case, the state // machine will ignore this event and change state to Failure. - stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidCancel) + stateMachine.dispatch(StateMachineEvent.DidCancel) } VerificationFlowState.DidFail -> { - stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidFail) + stateMachine.dispatch(StateMachineEvent.DidFail) } } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt index 241ea8e5ad0..95f81af479d 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt @@ -11,10 +11,12 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.VerificationRequest @Immutable data class IncomingVerificationState( val step: Step, + val request: VerificationRequest.Incoming, val eventSink: (IncomingVerificationViewEvents) -> Unit, ) { @Stable diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt index d873c6fd50a..9ff9f50cd80 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt @@ -26,7 +26,7 @@ class IncomingVerificationStateMachine @Inject constructor( init { spec { inState { - on { _: Event.AcceptIncomingRequest, state -> + on { _, state -> state.override { State.AcceptingIncomingVerification.andLogStateChange() } } } @@ -39,23 +39,23 @@ class IncomingVerificationStateMachine @Inject constructor( } } inState { - on { _: Event.AcceptChallenge, state -> + on { _, state -> state.override { State.AcceptingChallenge(state.snapshot.data).andLogStateChange() } } - on { _: Event.DeclineChallenge, state -> + on { _, state -> state.override { State.RejectingChallenge(state.snapshot.data).andLogStateChange() } } } inState { - onEnterEffect { _ -> + onEnterEffect { sessionVerificationService.approveVerification() } - on { _: Event.DidAcceptChallenge, state -> + on { _, state -> state.override { State.Completed.andLogStateChange() } } } inState { - onEnterEffect { _ -> + onEnterEffect { sessionVerificationService.declineVerification() } } @@ -66,7 +66,7 @@ class IncomingVerificationStateMachine @Inject constructor( } inState { logReceivedEvents() - on { _: Event.Cancel, state: MachineState -> + on { _, state: MachineState -> when (state.snapshot) { State.Completed, State.Canceled -> state.noChange() else -> { @@ -75,7 +75,7 @@ class IncomingVerificationStateMachine @Inject constructor( } } } - on { _: Event.DidCancel, state: MachineState -> + on { _, state: MachineState -> when (state.snapshot) { is State.RejectingChallenge -> { state.override { State.Failure.andLogStateChange() } @@ -91,7 +91,7 @@ class IncomingVerificationStateMachine @Inject constructor( State.Failure -> state.noChange() } } - on { _: Event.DidFail, state: MachineState -> + on { _, state: MachineState -> state.override { State.Failure.andLogStateChange() } } } @@ -128,6 +128,11 @@ class IncomingVerificationStateMachine @Inject constructor( /** Verification failure. */ data object Failure : State + + fun isPending(): Boolean = when (this) { + AcceptingIncomingVerification, RejectingIncomingVerification, Failure, is ChallengeReceived, is AcceptingChallenge, is RejectingChallenge -> true + is Initial, Canceling, Canceled, Completed -> false + } } sealed interface Event { 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 d57f56385b2..cdad2669d69 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,16 +12,32 @@ import io.element.android.features.verifysession.impl.incoming.IncomingVerificat import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData 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 +import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.verification.VerificationRequest open class IncomingVerificationStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anIncomingVerificationState(), - anIncomingVerificationState(step = aStepInitial(isWaiting = true)), + anIncomingVerificationState(step = aStepInitial(isWaiting = false), verificationRequest = anIncomingSessionVerificationRequest()), + anIncomingVerificationState(step = aStepInitial(isWaiting = false), verificationRequest = anIncomingUserVerificationRequest()), + anIncomingVerificationState(step = aStepInitial(isWaiting = true), verificationRequest = anIncomingSessionVerificationRequest()), + anIncomingVerificationState(step = aStepInitial(isWaiting = true), verificationRequest = anIncomingUserVerificationRequest()), anIncomingVerificationState(step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = false)), + anIncomingVerificationState( + step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = false), + verificationRequest = anIncomingUserVerificationRequest() + ), anIncomingVerificationState(step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = true)), + anIncomingVerificationState( + step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = true), + verificationRequest = anIncomingUserVerificationRequest() + ), anIncomingVerificationState(step = Step.Verifying(data = aDecimalsSessionVerificationData(), isWaiting = false)), anIncomingVerificationState(step = Step.Completed), + anIncomingVerificationState(step = Step.Completed, verificationRequest = anIncomingUserVerificationRequest()), anIncomingVerificationState(step = Step.Failure), anIncomingVerificationState(step = Step.Canceled), // Add other state here @@ -37,10 +53,38 @@ internal fun aStepInitial( isWaiting = isWaiting, ) +internal fun anIncomingSessionVerificationRequest() = VerificationRequest.Incoming.OtherSession( + details = SessionVerificationRequestDetails( + senderProfile = SessionVerificationRequestDetails.SenderProfile( + userId = UserId("@alice:example.com"), + displayName = "Alice", + avatarUrl = null, + ), + flowId = FlowId("1234"), + deviceId = DeviceId("ILAKNDNASDLK"), + firstSeenTimestamp = 0, + ) +) + +internal fun anIncomingUserVerificationRequest() = VerificationRequest.Incoming.User( + details = SessionVerificationRequestDetails( + senderProfile = SessionVerificationRequestDetails.SenderProfile( + userId = UserId("@alice:example.com"), + displayName = "Alice", + avatarUrl = null, + ), + flowId = FlowId("1234"), + deviceId = DeviceId("ILAKNDNASDLK"), + firstSeenTimestamp = 0, + ) +) + internal fun anIncomingVerificationState( step: Step = aStepInitial(), + verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest(), eventSink: (IncomingVerificationViewEvents) -> Unit = {}, ) = IncomingVerificationState( step = step, + request = verificationRequest, eventSink = eventSink, ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt index b31c937f369..12bfe99fcdf 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt @@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults.topAppBarColors 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.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter @@ -27,9 +29,11 @@ import io.element.android.features.verifysession.impl.incoming.IncomingVerificat import io.element.android.features.verifysession.impl.incoming.ui.SessionDetailsView import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying +import io.element.android.features.verifysession.impl.ui.VerificationUserProfileContent import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.PageTitle +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.Button @@ -38,6 +42,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.ui.strings.CommonStrings /** @@ -59,50 +64,80 @@ fun IncomingVerificationView( topBar = { TopAppBar( title = {}, + navigationIcon = { + when { + step is Step.Initial && !step.isWaiting -> Unit + step is Step.Completed -> Unit + else -> BackButton(onClick = { state.eventSink(IncomingVerificationViewEvents.GoBack) }) + } + }, + colors = topAppBarColors(containerColor = Color.Transparent), ) }, header = { - IncomingVerificationHeader(step = step) + IncomingVerificationHeader(step = step, request = state.request) }, footer = { IncomingVerificationBottomMenu( state = state, ) - } + }, + isScrollable = true, ) { IncomingVerificationContent( step = step, + request = state.request, ) } } @Composable -private fun IncomingVerificationHeader(step: Step) { +private fun IncomingVerificationHeader(step: Step, request: VerificationRequest.Incoming) { val iconStyle = when (step) { Step.Canceled -> BigIcon.Style.AlertSolid - is Step.Initial -> BigIcon.Style.Default(CompoundIcons.LockSolid()) - is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction()) + is Step.Initial -> if (step.isWaiting) { + BigIcon.Style.Loading + } else { + when (request) { + is VerificationRequest.Incoming.OtherSession -> BigIcon.Style.Default(CompoundIcons.LockSolid()) + is VerificationRequest.Incoming.User -> BigIcon.Style.Default(CompoundIcons.UserProfileSolid()) + } + } + is Step.Verifying -> if (step.isWaiting) { + BigIcon.Style.Loading + } else { + BigIcon.Style.Default(CompoundIcons.ReactionSolid()) + } Step.Completed -> BigIcon.Style.SuccessSolid Step.Failure -> BigIcon.Style.AlertSolid } val titleTextId = when (step) { - Step.Canceled -> R.string.screen_session_verification_request_failure_title + Step.Canceled -> CommonStrings.common_verification_failed is Step.Initial -> R.string.screen_session_verification_request_title is Step.Verifying -> when (step.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title } - Step.Completed -> R.string.screen_session_verification_request_success_title + Step.Completed -> CommonStrings.common_verification_complete Step.Failure -> R.string.screen_session_verification_request_failure_title } val subtitleTextId = when (step) { Step.Canceled -> R.string.screen_session_verification_request_failure_subtitle - is Step.Initial -> R.string.screen_session_verification_request_subtitle + is Step.Initial -> when (request) { + is VerificationRequest.Incoming.OtherSession -> R.string.screen_session_verification_request_subtitle + is VerificationRequest.Incoming.User -> R.string.screen_session_verification_user_responder_subtitle + } is Step.Verifying -> when (step.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle - is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle + is SessionVerificationData.Emojis -> when (request) { + is VerificationRequest.Incoming.OtherSession -> R.string.screen_session_verification_compare_emojis_subtitle + is VerificationRequest.Incoming.User -> R.string.screen_session_verification_compare_emojis_user_subtitle + } + } + Step.Completed -> when (request) { + is VerificationRequest.Incoming.OtherSession -> R.string.screen_session_verification_complete_subtitle + is VerificationRequest.Incoming.User -> R.string.screen_session_verification_complete_user_subtitle } - Step.Completed -> R.string.screen_session_verification_request_success_subtitle Step.Failure -> R.string.screen_session_verification_request_failure_subtitle } PageTitle( @@ -115,9 +150,10 @@ private fun IncomingVerificationHeader(step: Step) { @Composable private fun IncomingVerificationContent( step: Step, + request: VerificationRequest.Incoming, ) { when (step) { - is Step.Initial -> ContentInitial(step) + is Step.Initial -> ContentInitial(step, request) is Step.Verifying -> VerificationContentVerifying(step.data) else -> Unit } @@ -126,24 +162,40 @@ private fun IncomingVerificationContent( @Composable private fun ContentInitial( initialIncoming: Step.Initial, + request: VerificationRequest.Incoming, ) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - SessionDetailsView( - deviceName = initialIncoming.deviceDisplayName, - deviceId = initialIncoming.deviceId, - signInFormattedTimestamp = initialIncoming.formattedSignInTime, - ) - Text( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(bottom = 16.dp), - text = stringResource(R.string.screen_session_verification_request_footer), - style = ElementTheme.typography.fontBodyMdMedium, - textAlign = TextAlign.Center, - ) + when (request) { + is VerificationRequest.Incoming.OtherSession -> { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + SessionDetailsView( + deviceName = initialIncoming.deviceDisplayName, + deviceId = initialIncoming.deviceId, + signInFormattedTimestamp = initialIncoming.formattedSignInTime, + ) + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp), + text = stringResource(R.string.screen_session_verification_request_footer), + style = ElementTheme.typography.fontBodyMdMedium, + textAlign = TextAlign.Center, + ) + } + } + is VerificationRequest.Incoming.User -> { + Column( + modifier = Modifier.fillMaxWidth().padding(top = 24.dp), + ) { + VerificationUserProfileContent( + userId = request.details.senderProfile.userId, + displayName = request.details.senderProfile.displayName, + avatarUrl = request.details.senderProfile.avatarUrl, + ) + } + } } } @@ -157,16 +209,7 @@ private fun IncomingVerificationBottomMenu( when (step) { is Step.Initial -> { if (step.isWaiting) { - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_waiting_on_other_device), - onClick = {}, - enabled = false, - showProgress = true, - ) - InvisibleButton() - } + // Show nothing } else { VerificationBottomMenu { Button( @@ -184,14 +227,9 @@ private fun IncomingVerificationBottomMenu( } is Step.Verifying -> { if (step.isWaiting) { + // Add invisible buttons to keep the same screen layout VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing), - onClick = {}, - enabled = false, - showProgress = true, - ) + InvisibleButton() InvisibleButton() } } else { diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultOutgoingVerificationEntryPoint.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultOutgoingVerificationEntryPoint.kt new file mode 100644 index 00000000000..8ab29ce9f27 --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultOutgoingVerificationEntryPoint.kt @@ -0,0 +1,40 @@ +/* + * 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. + */ + +package io.element.android.features.verifysession.impl.outgoing + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultOutgoingVerificationEntryPoint @Inject constructor() : OutgoingVerificationEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): OutgoingVerificationEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : OutgoingVerificationEntryPoint.NodeBuilder { + override fun callback(callback: OutgoingVerificationEntryPoint.Callback): OutgoingVerificationEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun params(params: OutgoingVerificationEntryPoint.Params): OutgoingVerificationEntryPoint.NodeBuilder { + plugins += params + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultVerifySessionEntryPoint.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultVerifySessionEntryPoint.kt deleted file mode 100644 index 2a137a06be5..00000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultVerifySessionEntryPoint.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.verifysession.impl.outgoing - -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.verifysession.api.VerifySessionEntryPoint -import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultVerifySessionEntryPoint @Inject constructor() : VerifySessionEntryPoint { - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): VerifySessionEntryPoint.NodeBuilder { - val plugins = ArrayList() - - return object : VerifySessionEntryPoint.NodeBuilder { - override fun callback(callback: VerifySessionEntryPoint.Callback): VerifySessionEntryPoint.NodeBuilder { - plugins += callback - return this - } - - override fun params(params: VerifySessionEntryPoint.Params): VerifySessionEntryPoint.NodeBuilder { - plugins += params - return this - } - - override fun build(): Node { - return parentNode.createNode(buildContext, plugins) - } - } - } -} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationNode.kt new file mode 100644 index 00000000000..4691a1d23fa --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationNode.kt @@ -0,0 +1,49 @@ +/* + * 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. + */ + +package io.element.android.features.verifysession.impl.outgoing + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class OutgoingVerificationNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: OutgoingVerificationPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + private val callback = plugins().first() + + private val inputs = inputs() + + private val presenter = presenterFactory.create( + showDeviceVerifiedScreen = inputs.showDeviceVerifiedScreen, + verificationRequest = inputs.verificationRequest, + ) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + OutgoingVerificationView( + state = state, + modifier = modifier, + onLearnMoreClick = callback::onLearnMoreAboutEncryption, + onFinish = callback::onDone, + onBack = callback::onBack, + ) + } +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenter.kt new file mode 100644 index 00000000000..81856810908 --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenter.kt @@ -0,0 +1,179 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.verifysession.impl.outgoing + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import com.freeletics.flowredux.compose.rememberStateAndDispatch +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus +import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationStateMachine.Event as StateMachineEvent +import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationStateMachine.State as StateMachineState + +class OutgoingVerificationPresenter @AssistedInject constructor( + @Assisted private val showDeviceVerifiedScreen: Boolean, + @Assisted private val verificationRequest: VerificationRequest.Outgoing, + private val sessionVerificationService: SessionVerificationService, + private val encryptionService: EncryptionService, +) : Presenter { + @AssistedFactory + interface Factory { + fun create( + verificationRequest: VerificationRequest.Outgoing, + showDeviceVerifiedScreen: Boolean, + ): OutgoingVerificationPresenter + } + + private val stateMachine = OutgoingVerificationStateMachine( + sessionVerificationService = sessionVerificationService, + encryptionService = encryptionService, + ) + + @Composable + override fun present(): OutgoingVerificationState { + val stateAndDispatch = stateMachine.rememberStateAndDispatch() + + val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState() + val step by remember { + derivedStateOf { + when (verificationRequest) { + is VerificationRequest.Outgoing.CurrentSession -> { + when (sessionVerifiedStatus) { + SessionVerifiedStatus.Unknown -> OutgoingVerificationState.Step.Loading + SessionVerifiedStatus.NotVerified -> { + stateAndDispatch.state.value.toVerificationStep() + } + SessionVerifiedStatus.Verified -> { + if (stateAndDispatch.state.value != StateMachineState.Initial || showDeviceVerifiedScreen) { + // The user has verified the session, we need to show the success screen + OutgoingVerificationState.Step.Completed + } else { + // Automatic verification, which can happen on freshly created account, in this case, skip the screen + OutgoingVerificationState.Step.Exit + } + } + } + } + is VerificationRequest.Outgoing.User -> stateAndDispatch.state.value.toVerificationStep() + } + } + } + + // Start this after observing state machine + LaunchedEffect(Unit) { + // Force reset, just in case the service was left in a broken state + sessionVerificationService.reset(cancelAnyPendingVerificationAttempt = true) + + observeVerificationService() + } + + fun handleEvents(event: OutgoingVerificationViewEvents) { + Timber.d("Verification user action: ${event::class.simpleName}") + when (event) { + // Just relay the event to the state machine + OutgoingVerificationViewEvents.RequestVerification -> StateMachineEvent.RequestVerification(verificationRequest) + OutgoingVerificationViewEvents.StartSasVerification -> StateMachineEvent.StartSasVerification + OutgoingVerificationViewEvents.ConfirmVerification -> StateMachineEvent.AcceptChallenge + OutgoingVerificationViewEvents.DeclineVerification -> StateMachineEvent.DeclineChallenge + OutgoingVerificationViewEvents.Cancel -> StateMachineEvent.Cancel + OutgoingVerificationViewEvents.Reset -> StateMachineEvent.Reset + }.let { stateMachineEvent -> + stateAndDispatch.dispatchAction(stateMachineEvent) + } + } + return OutgoingVerificationState( + step = step, + request = verificationRequest, + eventSink = ::handleEvents, + ) + } + + private fun StateMachineState?.toVerificationStep(): OutgoingVerificationState.Step = + when (val machineState = this) { + StateMachineState.Initial, null -> { + OutgoingVerificationState.Step.Initial + } + is StateMachineState.RequestingVerification, + is StateMachineState.StartingSasVerification, + StateMachineState.SasVerificationStarted -> { + OutgoingVerificationState.Step.AwaitingOtherDeviceResponse + } + + StateMachineState.VerificationRequestAccepted -> { + OutgoingVerificationState.Step.Ready + } + + is StateMachineState.Canceled -> { + OutgoingVerificationState.Step.Canceled + } + + is StateMachineState.Verifying -> { + val async = when (machineState) { + is StateMachineState.Verifying.Replying -> AsyncData.Loading() + else -> AsyncData.Uninitialized + } + OutgoingVerificationState.Step.Verifying(machineState.data, async) + } + + StateMachineState.Completed -> { + OutgoingVerificationState.Step.Completed + } + + StateMachineState.Exit -> { + OutgoingVerificationState.Step.Exit + } + } + + private fun CoroutineScope.observeVerificationService() { + sessionVerificationService.verificationFlowState + .onEach { Timber.d("Verification flow state: ${it::class.simpleName}") } + .onEach { verificationAttemptState -> + when (verificationAttemptState) { + VerificationFlowState.Initial -> stateMachine.dispatch(StateMachineEvent.Reset) + VerificationFlowState.DidAcceptVerificationRequest -> { + stateMachine.dispatch(StateMachineEvent.DidAcceptVerificationRequest) + } + VerificationFlowState.DidStartSasVerification -> { + stateMachine.dispatch(StateMachineEvent.DidStartSasVerification) + } + is VerificationFlowState.DidReceiveVerificationData -> { + stateMachine.dispatch(StateMachineEvent.DidReceiveChallenge(verificationAttemptState.data)) + } + VerificationFlowState.DidFinish -> { + stateMachine.dispatch(StateMachineEvent.DidAcceptChallenge) + } + VerificationFlowState.DidCancel -> { + stateMachine.dispatch(StateMachineEvent.DidCancel) + } + VerificationFlowState.DidFail -> { + stateMachine.dispatch(StateMachineEvent.DidFail) + } + } + } + .launchIn(this) + } +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationState.kt new file mode 100644 index 00000000000..02653124abb --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationState.kt @@ -0,0 +1,33 @@ +/* + * 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. + */ + +package io.element.android.features.verifysession.impl.outgoing + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.VerificationRequest + +@Immutable +data class OutgoingVerificationState( + val step: Step, + val request: VerificationRequest.Outgoing, + val eventSink: (OutgoingVerificationViewEvents) -> Unit, +) { + @Stable + sealed interface Step { + data object Loading : Step + data object Initial : Step + data object Canceled : Step + data object AwaitingOtherDeviceResponse : Step + data object Ready : Step + data class Verifying(val data: SessionVerificationData, val state: AsyncData) : Step + data object Completed : Step + data object Exit : Step + } +} 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 new file mode 100644 index 00000000000..b078b6ed994 --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateMachine.kt @@ -0,0 +1,202 @@ +/* + * 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. + */ + +@file:Suppress("WildcardImport") +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.verifysession.impl.outgoing + +import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import io.element.android.features.verifysession.impl.util.andLogStateChange +import io.element.android.features.verifysession.impl.util.logReceivedEvents +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.core.data.tryOrNull +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.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.timeout +import kotlin.time.Duration.Companion.seconds +import com.freeletics.flowredux.dsl.State as MachineState + +@OptIn(FlowPreview::class) +class OutgoingVerificationStateMachine( + private val sessionVerificationService: SessionVerificationService, + private val encryptionService: EncryptionService, +) : FlowReduxStateMachine( + initialState = State.Initial, +) { + init { + spec { + inState { + on { event, state -> + state.override { State.RequestingVerification(event.verificationRequest).andLogStateChange() } + } + } + inState { + onEnterEffect { event -> + when (event.verificationRequest) { + is VerificationRequest.Outgoing.CurrentSession -> sessionVerificationService.requestCurrentSessionVerification() + is VerificationRequest.Outgoing.User -> sessionVerificationService.requestUserVerification(event.verificationRequest.userId) + } + } + on { _, state -> + state.override { State.VerificationRequestAccepted.andLogStateChange() } + } + } + inState { + onEnterEffect { + sessionVerificationService.startVerification() + } + } + inState { + on { _, state -> + state.override { State.StartingSasVerification.andLogStateChange() } + } + } + inState { + on { _, state -> + sessionVerificationService.reset(cancelAnyPendingVerificationAttempt = false) + state.override { State.Initial.andLogStateChange() } + } + } + inState { + on { event, state -> + state.override { State.Verifying.ChallengeReceived(event.data).andLogStateChange() } + } + } + inState { + on { _, state -> + state.override { State.Verifying.Replying(state.snapshot.data, accept = true).andLogStateChange() } + } + on { _, state -> + state.override { State.Verifying.Replying(state.snapshot.data, accept = false).andLogStateChange() } + } + } + inState { + onEnterEffect { state -> + if (state.accept) { + sessionVerificationService.approveVerification() + } else { + sessionVerificationService.declineVerification() + } + } + on { _, state -> + // If a key backup exists, wait until it's restored or a timeout happens + val hasBackup = encryptionService.doesBackupExistOnServer().getOrNull().orFalse() + if (hasBackup) { + tryOrNull { + encryptionService.recoveryStateStateFlow.filter { it == RecoveryState.ENABLED } + .timeout(10.seconds) + .first() + } + } + state.override { State.Completed.andLogStateChange() } + } + } + inState { + logReceivedEvents() + on { _, state: MachineState -> + state.override { State.SasVerificationStarted.andLogStateChange() } + } + on { event, state: MachineState -> + when (state.snapshot) { + State.Initial, State.Completed, is State.Canceled -> state.override { State.Exit } + // For some reason `cancelVerification` is not calling its delegate `didCancel` method so we don't pass from + // `Canceling` state to `Canceled` automatically anymore + else -> { + sessionVerificationService.cancelVerification() + state.override { State.Canceled.andLogStateChange() } + } + } + } + on { event, state: MachineState -> + state.override { State.Canceled.andLogStateChange() } + } + on { event, state: MachineState -> + state.override { State.Canceled.andLogStateChange() } + } + } + } + } + + sealed interface State { + /** Let the user know that they need to get ready on their other session. */ + data object Initial : State + + /** Waiting for verification acceptance. */ + data class RequestingVerification(val verificationRequest: VerificationRequest.Outgoing) : State + + /** Verification request accepted. Waiting for start. */ + data object VerificationRequestAccepted : State + + /** Waiting for SaS verification start. */ + data object StartingSasVerification : State + + /** A SaS verification flow has been started. */ + data object SasVerificationStarted : State + + sealed class Verifying(open val data: SessionVerificationData) : State { + /** Verification accepted and emojis received. */ + data class ChallengeReceived(override val data: SessionVerificationData) : Verifying(data) + + /** Replying to a verification challenge. */ + data class Replying(override val data: SessionVerificationData, val accept: Boolean) : Verifying(data) + } + + /** The verification has been canceled, remotely or locally. */ + data object Canceled : State + + /** Verification successful. */ + data object Completed : State + + data object Exit : State + } + + sealed interface Event { + /** Request verification. */ + data class RequestVerification(val verificationRequest: VerificationRequest.Outgoing) : Event + + /** The current verification request has been accepted. */ + data object DidAcceptVerificationRequest : Event + + /** Start a SaS verification flow. */ + data object StartSasVerification : Event + + /** Started a SaS verification flow. */ + data object DidStartSasVerification : Event + + /** Has received data. */ + data class DidReceiveChallenge(val data: SessionVerificationData) : Event + + /** Emojis match. */ + data object AcceptChallenge : Event + + /** Emojis do not match. */ + data object DeclineChallenge : Event + + /** Remote accepted challenge. */ + data object DidAcceptChallenge : Event + + /** Request cancellation. */ + data object Cancel : Event + + /** Verification cancelled. */ + data object DidCancel : Event + + /** Request failed. */ + data object DidFail : Event + + /** Reset the verification flow to the initial state. */ + data object Reset : Event + } +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateProvider.kt new file mode 100644 index 00000000000..ab7119d2138 --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateProvider.kt @@ -0,0 +1,86 @@ +/* + * 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. + */ + +package io.element.android.features.verifysession.impl.outgoing + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationState.Step +import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData +import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.verification.VerificationRequest + +open class OutgoingVerificationStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anOutgoingVerificationState( + step = Step.Initial, + request = anOutgoingSessionVerificationRequest(), + ), + anOutgoingVerificationState( + step = Step.Initial, + request = anOutgoingUserVerificationRequest(), + ), + anOutgoingVerificationState( + step = Step.AwaitingOtherDeviceResponse, + request = anOutgoingSessionVerificationRequest(), + ), + anOutgoingVerificationState( + step = Step.AwaitingOtherDeviceResponse, + request = anOutgoingUserVerificationRequest(), + ), + anOutgoingVerificationState( + step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized), + request = anOutgoingSessionVerificationRequest(), + ), + anOutgoingVerificationState( + step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized), + request = anOutgoingUserVerificationRequest(), + ), + anOutgoingVerificationState( + step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading()) + ), + anOutgoingVerificationState( + step = Step.Canceled + ), + anOutgoingVerificationState( + step = Step.Ready + ), + anOutgoingVerificationState( + step = Step.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized) + ), + anOutgoingVerificationState( + step = Step.Completed, + request = anOutgoingSessionVerificationRequest(), + ), + anOutgoingVerificationState( + step = Step.Completed, + request = anOutgoingUserVerificationRequest(), + ), + anOutgoingVerificationState( + step = Step.Loading + ), + anOutgoingVerificationState( + step = Step.Exit + ), + // Add other state here + ) +} + +internal fun anOutgoingUserVerificationRequest() = VerificationRequest.Outgoing.User(userId = UserId("@alice:example.com")) +internal fun anOutgoingSessionVerificationRequest() = VerificationRequest.Outgoing.CurrentSession + +internal fun anOutgoingVerificationState( + step: Step = Step.Initial, + request: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(), + eventSink: (OutgoingVerificationViewEvents) -> Unit = {}, +) = OutgoingVerificationState( + step = step, + request = request, + eventSink = eventSink, +) 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 new file mode 100644 index 00000000000..8669472cf4c --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt @@ -0,0 +1,325 @@ +/* + * 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. + */ + +package io.element.android.features.verifysession.impl.outgoing + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults.topAppBarColors +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.verifysession.impl.R +import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationState.Step +import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu +import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.PageTitle +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.Button +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.InvisibleButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OutgoingVerificationView( + state: OutgoingVerificationState, + onLearnMoreClick: () -> Unit, + onFinish: () -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + val step = state.step + fun cancelOrResetFlow() { + when (step) { + is Step.Canceled -> state.eventSink(OutgoingVerificationViewEvents.Reset) + Step.Initial, Step.Completed -> onBack() + Step.Ready, is Step.AwaitingOtherDeviceResponse -> state.eventSink(OutgoingVerificationViewEvents.Cancel) + is Step.Verifying -> { + if (!step.state.isLoading()) { + state.eventSink(OutgoingVerificationViewEvents.DeclineVerification) + } + } + else -> Unit + } + } + + BackHandler { + cancelOrResetFlow() + } + + if (step is Step.Loading) { + // Just display a loader in this case, to avoid UI glitch. + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } else { + HeaderFooterPage( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + navigationIcon = if (step != Step.Completed) { + { BackButton(onClick = ::cancelOrResetFlow) } + } else { + {} + }, + colors = topAppBarColors(containerColor = Color.Transparent) + ) + }, + header = { + OutgoingVerificationHeader(step = step, request = state.request) + }, + footer = { + OutgoingVerificationViewBottomMenu( + screenState = state, + onCancelClick = ::cancelOrResetFlow, + onContinueClick = onFinish, + ) + }, + isScrollable = true, + ) { + OutgoingVerificationContent( + flowState = step, + request = state.request, + onLearnMoreClick = onLearnMoreClick, + ) + } + } +} + +@Composable +private fun OutgoingVerificationHeader(step: Step, request: VerificationRequest.Outgoing) { + val iconStyle = when (step) { + Step.Loading -> error("Should not happen") + Step.Initial -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> BigIcon.Style.Default(CompoundIcons.Devices()) + is VerificationRequest.Outgoing.User -> BigIcon.Style.Default(CompoundIcons.LockSolid()) + } + Step.AwaitingOtherDeviceResponse -> BigIcon.Style.Loading + Step.Canceled -> BigIcon.Style.AlertSolid + Step.Ready -> BigIcon.Style.Default(CompoundIcons.ReactionSolid()) + Step.Completed -> BigIcon.Style.SuccessSolid + is Step.Verifying -> { + if (step.state is AsyncData.Loading) { + BigIcon.Style.Loading + } else { + BigIcon.Style.Default(CompoundIcons.ReactionSolid()) + } + } + is Step.Exit -> return + } + val titleTextId = when (step) { + Step.Loading -> error("Should not happen") + Step.Initial -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_session_verification_use_another_device_title + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_user_initiator_title + } + Step.AwaitingOtherDeviceResponse -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_session_verification_waiting_another_device_title + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_waiting_other_user_title + } + Step.Canceled -> CommonStrings.common_verification_failed + Step.Ready -> R.string.screen_session_verification_compare_emojis_title + Step.Completed -> CommonStrings.common_verification_complete + is Step.Verifying -> when (step.data) { + is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title + is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title + } + is Step.Exit -> return + } + val subtitleTextId = when (step) { + Step.Loading -> error("Should not happen") + Step.Initial -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_session_verification_use_another_device_subtitle + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_user_initiator_subtitle + } + Step.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_subtitle + Step.Canceled -> R.string.screen_session_verification_failed_subtitle + Step.Ready -> R.string.screen_session_verification_ready_subtitle + Step.Completed -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_identity_confirmed_subtitle + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_complete_user_subtitle + } + is Step.Verifying -> when (step.data) { + is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle + is SessionVerificationData.Emojis -> when (request) { + is VerificationRequest.Outgoing.CurrentSession -> R.string.screen_session_verification_compare_emojis_subtitle + is VerificationRequest.Outgoing.User -> R.string.screen_session_verification_compare_emojis_user_subtitle + } + } + is Step.Exit -> return + } + + PageTitle( + iconStyle = iconStyle, + title = stringResource(id = titleTextId), + subtitle = stringResource(id = subtitleTextId) + ) +} + +@Composable +private fun OutgoingVerificationContent( + flowState: Step, + request: VerificationRequest.Outgoing, + onLearnMoreClick: () -> Unit, +) { + when (flowState) { + is Step.Initial -> { + when (request) { + is VerificationRequest.Outgoing.CurrentSession -> Unit + is VerificationRequest.Outgoing.User -> ContentInitial(onLearnMoreClick) + } + } + is Step.Verifying -> { + VerificationContentVerifying(flowState.data) + } + else -> Unit + } +} + +@Composable +private fun ContentInitial( + onLearnMoreClick: () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Text( + modifier = Modifier + .clickable { onLearnMoreClick() } + .padding(vertical = 4.dp, horizontal = 16.dp), + text = stringResource(CommonStrings.action_learn_more), + style = ElementTheme.typography.fontBodyLgMedium + ) + } +} + +@Composable +private fun OutgoingVerificationViewBottomMenu( + screenState: OutgoingVerificationState, + onCancelClick: () -> Unit, + onContinueClick: () -> Unit, +) { + val verificationViewState = screenState.step + val eventSink = screenState.eventSink + + val isVerifying = (verificationViewState as? Step.Verifying)?.state is AsyncData.Loading + + when (verificationViewState) { + Step.Loading -> error("Should not happen") + is Step.Initial -> { + VerificationBottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_start_verification), + onClick = { eventSink(OutgoingVerificationViewEvents.RequestVerification) }, + ) + InvisibleButton() + } + } + is Step.Canceled -> { + VerificationBottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_done), + onClick = onCancelClick, + ) + InvisibleButton() + } + } + is Step.Ready -> { + VerificationBottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_start), + onClick = { eventSink(OutgoingVerificationViewEvents.StartSasVerification) }, + ) + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_cancel), + onClick = onCancelClick, + ) + } + } + is Step.AwaitingOtherDeviceResponse -> Unit + is Step.Verifying -> { + if (isVerifying) { + // Add invisible buttons to keep the same screen layout + VerificationBottomMenu { + InvisibleButton() + InvisibleButton() + } + } else { + VerificationBottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_they_match), + onClick = { + eventSink(OutgoingVerificationViewEvents.ConfirmVerification) + }, + ) + + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_they_dont_match), + onClick = { eventSink(OutgoingVerificationViewEvents.DeclineVerification) }, + ) + } + } + } + is Step.Completed -> { + VerificationBottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_continue), + onClick = onContinueClick, + ) + InvisibleButton() + } + } + is Step.Exit -> return + } +} + +@PreviewsDayNight +@Composable +internal fun OutgoingVerificationViewPreview(@PreviewParameter(OutgoingVerificationStateProvider::class) state: OutgoingVerificationState) = ElementPreview { + OutgoingVerificationView( + state = state, + onLearnMoreClick = {}, + onFinish = {}, + onBack = {}, + ) +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewEvents.kt new file mode 100644 index 00000000000..3a59f579c8f --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewEvents.kt @@ -0,0 +1,17 @@ +/* + * 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. + */ + +package io.element.android.features.verifysession.impl.outgoing + +sealed interface OutgoingVerificationViewEvents { + data object RequestVerification : OutgoingVerificationViewEvents + data object StartSasVerification : OutgoingVerificationViewEvents + data object ConfirmVerification : OutgoingVerificationViewEvents + data object DeclineVerification : OutgoingVerificationViewEvents + data object Cancel : OutgoingVerificationViewEvents + data object Reset : OutgoingVerificationViewEvents +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt deleted file mode 100644 index bf7afffaadb..00000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.verifysession.impl.outgoing - -import android.app.Activity -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 com.bumble.appyx.core.plugin.plugins -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.appconfig.LearnMoreConfig -import io.element.android.compound.theme.ElementTheme -import io.element.android.features.logout.api.util.onSuccessLogout -import io.element.android.features.verifysession.api.VerifySessionEntryPoint -import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab -import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.SessionScope - -@ContributesNode(SessionScope::class) -class VerifySelfSessionNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - presenterFactory: VerifySelfSessionPresenter.Factory, -) : Node(buildContext, plugins = plugins) { - private val callback = plugins().first() - - private val presenter = presenterFactory.create( - showDeviceVerifiedScreen = inputs().showDeviceVerifiedScreen, - ) - - private fun onLearnMoreClick(activity: Activity, dark: Boolean) { - activity.openUrlInChromeCustomTab(null, dark, LearnMoreConfig.ENCRYPTION_URL) - } - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - val activity = LocalContext.current as Activity - val isDark = ElementTheme.isLightTheme.not() - VerifySelfSessionView( - state = state, - modifier = modifier, - onLearnMoreClick = { - onLearnMoreClick(activity, isDark) - }, - onEnterRecoveryKey = callback::onEnterRecoveryKey, - onResetKey = callback::onResetKey, - onFinish = callback::onDone, - onSuccessLogout = { onSuccessLogout(activity, isDark, it) }, - ) - } -} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt deleted file mode 100644 index 7f8ec701fda..00000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package io.element.android.features.verifysession.impl.outgoing - -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 -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import com.freeletics.flowredux.compose.rememberStateAndDispatch -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.element.android.features.logout.api.LogoutUseCase -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.core.meta.BuildMeta -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.verification.SessionVerificationService -import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus -import io.element.android.libraries.matrix.api.verification.VerificationFlowState -import io.element.android.libraries.preferences.api.store.SessionPreferencesStore -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import timber.log.Timber -import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.Event as StateMachineEvent -import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.State as StateMachineState - -class VerifySelfSessionPresenter @AssistedInject constructor( - @Assisted private val showDeviceVerifiedScreen: Boolean, - private val sessionVerificationService: SessionVerificationService, - private val encryptionService: EncryptionService, - private val stateMachine: VerifySelfSessionStateMachine, - private val buildMeta: BuildMeta, - private val sessionPreferencesStore: SessionPreferencesStore, - private val logoutUseCase: LogoutUseCase, -) : Presenter { - @AssistedFactory - interface Factory { - fun create(showDeviceVerifiedScreen: Boolean): VerifySelfSessionPresenter - } - - @Composable - override fun present(): VerifySelfSessionState { - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(Unit) { - // Force reset, just in case the service was left in a broken state - sessionVerificationService.reset(true) - } - val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() - val stateAndDispatch = stateMachine.rememberStateAndDispatch() - val skipVerification by sessionPreferencesStore.isSessionVerificationSkipped().collectAsState(initial = false) - val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState() - val signOutAction = remember { - mutableStateOf>(AsyncAction.Uninitialized) - } - val step by remember { - derivedStateOf { - if (skipVerification) { - VerifySelfSessionState.Step.Skipped - } else { - when (sessionVerifiedStatus) { - SessionVerifiedStatus.Unknown -> VerifySelfSessionState.Step.Loading - SessionVerifiedStatus.NotVerified -> { - stateAndDispatch.state.value.toVerificationStep( - canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE - ) - } - SessionVerifiedStatus.Verified -> { - if (stateAndDispatch.state.value != StateMachineState.Initial || showDeviceVerifiedScreen) { - // The user has verified the session, we need to show the success screen - VerifySelfSessionState.Step.Completed - } else { - // Automatic verification, which can happen on freshly created account, in this case, skip the screen - VerifySelfSessionState.Step.Skipped - } - } - } - } - } - } - // Start this after observing state machine - LaunchedEffect(Unit) { - observeVerificationService() - } - - fun handleEvents(event: VerifySelfSessionViewEvents) { - Timber.d("Verification user action: ${event::class.simpleName}") - when (event) { - VerifySelfSessionViewEvents.UseAnotherDevice -> stateAndDispatch.dispatchAction(StateMachineEvent.UseAnotherDevice) - VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification) - VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification) - VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge) - VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge) - VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel) - VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset) - VerifySelfSessionViewEvents.SignOut -> coroutineScope.signOut(signOutAction) - VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch { - sessionPreferencesStore.setSkipSessionVerification(true) - } - } - } - return VerifySelfSessionState( - step = step, - signOutAction = signOutAction.value, - displaySkipButton = true, - eventSink = ::handleEvents, - ) - } - - private fun StateMachineState?.toVerificationStep( - canEnterRecoveryKey: Boolean - ): VerifySelfSessionState.Step = - when (val machineState = this) { - StateMachineState.Initial, null -> { - VerifySelfSessionState.Step.Initial( - canEnterRecoveryKey = canEnterRecoveryKey, - isLastDevice = encryptionService.isLastDevice.value - ) - } - VerifySelfSessionStateMachine.State.UseAnotherDevice -> { - VerifySelfSessionState.Step.UseAnotherDevice - } - StateMachineState.RequestingVerification, - StateMachineState.StartingSasVerification, - StateMachineState.SasVerificationStarted, - StateMachineState.Canceling -> { - VerifySelfSessionState.Step.AwaitingOtherDeviceResponse - } - - StateMachineState.VerificationRequestAccepted -> { - VerifySelfSessionState.Step.Ready - } - - StateMachineState.Canceled -> { - VerifySelfSessionState.Step.Canceled - } - - is StateMachineState.Verifying -> { - val async = when (machineState) { - is StateMachineState.Verifying.Replying -> AsyncData.Loading() - else -> AsyncData.Uninitialized - } - VerifySelfSessionState.Step.Verifying(machineState.data, async) - } - - StateMachineState.Completed -> { - VerifySelfSessionState.Step.Completed - } - } - - private fun CoroutineScope.observeVerificationService() { - sessionVerificationService.verificationFlowState - .onEach { Timber.d("Verification flow state: ${it::class.simpleName}") } - .onEach { verificationAttemptState -> - when (verificationAttemptState) { - VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Reset) - VerificationFlowState.DidAcceptVerificationRequest -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest) - } - VerificationFlowState.DidStartSasVerification -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidStartSasVerification) - } - is VerificationFlowState.DidReceiveVerificationData -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data)) - } - VerificationFlowState.DidFinish -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptChallenge) - } - VerificationFlowState.DidCancel -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidCancel) - } - VerificationFlowState.DidFail -> { - stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidFail) - } - } - } - .launchIn(this) - } - - private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { - suspend { - logoutUseCase.logout(ignoreSdkError = true) - }.runCatchingUpdatingState(signOutAction) - } -} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt deleted file mode 100644 index 6c04042b929..00000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.verifysession.impl.outgoing - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.verification.SessionVerificationData - -@Immutable -data class VerifySelfSessionState( - val step: Step, - val signOutAction: AsyncAction, - val displaySkipButton: Boolean, - val eventSink: (VerifySelfSessionViewEvents) -> Unit, -) { - @Stable - sealed interface Step { - data object Loading : Step - - // FIXME canEnterRecoveryKey value is never read. - data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : Step - data object UseAnotherDevice : Step - data object Canceled : Step - data object AwaitingOtherDeviceResponse : Step - data object Ready : Step - data class Verifying(val data: SessionVerificationData, val state: AsyncData) : Step - data object Completed : Step - data object Skipped : Step - } -} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt deleted file mode 100644 index a71b09c7831..00000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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. - */ - -@file:Suppress("WildcardImport") -@file:OptIn(ExperimentalCoroutinesApi::class) - -package io.element.android.features.verifysession.impl.outgoing - -import com.freeletics.flowredux.dsl.FlowReduxStateMachine -import io.element.android.features.verifysession.impl.util.andLogStateChange -import io.element.android.features.verifysession.impl.util.logReceivedEvents -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.core.data.tryOrNull -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.verification.SessionVerificationData -import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.timeout -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds -import com.freeletics.flowredux.dsl.State as MachineState - -@OptIn(FlowPreview::class) -class VerifySelfSessionStateMachine @Inject constructor( - private val sessionVerificationService: SessionVerificationService, - private val encryptionService: EncryptionService, -) : FlowReduxStateMachine( - initialState = State.Initial -) { - init { - spec { - inState { - on { _: Event.UseAnotherDevice, state -> - state.override { State.UseAnotherDevice.andLogStateChange() } - } - } - inState { - on { _: Event.RequestVerification, state -> - state.override { State.RequestingVerification.andLogStateChange() } - } - } - inState { - onEnterEffect { - sessionVerificationService.requestVerification() - } - on { _: Event.DidAcceptVerificationRequest, state -> - state.override { State.VerificationRequestAccepted.andLogStateChange() } - } - } - inState { - onEnterEffect { - sessionVerificationService.startVerification() - } - } - inState { - on { _: Event.StartSasVerification, state -> - state.override { State.StartingSasVerification.andLogStateChange() } - } - } - inState { - on { _: Event.Reset, state -> - state.override { State.Initial.andLogStateChange() } - } - } - inState { - on { event: Event.DidReceiveChallenge, state -> - state.override { State.Verifying.ChallengeReceived(event.data).andLogStateChange() } - } - } - inState { - on { _: Event.AcceptChallenge, state -> - state.override { State.Verifying.Replying(state.snapshot.data, accept = true).andLogStateChange() } - } - on { _: Event.DeclineChallenge, state -> - state.override { State.Verifying.Replying(state.snapshot.data, accept = false).andLogStateChange() } - } - } - inState { - onEnterEffect { state -> - if (state.accept) { - sessionVerificationService.approveVerification() - } else { - sessionVerificationService.declineVerification() - } - } - on { _: Event.DidAcceptChallenge, state -> - // If a key backup exists, wait until it's restored or a timeout happens - val hasBackup = encryptionService.doesBackupExistOnServer().getOrNull().orFalse() - if (hasBackup) { - tryOrNull { - encryptionService.recoveryStateStateFlow.filter { it == RecoveryState.ENABLED } - .timeout(10.seconds) - .first() - } - } - state.override { State.Completed.andLogStateChange() } - } - } - inState { - // TODO The 'Canceling' -> 'Canceled' transitions doesn't seem to work anymore, check if something changed in the Rust SDK - onEnterEffect { - sessionVerificationService.cancelVerification() - } - } - inState { - logReceivedEvents() - on { _: Event.DidStartSasVerification, state: MachineState -> - state.override { State.SasVerificationStarted.andLogStateChange() } - } - on { _: Event.Cancel, state: MachineState -> - when (state.snapshot) { - State.Initial, State.Completed, State.Canceled -> state.noChange() - State.UseAnotherDevice -> state.override { State.Initial.andLogStateChange() } - // For some reason `cancelVerification` is not calling its delegate `didCancel` method so we don't pass from - // `Canceling` state to `Canceled` automatically anymore - else -> { - sessionVerificationService.cancelVerification() - state.override { State.Canceled.andLogStateChange() } - } - } - } - on { _: Event.DidCancel, state: MachineState -> - state.override { State.Canceled.andLogStateChange() } - } - on { _: Event.DidFail, state: MachineState -> - when (state.snapshot) { - is State.RequestingVerification -> state.override { State.Initial.andLogStateChange() } - else -> state.override { State.Canceled.andLogStateChange() } - } - } - } - } - } - - sealed interface State { - /** The initial state, before verification started. */ - data object Initial : State - - /** Let the user know that they need to get ready on their other session. */ - data object UseAnotherDevice : State - - /** Waiting for verification acceptance. */ - data object RequestingVerification : State - - /** Verification request accepted. Waiting for start. */ - data object VerificationRequestAccepted : State - - /** Waiting for SaS verification start. */ - data object StartingSasVerification : State - - /** A SaS verification flow has been started. */ - data object SasVerificationStarted : State - - sealed class Verifying(open val data: SessionVerificationData) : State { - /** Verification accepted and emojis received. */ - data class ChallengeReceived(override val data: SessionVerificationData) : Verifying(data) - - /** Replying to a verification challenge. */ - data class Replying(override val data: SessionVerificationData, val accept: Boolean) : Verifying(data) - } - - /** The verification is being canceled. */ - data object Canceling : State - - /** The verification has been canceled, remotely or locally. */ - data object Canceled : State - - /** Verification successful. */ - data object Completed : State - } - - sealed interface Event { - /** User wants to use another session. */ - data object UseAnotherDevice : Event - - /** Request verification. */ - data object RequestVerification : Event - - /** The current verification request has been accepted. */ - data object DidAcceptVerificationRequest : Event - - /** Start a SaS verification flow. */ - data object StartSasVerification : Event - - /** Started a SaS verification flow. */ - data object DidStartSasVerification : Event - - /** Has received data. */ - data class DidReceiveChallenge(val data: SessionVerificationData) : Event - - /** Emojis match. */ - data object AcceptChallenge : Event - - /** Emojis do not match. */ - data object DeclineChallenge : Event - - /** Remote accepted challenge. */ - data object DidAcceptChallenge : Event - - /** Request cancellation. */ - data object Cancel : Event - - /** Verification cancelled. */ - data object DidCancel : Event - - /** Request failed. */ - data object DidFail : Event - - /** Reset the verification flow to the initial state. */ - data object Reset : Event - } -} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt deleted file mode 100644 index 31927562bf5..00000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.verifysession.impl.outgoing - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step -import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData -import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData - -open class VerifySelfSessionStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aVerifySelfSessionState(displaySkipButton = true), - aVerifySelfSessionState( - step = Step.AwaitingOtherDeviceResponse - ), - aVerifySelfSessionState( - step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized) - ), - aVerifySelfSessionState( - step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading()) - ), - aVerifySelfSessionState( - step = Step.Canceled - ), - aVerifySelfSessionState( - step = Step.Ready - ), - aVerifySelfSessionState( - step = Step.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized) - ), - aVerifySelfSessionState( - step = Step.Initial(canEnterRecoveryKey = true) - ), - aVerifySelfSessionState( - step = Step.Initial(canEnterRecoveryKey = true, isLastDevice = true) - ), - aVerifySelfSessionState( - step = Step.Completed, - displaySkipButton = true, - ), - aVerifySelfSessionState( - signOutAction = AsyncAction.Loading, - displaySkipButton = true, - ), - aVerifySelfSessionState( - step = Step.Loading - ), - aVerifySelfSessionState( - step = Step.Skipped - ), - aVerifySelfSessionState( - step = Step.UseAnotherDevice - ), - // Add other state here - ) -} - -internal fun aVerifySelfSessionState( - step: Step = Step.Initial(canEnterRecoveryKey = false), - signOutAction: AsyncAction = AsyncAction.Uninitialized, - displaySkipButton: Boolean = false, - eventSink: (VerifySelfSessionViewEvents) -> Unit = {}, -) = VerifySelfSessionState( - step = step, - displaySkipButton = displaySkipButton, - eventSink = eventSink, - signOutAction = signOutAction, -) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt deleted file mode 100644 index ca683424e95..00000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt +++ /dev/null @@ -1,377 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.verifysession.impl.outgoing - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -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.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode -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.verifysession.impl.R -import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step -import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu -import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage -import io.element.android.libraries.designsystem.components.BigIcon -import io.element.android.libraries.designsystem.components.PageTitle -import io.element.android.libraries.designsystem.components.ProgressDialog -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.CircularProgressIndicator -import io.element.android.libraries.designsystem.theme.components.InvisibleButton -import io.element.android.libraries.designsystem.theme.components.OutlinedButton -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TextButton -import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.matrix.api.verification.SessionVerificationData -import io.element.android.libraries.ui.strings.CommonStrings - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun VerifySelfSessionView( - state: VerifySelfSessionState, - onLearnMoreClick: () -> Unit, - onEnterRecoveryKey: () -> Unit, - onResetKey: () -> Unit, - onFinish: () -> Unit, - onSuccessLogout: (String?) -> Unit, - modifier: Modifier = Modifier, -) { - val step = state.step - fun cancelOrResetFlow() { - when (step) { - is Step.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset) - is Step.AwaitingOtherDeviceResponse, - Step.UseAnotherDevice, - Step.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel) - is Step.Verifying -> { - if (!step.state.isLoading()) { - state.eventSink(VerifySelfSessionViewEvents.DeclineVerification) - } - } - else -> Unit - } - } - - val latestOnFinish by rememberUpdatedState(newValue = onFinish) - LaunchedEffect(step, latestOnFinish) { - if (step is Step.Skipped) { - latestOnFinish() - } - } - BackHandler { - cancelOrResetFlow() - } - - if (step is Step.Loading || - step is Step.Skipped) { - // Just display a loader in this case, to avoid UI glitch. - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - } else { - HeaderFooterPage( - modifier = modifier, - topBar = { - TopAppBar( - title = {}, - actions = { - if (step !is Step.Completed && - state.displaySkipButton && - LocalInspectionMode.current.not()) { - TextButton( - text = stringResource(CommonStrings.action_skip), - onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) } - ) - } - if (step is Step.Initial) { - TextButton( - text = stringResource(CommonStrings.action_signout), - onClick = { state.eventSink(VerifySelfSessionViewEvents.SignOut) } - ) - } - } - ) - }, - header = { - VerifySelfSessionHeader(step = step) - }, - footer = { - VerifySelfSessionBottomMenu( - screenState = state, - onCancelClick = ::cancelOrResetFlow, - onEnterRecoveryKey = onEnterRecoveryKey, - onContinueClick = onFinish, - onResetKey = onResetKey, - ) - } - ) { - VerifySelfSessionContent( - flowState = step, - onLearnMoreClick = onLearnMoreClick, - ) - } - } - - when (state.signOutAction) { - AsyncAction.Loading -> { - ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) - } - is AsyncAction.Success -> { - val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout) - LaunchedEffect(state) { - latestOnSuccessLogout(state.signOutAction.data) - } - } - is AsyncAction.Confirming, - is AsyncAction.Failure, - AsyncAction.Uninitialized -> Unit - } -} - -@Composable -private fun VerifySelfSessionHeader(step: Step) { - val iconStyle = when (step) { - Step.Loading -> error("Should not happen") - is Step.Initial -> BigIcon.Style.Default(CompoundIcons.LockSolid()) - Step.UseAnotherDevice -> BigIcon.Style.Default(CompoundIcons.Devices()) - Step.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.Devices()) - Step.Canceled -> BigIcon.Style.AlertSolid - Step.Ready, is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction()) - Step.Completed -> BigIcon.Style.SuccessSolid - is Step.Skipped -> return - } - val titleTextId = when (step) { - Step.Loading -> error("Should not happen") - is Step.Initial -> R.string.screen_identity_confirmation_title - Step.UseAnotherDevice -> R.string.screen_session_verification_use_another_device_title - Step.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_another_device_title - Step.Canceled -> CommonStrings.common_verification_failed - Step.Ready -> R.string.screen_session_verification_compare_emojis_title - Step.Completed -> R.string.screen_identity_confirmed_title - is Step.Verifying -> when (step.data) { - is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title - is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title - } - is Step.Skipped -> return - } - val subtitleTextId = when (step) { - Step.Loading -> error("Should not happen") - is Step.Initial -> R.string.screen_identity_confirmation_subtitle - Step.UseAnotherDevice -> R.string.screen_session_verification_use_another_device_subtitle - Step.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_another_device_subtitle - Step.Canceled -> R.string.screen_session_verification_failed_subtitle - Step.Ready -> R.string.screen_session_verification_ready_subtitle - Step.Completed -> R.string.screen_identity_confirmed_subtitle - is Step.Verifying -> when (step.data) { - is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle - is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle - } - is Step.Skipped -> return - } - - PageTitle( - iconStyle = iconStyle, - title = stringResource(id = titleTextId), - subtitle = stringResource(id = subtitleTextId) - ) -} - -@Composable -private fun VerifySelfSessionContent( - flowState: Step, - onLearnMoreClick: () -> Unit, -) { - when (flowState) { - is Step.Initial -> { - ContentInitial(onLearnMoreClick) - } - is Step.Verifying -> { - VerificationContentVerifying(flowState.data) - } - else -> Unit - } -} - -@Composable -private fun ContentInitial( - onLearnMoreClick: () -> Unit, -) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - Text( - modifier = Modifier - .clickable { onLearnMoreClick() } - .padding(vertical = 4.dp, horizontal = 16.dp), - text = stringResource(CommonStrings.action_learn_more), - style = ElementTheme.typography.fontBodyLgMedium - ) - } -} - -@Composable -private fun VerifySelfSessionBottomMenu( - screenState: VerifySelfSessionState, - onEnterRecoveryKey: () -> Unit, - onResetKey: () -> Unit, - onCancelClick: () -> Unit, - onContinueClick: () -> Unit, -) { - val verificationViewState = screenState.step - val eventSink = screenState.eventSink - - val isVerifying = (verificationViewState as? Step.Verifying)?.state is AsyncData.Loading - - when (verificationViewState) { - Step.Loading -> error("Should not happen") - is Step.Initial -> { - VerificationBottomMenu { - if (verificationViewState.isLastDevice.not()) { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_use_another_device), - onClick = { eventSink(VerifySelfSessionViewEvents.UseAnotherDevice) }, - ) - } - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_session_verification_enter_recovery_key), - onClick = onEnterRecoveryKey, - ) - OutlinedButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), - onClick = onResetKey, - ) - } - } - is Step.UseAnotherDevice -> { - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(CommonStrings.action_start_verification), - onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, - ) - InvisibleButton() - } - } - is Step.Canceled -> { - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(CommonStrings.action_done), - onClick = onCancelClick, - ) - InvisibleButton() - } - } - is Step.Ready -> { - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(CommonStrings.action_start), - onClick = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) }, - ) - TextButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(CommonStrings.action_cancel), - onClick = onCancelClick, - ) - } - } - is Step.AwaitingOtherDeviceResponse -> { - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_waiting_on_other_device), - onClick = {}, - showProgress = true, - enabled = false, - ) - InvisibleButton() - } - } - is Step.Verifying -> { - val positiveButtonTitle = if (isVerifying) { - stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing) - } else { - stringResource(R.string.screen_session_verification_they_match) - } - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = positiveButtonTitle, - showProgress = isVerifying, - enabled = !isVerifying, - onClick = { - if (!isVerifying) { - eventSink(VerifySelfSessionViewEvents.ConfirmVerification) - } - }, - ) - if (isVerifying) { - InvisibleButton() - } else { - TextButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_session_verification_they_dont_match), - onClick = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) }, - ) - } - } - } - is Step.Completed -> { - VerificationBottomMenu { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(CommonStrings.action_continue), - onClick = onContinueClick, - ) - InvisibleButton() - } - } - is Step.Skipped -> return - } -} - -@PreviewsDayNight -@Composable -internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview { - VerifySelfSessionView( - state = state, - onLearnMoreClick = {}, - onEnterRecoveryKey = {}, - onResetKey = {}, - onFinish = {}, - onSuccessLogout = {}, - ) -} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt deleted file mode 100644 index 752dbc3d7aa..00000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.verifysession.impl.outgoing - -sealed interface VerifySelfSessionViewEvents { - data object UseAnotherDevice : VerifySelfSessionViewEvents - data object RequestVerification : VerifySelfSessionViewEvents - data object StartSasVerification : VerifySelfSessionViewEvents - data object ConfirmVerification : VerifySelfSessionViewEvents - data object DeclineVerification : VerifySelfSessionViewEvents - data object Cancel : VerifySelfSessionViewEvents - data object Reset : VerifySelfSessionViewEvents - data object SignOut : VerifySelfSessionViewEvents - data object SkipVerification : VerifySelfSessionViewEvents -} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/Common.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/Common.kt index a3724912a1c..8859a4180b1 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/Common.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/Common.kt @@ -23,11 +23,11 @@ internal fun aDecimalsSessionVerificationData( } private fun aVerificationEmojiList() = listOf( - VerificationEmoji(number = 27, emoji = "🍕", description = "Pizza"), - VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"), - VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"), - VerificationEmoji(number = 42, emoji = "📕", description = "Book"), - VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"), - VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"), - VerificationEmoji(number = 63, emoji = "📌", description = "Pin"), + VerificationEmoji(number = 27), + VerificationEmoji(number = 54), + VerificationEmoji(number = 54), + VerificationEmoji(number = 42), + VerificationEmoji(number = 48), + VerificationEmoji(number = 48), + VerificationEmoji(number = 63), ) 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 4484c9c7081..b2010bd61ca 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 @@ -16,9 +16,9 @@ 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.widthIn -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,7 +39,7 @@ internal fun VerificationContentVerifying( modifier: Modifier = Modifier, ) { Box( - modifier = modifier.fillMaxSize(), + modifier = modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.Center ) { when (data) { @@ -49,7 +49,7 @@ internal fun VerificationContentVerifying( modifier = Modifier.fillMaxWidth(), text = text, style = ElementTheme.typography.fontHeadingLgBold, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, textAlign = TextAlign.Center, ) } @@ -86,9 +86,9 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie Text( text = stringResource(id = emojiResource.nameRes), style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + color = ElementTheme.colors.textSecondary, + overflow = TextOverflow.Visible, + textAlign = TextAlign.Center, ) } } 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 new file mode 100644 index 00000000000..4f6cfcf4566 --- /dev/null +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt @@ -0,0 +1,79 @@ +/* + * 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.verifysession.impl.ui + +import androidx.compose.foundation.background +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.padding +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.unit.dp +import io.element.android.compound.theme.ElementTheme +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.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.matrix.api.core.UserId + +@Composable +fun VerificationUserProfileContent( + userId: UserId, + displayName: String?, + avatarUrl: String?, + modifier: Modifier = Modifier, +) { + val avatarData = remember(userId, displayName, avatarUrl) { + AvatarData(id = userId.value, name = displayName, url = avatarUrl, size = AvatarSize.UserVerification) + } + + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(ElementTheme.colors.bgSubtleSecondary) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar( + avatarData = avatarData, + avatarType = AvatarType.User, + ) + Spacer(modifier = Modifier.padding(12.dp)) + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text(text = displayName ?: userId.value, style = ElementTheme.typography.fontBodyLgMedium, color = ElementTheme.colors.textPrimary) + + if (displayName != null) { + Text(text = userId.value, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun VerificationUserProfileContentPreview() = ElementPreview( + drawableFallbackForImages = CommonDrawables.sample_avatar +) { + VerificationUserProfileContent( + userId = UserId("@alice:example.com"), + displayName = "Alice", + avatarUrl = "https://example.com/avatar.png", + ) +} diff --git a/features/verifysession/impl/src/main/res/values-be/translations.xml b/features/verifysession/impl/src/main/res/values-be/translations.xml index da894a2ddb4..c48da7e8d0a 100644 --- a/features/verifysession/impl/src/main/res/values-be/translations.xml +++ b/features/verifysession/impl/src/main/res/values-be/translations.xml @@ -24,6 +24,7 @@ "Чаканне супадзення" "Параўнайце ўнікальны набор эмодзі." "Параўнайце ўнікальныя эмодзі, пераканаўшыся, што яны размешчаны ў тым жа парадку." + "Прылада праверана" "Яны не супадаюць" "Яны супадаюць" "Для працягу працы прыміце запыт на запуск працэсу праверкі ў іншым сеансе." diff --git a/features/verifysession/impl/src/main/res/values-bg/translations.xml b/features/verifysession/impl/src/main/res/values-bg/translations.xml index bd4c7071efd..ab0a15f4edc 100644 --- a/features/verifysession/impl/src/main/res/values-bg/translations.xml +++ b/features/verifysession/impl/src/main/res/values-bg/translations.xml @@ -1,5 +1,12 @@ + "Не можете да потвърдите?" + "Потвърдете това устройство, за да настроите защитени съобщения." + "Потвърдете самоличността си" + "Използване на друго устройство" + "Използване на ключ за възстановяване" + "Устройството е потвърдено" + "Използване на друго устройство" "Нещо не изглежда наред. Или времето за изчакване на заявката е изтекло, или заявката е отхвърлена." "Потвърдете, че емоджитата по-долу съвпадат с показаните в другата ви сесия." "Сравнете емоджита" @@ -10,8 +17,16 @@ "Готов съм" "В очакване на съвпадение" "Сравнете уникален набор от емоджита." + "Неуспешно потвърждаване" + "Сега можете да четете или изпращате съобщения сигурно на другото си устройство." + "Устройството е потвърдено" "Те не съвпадат" "Те съвпадат" + "Уверете се, че приложението е отворено на другото устройство, преди да започнете потвърждението оттук." + "Отворете приложението на друго потвърдено устройство" + "Чака се другото устройство" + "Чака се другият потребител" + "След като бъдете приети, ще можете да продължите потвърждението." "Приемете заявката, за да започнете процеса на потвърждаване в другата си сесия, за да продължите." "В очакване на приемане на заявка" "Излизане…" diff --git a/features/verifysession/impl/src/main/res/values-cs/translations.xml b/features/verifysession/impl/src/main/res/values-cs/translations.xml index 30e43eadc99..bdf73c53f10 100644 --- a/features/verifysession/impl/src/main/res/values-cs/translations.xml +++ b/features/verifysession/impl/src/main/res/values-cs/translations.xml @@ -13,9 +13,11 @@ "Něco není v pořádku. Buď vypršel časový limit požadavku, nebo byl požadavek zamítnut." "Zkontrolujte, zda se níže uvedené emotikony shodují s emotikony zobrazenými na jiné relaci." "Porovnání emotikonů" + "Zkontrolujte, zda se níže uvedené emotikony shodují s emotikony zobrazenými v zařízení druhého uživatele." "Potvrďte, že níže uvedená čísla odpovídají číslům zobrazeným na vaší druhé relaci." "Porovnejte čísla" "Vaše nová relace je nyní ověřena. Má přístup k vašim zašifrovaným zprávám a ostatní uživatelé ji uvidí jako důvěryhodnou." + "Nyní můžete důvěřovat identitě tohoto uživatele při odesílání nebo přijímání zpráv." "Zadejte klíč pro obnovení" "Buď vypršel časový limit požadavku, požadavek byl zamítnut, nebo došlo k nesouladu ověření." "Pro přístup k historii zašifrovaných zpráv prokažte, že jste to vy." @@ -37,8 +39,14 @@ "Shodují se" "Před zahájením ověřování se ujistěte, že máte aplikaci otevřenou na druhém zařízení." "Otevřete aplikaci na jiném ověřeném zařízení" + "Pro větší bezpečnost ověřte tohoto uživatele porovnáním sady emotikonů na svých zařízeních. Proveďte to pomocí důvěryhodného způsobu komunikace." + "Ověřte tohoto uživatele?" + "Pro větší bezpečnost chce jiný uživatel ověřit vaši identitu. Zobrazí se vám sada emotikonů k porovnání." "Na druhém zařízení byste měli vidět vyskakovací okno. Začněte s ověrením tam." "Spusťte ověření na druhém zařízení" + "Čekání na druhé zařízení" + "Čekání na druhého uživatele" + "Po přijetí budete moci pokračovat v ověřování." "Pro pokračování přijměte požadavek na zahájení ověření v jiné relaci." "Čekání na přijetí žádosti" "Odhlašování…" diff --git a/features/verifysession/impl/src/main/res/values-cy/translations.xml b/features/verifysession/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..eff8ac87d93 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,53 @@ + + + "Methu cadarnhau?" + "Crëwch allwedd adfer newydd" + "Dilyswch y ddyfais hon er mwyn gosod negeseuon diogel." + "Cadarnhewch eich hunaniaeth" + "Defnyddiwch ddyfais arall" + "Defnyddiwch allwedd adfer" + "Nawr gallwch chi ddarllen neu anfon negeseuon yn ddiogel, a gall unrhyw un rydych chi\'n sgwrsio â nhw ymddiried yn y ddyfais hon hefyd." + "Dyfais wedi\'i dilysu" + "Defnyddiwch ddyfais arall" + "Yn aros ar ddyfais arall…" + "Mae rhywbeth i weld o\'i le. Naill ai daeth y cais i ben neu cafodd y cais ei wrthod." + "Cadarnhewch fod yr emojis isod yn cyd-fynd â\'r rhai sy\'n cael eu dangos ar eich sesiwn arall." + "Cymharwch emojis" + "Cadarnhewch fod yr emojis isod yn cyd-fynd â\'r rhai sy\'n cael eu dangos ar ddyfais y defnyddiwr arall." + "Cadarnhewch fod y rhifau isod yn cyfateb i\'r rhai sy\'n cael eu dangos ar eich sesiwn arall." + "Cymharwch rifau" + "Mae eich sesiwn newydd bellach wedi\'i dilysu. Mae ganddo fynediad i\'ch negeseuon wedi\'u hamgryptio, a bydd defnyddwyr eraill yn ei ystyried yn rhai y mae modd ymddiried ynddyn nhw." + "Nawr gallwch ymddiried yn hunaniaeth y defnyddiwr hwn wrth anfon neu dderbyn negeseuon." + "Rhowch eich allwedd adfer" + "Naill ai daeth y cais i ben, gwrthodwyd y cais, neu roedd diffyg cyfatebiaeth dilysu." + "Profwch mai chi sydd yno i gael mynediad at eich hanes negeseuon wedi\'u hamgryptio." + "Agor sesiwn sy\'n bodoli eisoes" + "Rhowch gynnig arall ar ddilysu" + "Rwy\'n barod" + "Aros i gyfateb…" + "Cymharwch set unigryw o emojis." + "Cymharwch yr emoji unigryw, gan sicrhau eu bod yn ymddangos yn yr un drefn." + "Mewngofnodwyd" + "Naill ai daeth y cais i ben, gwrthodwyd y cais, neu roedd diffyg cyfatebiaeth dilysu." + "Methodd y gwirio" + "Parhewch dim ond os taw chi gychwynodd y dilysiad hwn." + "Dilyswch y ddyfais arall i gadw hanes eich neges yn ddiogel." + "Nawr gallwch chi ddarllen neu anfon negeseuon yn ddiogel ar eich dyfais arall." + "Dyfais wedi\'i dilysu" + "Gofynnwyd am ddilysiad" + "Dy\'n nhw ddim yn cyfateb" + "Maen nhw\'n cyfateb" + "Gwnewch yn siŵr fod gennych yr ap ar agor yn y ddyfais arall cyn dechrau dilysu o\'r fan hon." + "Agorwch yr ap ar ddyfais arall sydd wedi\'i gwirio" + "Er mwyn sicrhau diogelwch ychwanegol, gwiriwch y defnyddiwr hwn trwy gymharu set o emojis ar eich dyfeisiau. Gwnewch hyn trwy ddefnyddio ffordd ddibynadwy o gyfathrebu." + "Dilysu\'r defnyddiwr hwn?" + "Er mwyn diogelwch ychwanegol, mae defnyddiwr arall eisiau gwirio pwy ydych chi. Bydd set o emojis yn cael eu dangos i chi eu cymharu." + "Dylech weld llamlen ar y ddyfais arall. Dechreuwch y dilysiad o\'r fan honno nawr." + "Cychwyn dilysu ar y ddyfais arall" + "Yn aros am eich dyfais arall" + "Yn aros am y defnyddiwr arall" + "Unwaith y byddwch wedi\'ch derbyn, byddwch yn gallu parhau â\'r dilysu." + "Derbyniwch y cais i gychwyn y broses ddilysu yn eich sesiwn arall i barhau." + "Yn aros i dderbyn cais" + "Yn allgofnodi…" + diff --git a/features/verifysession/impl/src/main/res/values-da/translations.xml b/features/verifysession/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..a1586feff74 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,53 @@ + + + "Kan ikke bekræfte?" + "Opret en ny gendannelsesnøgle" + "Verificér denne enhed for at konfigurere sikre meddelelser." + "Bekræft din identitet" + "Brug en anden enhed" + "Brug gendannelsesnøgle" + "Nu kan du læse eller sende beskeder sikkert, og enhver du samtaler med kan også stole på denne enhed." + "Enhed verificeret" + "Brug en anden enhed" + "Venter på en anden enhed…" + "Et ellervandet virker ikke rigtigt. Enten udløb anmodningen, eller anmodningen blev afvist." + "Bekræft, at emojierne nedenfor matcher dem, der vises på din anden session." + "Sammenlign emojier" + "Bekræft, at emojierne nedenfor matcher dem, der vises på den anden brugers enhed." + "Bekræft, at numrene nedenfor stemmer overens med dem, der vises på din anden session." + "Sammenlign tal" + "Din nye session er nu bekræftet. Det har adgang til dine krypterede meddelelser, og andre brugere vil se den som betroet." + "Nu kan du stole på identiteten af denne bruger, når I sender og modtager beskeder fra hinanden." + "Indtast gendannelsesnøgle" + "Enten udløb anmodningen, den blev afvist, eller der var en fejl i verifikationen." + "Bevis, at det er dig, for at få adgang til din krypterede beskedhistorik." + "Åbn en eksisterende session" + "Prøv bekræftelsen igen" + "Jeg er klar" + "Venter på at matche…" + "Sammenlign et unikt sæt af emojis." + "Sammenlign de unikke emoji, og vær opmærksom på, at de vises i den samme rækkefølge." + "Logget ind" + "Enten udløb anmodningen, den blev afvist, eller der var en fejl i verifikationen." + "Verifikation mislykkedes" + "Fortsæt kun, hvis du selv har startet denne verifikation." + "Verificér den anden enhed for at holde din meddelelseshistorik sikker." + "Nu kan du læse eller sende beskeder sikkert på din anden enhed." + "Enhed verificeret" + "Anmodet om verifikation" + "De matcher ikke" + "De matcher" + "Sørg for, at du har appen åben på den anden enhed, før du starter verifikationen herfra." + "Åbn appen på en anden bekræftet enhed" + "For ekstra sikkerhed, verificér denne bruger ved at sammenligne et sæt emojier på jeres enheder. Gør dette ved at bruge en kommunikationsmetode i stoler på." + "Verificér denne bruger?" + "For ekstra sikkerhed ønsker en anden bruger at bekræfte din identitet. Du får vist et sæt emojier til sammenligning." + "Du burde se en popup på den anden enhed. Start verifikationen derfra nu." + "Start verifikation på den anden enhed" + "Venter på den anden enhed" + "Venter på den anden bruger" + "Når du er blevet accepteret, kan du fortsætte med verifikationen." + "Accepter anmodningen om at starte bekræftelsesprocessen i din anden session for at fortsætte." + "Venter på at acceptere anmodningen" + "Logger ud…" + 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 4534411033a..5f47ad673eb 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -1,21 +1,23 @@ - "Sie können es nicht bestätigen?" + "Bestätigung unmöglich?" "Erstelle einen neuen Wiederherstellungsschlüssel" "Verifiziere dieses Gerät, um sicheres Messaging einzurichten." - "Bestätige, dass du es bist" + "Bestätigen Sie Ihre Identität" "Ein anderes Gerät verwenden" "Wiederherstellungsschlüssel verwenden" - "Du kannst nun verschlüsselte Nachrichten lesen oder versenden." + "Sie können jetzt verschlüsselte Nachrichten lesen und versenden. Ihre Chatpartner vertrauen nun diesem Gerät auch." "Gerät verifiziert" "Ein anderes Gerät verwenden" "Bitte warten bis das andere Gerät bereit ist." "Etwas scheint nicht zu stimmen. Entweder ist das Zeitlimit für die Anfrage abgelaufen oder die Anfrage wurde abgelehnt." "Vergewissere dich dass die folgenden Emojis mit denen in deiner anderen Session übereinstimmen." "Emojis vergleichen" + "Vergewissern Sie sich, dass die folgenden Emojis mit denen auf dem Gerät des anderen Benutzers übereinstimmen." "Bestätige, dass die Zahlen mit denen deiner anderen Sitzung übereinstimmen." "Vergleiche die Zahlen" "Deine neue Session ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten und wird von anderen Benutzern als vertrauenswürdig eingestuft." + "Jetzt können Sie der Identität dieses Benutzers vertrauen, wenn Sie Nachrichten senden oder empfangen." "Wiederherstellungsschlüssel eingeben" "Entweder ist bei der Anfrage ein Timeout aufgetreten, oder die Anfrage wurde abgelehnt, oder es gab eine Nichtübereinstimmung bei der Überprüfung." "Beweise deine Identität, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen." @@ -27,7 +29,7 @@ "Vergleiche die einzelnen Emojis und stelle sicher, dass sie in der gleichen Reihenfolge erscheinen." "Angemeldet" "Entweder ist bei der Anfrage ein Timeout aufgetreten, oder die Anfrage wurde abgelehnt, oder es gab eine Nichtübereinstimmung bei der Überprüfung." - "Überprüfung fehlgeschlagen" + "Verifizierung fehlgeschlagen" "Fahren Sie nur fort, falls Sie für diese Überprüfung verantwortlich sind.." "Verifizieren Sie das andere Gerät, um die Sicherheit Ihres Nachrichtenverlaufs zu gewährleisten." "Jetzt können Sie gesichert Nachrichten auf Ihrem anderen Gerät lesen oder senden." @@ -37,8 +39,14 @@ "Sie stimmen überein" "Stellen Sie sicher, dass die App auf dem anderen Gerät geöffnet ist, bevor Sie die Überprüfung auf diesem Gerät aus starten." "Öffnen Sie die App auf einem anderen verifizierten Gerät" + "Für zusätzliche Sicherheit verifizieren Sie diesen Benutzer, indem Sie eine Reihe von Emojis auf Ihren Geräten vergleichen. Verwenden Sie dazu eine vertrauenswürdige Art der Kommunikation." + "Diesen Benutzer verifizieren?" + "Für zusätzliche Sicherheit möchte ein anderer Benutzer Ihre Identität überprüfen. Es wird Ihnen eine Reihe von Emojis zum Vergleich angezeigt." "Sie sollten ein Popup-Fenster auf dem anderen Gerät sehen. Starten Sie die Überprüfung von dort aus." "Starten Sie die Überprüfung auf dem anderen Gerät" + "Warten auf das andere Gerät" + "Warten auf den anderen Benutzer" + "Sobald Sie die Bestätigung akzeptiert haben, können Sie mit der Überprüfung fortfahren." "Akzeptiere die Anfrage, um den Verifizierungsprozess in deiner anderen Session zu starten, um fortzufahren." "Warten auf die Annahme der Anfrage" "Abmelden…" diff --git a/features/verifysession/impl/src/main/res/values-el/translations.xml b/features/verifysession/impl/src/main/res/values-el/translations.xml index 1cac842a7b0..74e926f73d3 100644 --- a/features/verifysession/impl/src/main/res/values-el/translations.xml +++ b/features/verifysession/impl/src/main/res/values-el/translations.xml @@ -13,9 +13,11 @@ "Κάτι δεν πάει καλά. Είτε το αίτημα έληξε είτε απορρίφθηκε." "Επιβεβαίωσε ότι τα παρακάτω emoji ταιριάζουν με αυτά που εμφανίζονται στην άλλη συνεδρία σου." "Σύγκριση emoji" + "Επιβεβαιώστε ότι τα παρακάτω emoji ταιριάζουν με αυτά που εμφανίζονται στη συσκευή του άλλου χρήστη." "Επιβεβαίωσε ότι οι παρακάτω αριθμοί ταιριάζουν με αυτούς που εμφανίζονται στην άλλη συνεδρία σου." "Σύγκριση αριθμών" "Η νέα σου συνεδρία έχει πλέον επαληθευτεί. Έχει πρόσβαση στα κρυπτογραφημένα μηνύματά σας και άλλοι χρήστες θα το βλέπουν ως αξιόπιστο." + "Τώρα μπορείτε να εμπιστευτείτε την ταυτότητα αυτού του χρήστη κατά την αποστολή ή τη λήψη μηνυμάτων." "Εισαγωγή κλειδιού ανάκτησης" "Είτε το αίτημα έληξε είτε απορρίφθηκε είτε υπήρξε αναντιστοιχία επαλήθευσης." "Απέδειξε ότι είσαι εσύ για να αποκτήσεις πρόσβαση στο κρυπτογραφημένο ιστορικό μηνυμάτων σου." @@ -31,14 +33,20 @@ "Συνέχισε μόνο εάν ξεκίνησες εσύ αυτήν την επαλήθευση." "Επαλήθευσε την άλλη συσκευή για να διατηρήσεις το ιστορικό μηνυμάτων σου ασφαλές." "Τώρα μπορείς να διαβάσεις ή να στείλεις μηνύματα με ασφάλεια στην άλλη συσκευή σου." - "Η συσκευή επαληθεύτηκε" + "Επαληθευμένη συσκευή" "Ζητήθηκε επαλήθευση" "Δεν ταιριάζουν" "Ταιριάζουν" "Βεβαιώσου ότι έχεις ανοιχτή την εφαρμογή στην άλλη συσκευή πριν ξεκινήσεις την επαλήθευση από εδώ." "Άνοιξε την εφαρμογή σε άλλη επαληθευμένη συσκευή" + "Για επιπλέον ασφάλεια, επαληθεύστε αυτόν το χρήστη συγκρίνοντας ένα σύνολο emoji στις συσκευές σας. Κάντε το αυτό χρησιμοποιώντας έναν αξιόπιστο τρόπο επικοινωνίας." + "Επαλήθευση αυτού του χρήστη;" + "Για επιπλέον ασφάλεια, ένας άλλος χρήστης θέλει να επαληθεύσει την ταυτότητά σας. Θα σας εμφανιστεί μια σειρά από emojis για να τα συγκρίνετε." "Πρόκειται να δεις ένα αναδυόμενο παράθυρο στην άλλη συσκευή. Ξεκίνα την επαλήθευση από εκεί τώρα." "Έναρξη επαλήθευσης στην άλλη συσκευή" + "Αναμονή για την άλλη συσκευή" + "Αναμονή για τον άλλο χρήστη" + "Μόλις γίνει αποδεκτό, θα μπορείτε να συνεχίσετε με την επαλήθευση." "Αποδέξου το αίτημα για να ξεκινήσεις τη διαδικασία επαλήθευσης στην άλλη συνεδρία σου για να συνεχίσεις." "Αναμονή για αποδοχή αιτήματος" "Αποσύνδεση…" diff --git a/features/verifysession/impl/src/main/res/values-es/translations.xml b/features/verifysession/impl/src/main/res/values-es/translations.xml index 9c43fe28e89..0f92e96395e 100644 --- a/features/verifysession/impl/src/main/res/values-es/translations.xml +++ b/features/verifysession/impl/src/main/res/values-es/translations.xml @@ -1,27 +1,52 @@ + "¿No puedes confirmar?" + "Crear una nueva clave de recuperación" "Verifica este dispositivo para configurar la mensajería segura." "Confirma que eres tú" "Usar otro dispositivo" + "Usar clave de recuperación" "Ahora puedes leer o enviar mensajes de forma segura y cualquier persona con la que chatees también puede confiar en este dispositivo." "Dispositivo verificado" "Usar otro dispositivo" + "Esperando en otro dispositivo…" "Algo no fue bien. Se agotó el tiempo de espera de la solicitud o se rechazó." "Confirma que los emojis que aparecen a continuación coinciden con los que aparecen en tu otra sesión." "Comparar emojis" + "Confirma que los emojis que aparecen a continuación coinciden con los mostrados en el dispositivo del otro usuario." "Confirma que los números que aparecen a continuación coinciden con los mostrados en tu otra sesión." "Comparar números" "Tu nueva sesión ya está verificada. Tienes acceso a tus mensajes cifrados y otros usuarios lo considerarán de confianza." - "Introduzca la clave de recuperación" + "Ahora puedes confiar en la identidad de este usuario al enviar o recibir mensajes." + "Introduce la clave de recuperación" + "O bien se agotó el tiempo de solicitud, se rechazó la solicitud o hubo una discrepancia en la verificación." "Demuestra que eres tú para acceder a tu historial de mensajes cifrados." "Abrir una sesión existente" "Reintentar la verificación" "Estoy listo" - "Esperando a que coincida" + "A la espera de que coincida…" "Compara un conjunto único de emojis." "Compara los emoji, asegurándote de que aparecen en el mismo orden." + "Sesión iniciada" + "O bien se agotó el tiempo de solicitud, se rechazó la solicitud o hubo una discrepancia en la verificación." + "Verificación fallida" + "Continúa solo si has iniciado esta verificación." + "Verifica el otro dispositivo para mantener seguro tu historial de mensajes." + "Ahora puedes leer o enviar mensajes de forma segura en tu otro dispositivo." + "Dispositivo verificado" + "Verificación solicitada" "No coinciden" "Coinciden" + "Asegúrate de tener la aplicación abierta en el otro dispositivo antes de iniciar la verificación desde aquí." + "Abre la aplicación en otro dispositivo verificado" + "Para mayor seguridad, verifica a este usuario comparando un conjunto de emojis en vuestros dispositivos. Hazlo utilizando una forma de comunicación de confianza." + "¿Verificar a este usuario?" + "Para mayor seguridad, otro usuario quiere verificar tu identidad. Se te mostrará un conjunto de emojis para compararlos." + "Deberías ver una ventana emergente en el otro dispositivo. Inicia ahora la verificación desde allí." + "Iniciar la verificación en el otro dispositivo" + "A la espera del otro dispositivo" + "A la espera del otro usuario" + "Una vez aceptada, podrás continuar con la verificación." "Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar." "A la espera de aceptar la solicitud" "Cerrando sesión…" 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 dc9367bd021..fa2a5733da4 100644 --- a/features/verifysession/impl/src/main/res/values-et/translations.xml +++ b/features/verifysession/impl/src/main/res/values-et/translations.xml @@ -13,9 +13,11 @@ "Olukord pole päris õige. Päring kas aegus või teine osapool keeldus päringule vastamast." "Kinnita, et kõik järgnevalt kuvatud emojid on täpselt samad, mida sa näed oma teises sessioonis." "Võrdle emojisid" + "Palun kinnita, et allpool näidatud emojid vastavad täpselt neile, mida kuvatakse teise kasutaja seadmes." "Kinnita, et kõik järgnevalt kuvatud numbrid on täpselt samad, mida sa näed oma teises sessioonis." "Võrdle numbreid" "Sinu uus sessioon on nüüd verifitseeritud. Sellel sessioonil on nüüd ligipääs sinu krüptitud sõnumitele ja teised osapooled näevad teda usaldusväärsena." + "Nüüd sa võid sõnumite vastuvõtmisel ja saatmisel selle kasutaja identiteeti usaldada." "Sisesta taastevõti" "Kas verifitseerimine aegus, teine osapool keeldus vastamast või tekkis vastuste mittevastavus." "Saamaks ligipääsu krüptitud sõnumite ajaloole tõesta et tegemist on sinuga." @@ -37,8 +39,14 @@ "Nad klapivad omavahel" "Enne kui alustad siin verifitseerimist, palun ava rakendus teises seadmes." "Ava rakendus teises verifitseeritud seadmes" + "Lisaturvalisuse nimel verifitseeri seee kasutaja, võrreldes oma seadmetes olevaid emojisid. Tee seda, kasutades usaldusväärset suhtlusviisi." + "Kas verifitseerime selle kasutaja?" + "Lisaturvalisuse nimel soovib teine kasutaja sinu identiteeti verifitseerida. Järgmiseks näed sa emojisid, mida peate omavahel võrdlema." "Sa peaksid teises seadmes nägema hüpikakent. Palun alusta sealt verifitseerimist." "Alusta verifitseerimist teises seadmes" + "Ootame teise seadme järgi" + "Ootame teise kasutaja järgi" + "Kui oled nõustunud, siis saad sa verifitseerimist jätkata." "Jätkamaks nõustu verifitseerimisprotsessi alustamisega oma teises sessioonis." "Ootame nõustumist verifitseerimispäringuga" "Logime välja…" diff --git a/features/verifysession/impl/src/main/res/values-eu/translations.xml b/features/verifysession/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..2497e730691 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,47 @@ + + + "Ezin duzu baieztatu?" + "Sortu berreskuratze-gako berria" + "Egiaztatu gailua mezularitza segurua konfiguratzeko." + "Berretsi zure identitatea" + "Erabili beste gailu bat" + "Erabili berreskuratze-gakoa" + "Orain mezuak modu seguruan irakurri edo bidal ditzakezu, eta txateatzen duzun edonor ere fida daiteke gailu honetaz." + "Gailua egiaztatu da" + "Erabili beste gailu bat" + "Beste gailuaren zain…" + "Egiaztatu ondorengo emojiak bat datozela beste saioan erakusten direnekin." + "Alderatu emojiak" + "Egiaztatu beheko zenbakiak zure beste saioan erakutsitakoekin bat datozela." + "Konparatu zenbakiak" + "Saio berria egiaztatu da. Zifratutako mezu guztiak atzitu ditzake, eta gainerako erabiltzaileek fidagarritzat izango dute." + "Sartu berreskuratze-gakoa" + "Frogatu zeu zarela zifratutako mezuen historia atzitzeko." + "Ireki lehendik hasita dagoen saio bat" + "Saiatu berriro egiaztatzen" + "Prest nago" + "Bat etorriko zain…" + "Alderatu emojiak eta egiaztatu ordena berean ageri direla." + "Saioa hasita" + "Egiaztapenak huts egin du" + "Egiaztapen hau zeuk hasi baduzu bakarrik jarraitu." + "Egiaztatu beste gailua zure mezuen historia seguru mantentzeko." + "Orain mezuak modu seguruan irakurri edo bidal ditzakezu beste gailuan." + "Gailua egiaztatu da" + "Egiaztapena eskatu da" + "Ez datoz bat" + "Bat datoz" + "Ziurtatu aplikazioa irekita duzula beste gailuan hemendik egiaztatzea hasi aurretik." + "Ireki aplikazioa egiaztatutako beste gailu batean" + "Segurtasun handiagorako, egiaztatu erabiltzailea emoji multzo bat alderatuz. Egin hau komunikatzeko modu fidagarri bat erabiliz." + "Erabiltzailea egiaztatu?" + "Segurtasun handiagorako, beste erabiltzaile batek zure identitatea egiaztatu nahi du. Emoji sorta bat erakutsiko zaizu konparatzeko." + "Beste gailuan laster-menu bat ikusi beharko zenuke. Hasi egiaztapena hortik orain." + "Hasi egiaztapena beste gailuan" + "Beste gailuaren zain" + "Beste erabiltzailearen zain" + "Onartutakoan egiaztapenarekin jarraitu ahal izango duzu." + "Jarraitzeko, onartu zure beste saioan egiaztapen-prozesua hasteko eskaera." + "Eskaera onartzeko zain" + "Saioa amaitzen…" + 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 3899f7acff9..949f4ab27c1 100644 --- a/features/verifysession/impl/src/main/res/values-fa/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fa/translations.xml @@ -10,18 +10,24 @@ "افزاره تأیید شده" "استفاده از افزاره‌ای دیگر" "منتظر افزارهٔ دیگر…" + "يه چيزي درست به نظر نمياد یا زمان درخواست به پایان رسید یا درخواست رد شد." + "تأیید کنید که ایموجی های زیر با ایموجی های نشان داده شده در جلسه دیگر شما مطابقت دارند." "مقایسهٔ شکلک‌ها" "مقایسهٔ اعداد" "نشست جدید شما اکنون تأیید شده‌است. این نشست به پیام‌های رمزگذاری شده‌ی شما دسترسی داشته و سایر کاربران نیز این نشست را قابل اعتماد می دانند." "ورود کلید بازیابی" + "برای دسترسی به تاریخچه پیام‌های رمزگذاری‌شده‌تان، ثابت کنید که خودتان هستید." "گشودن نشستی موجود" "تلاش برای تأیید دوباره" "آماده‌ام" - "منتظر تطابق" + "منتظر تطابق…" "مقایسهٔ مجموعه‌ای یکتا از شکلک‌ها." "شکلک‌ها را مقایسه کنید، از ترتیب نمایش آنان نیز مطمئن شوید." + "صحت‌سنجی شکست خورد" + "افزاره تأیید شده" "مطابق نیستند" "مطابقند" + "برای ادامه، درخواست شروع فرآیند تأیید را در جلسه دیگر خود بپذیرید." "منظر پذیرش درخواست" "خارج شدن…" diff --git a/features/verifysession/impl/src/main/res/values-fi/translations.xml b/features/verifysession/impl/src/main/res/values-fi/translations.xml index c011022eb70..49e09aab99f 100644 --- a/features/verifysession/impl/src/main/res/values-fi/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fi/translations.xml @@ -11,11 +11,13 @@ "Käytä toista laitetta" "Odotetaan toista laitetta…" "Jokin ei vaikuta oikealta. Joko pyyntö aikakatkaistiin tai hylättiin." - "Vahvista, että alla olevat hymiöt vastaavat toisella laitteella näkyviä hymiöitä." - "Vertaa hymiöitä" + "Vahvista, että alla olevat emojit vastaavat toisella laitteella näkyviä emojeja." + "Vertaa emojeja" + "Vahvista, että alla olevat hymiöt vastaavat toisen käyttäjän laitteessa näkyviä hymiöitä." "Varmista, että alla olevat numerot vastaavat toisessa istunnossa näkyviä numeroita." "Vertaa numeroita" "Uusi kirjautumisesi on nyt vahvistettu. Sillä on pääsy salattuihin viesteihisi, ja muut käyttäjät näkevät sen luotettuna." + "Nyt voit luottaa tämän käyttäjän identiteettiin, kun lähetät tai vastaanotat viestejä." "Syötä palautusavain" "Joko pyyntö aikakatkaistiin, pyyntö hylättiin tai vahvistus ei täsmännyt." "Vahvista, että se olet sinä, jotta näet aiemmat salatut viestisi." @@ -24,7 +26,7 @@ "Olen valmis" "Odotetaan vahvistusta…" "Vertaa emojisarjaa." - "Vertaa hymiöitä, varmistaen että ne ovat samassa järjestyksessä." + "Vertaa emojeja, varmistaen että ne ovat samassa järjestyksessä." "Kirjautui sisään" "Joko pyyntö aikakatkaistiin, pyyntö hylättiin tai vahvistus ei täsmännyt." "Vahvistus epäonnistui" @@ -37,8 +39,14 @@ "Ne täsmäävät" "Varmista, että sovellus on avoinna toisessa laitteessa, ennen kuin aloitat vahvistuksen tästä." "Avaa sovellus toisella vahvistetulla laitteella" + "Vahvista tämä käyttäjä turvallisuuden lisäämiseksi vertaamalla emojeja laitteillanne. Tee tämä käyttämällä luotettavaa viestintätapaa." + "Vahvistetaanko tämä käyttäjä?" + "Toinen käyttäjä haluaa vahvistaa identiteettisi turvallisuuden lisäämiseksi. Sinulle näytetään joukko emojeja vertailtavaksi." "Sinun pitäisi nähdä ponnahdusikkuna toisessa laitteessa. Aloita vahvistus nyt sieltä." "Aloita vahvistus toisella laitteella" + "Odotetaan toista laitetta" + "Odotetaan toista käyttäjää" + "Kun pyyntö on hyväksytty, voit jatkaa vahvistusta." "Hyväksy vahvistuspyyntö toisella laitteella jatkaaksesi." "Odotetaan pyynnön hyväksymistä" "Kirjaudutaan ulos…" diff --git a/features/verifysession/impl/src/main/res/values-fr/translations.xml b/features/verifysession/impl/src/main/res/values-fr/translations.xml index bd3f194e4b8..a675c879f2d 100644 --- a/features/verifysession/impl/src/main/res/values-fr/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml @@ -11,34 +11,42 @@ "Utiliser une autre session" "En attente d’une autre session…" "Quelque chose ne va pas. Soit la demande a expiré, soit elle a été refusée." - "Confirmez que les emojis ci-dessous correspondent à ceux affichés sur votre autre session." + "Confirmez que les émojis ci-dessous correspondent à ceux affichés sur votre autre session." "Comparez les émojis" + "Vérifiez que les émojis ci-dessous correspondent à ceux affichés sur l’appareil de l’autre utilisateur." "Confirmez que les nombres ci-dessous correspondent à ceux affichés sur votre autre session." "Comparez les nombres" "Votre nouvelle session est désormais vérifiée. Elle a accès à vos messages chiffrés et les autres utilisateurs la verront identifiée comme fiable." + "Vous pouvez désormais avoir confiance en l’identité de cet utilisateur lorsque vous lui envoyez des messages ou que vous recevez des messages de sa part." "Utiliser la clé de récupération" - "Soit la demande a expiré, soit elle a été refusée, soit il y a eu une non-concordance de vérification." + "Soit la demande a expiré, soit elle a été refusée, soit les éléments à comparer ne correspondaient pas." "Prouvez qu’il s’agit bien de vous pour accéder à l’historique de vos messages chiffrés." "Ouvrir une session existante" "Réessayer la vérification" "Je suis prêt.e" "En attente de correspondance…" - "Comparer un groupe unique d’Emojis." - "Comparez les emoji uniques en veillant à ce qu’ils apparaissent dans le même ordre." + "Comparer un groupe unique d’émojis." + "Comparez les émojis uniques en veillant à ce qu’ils apparaissent dans le même ordre." "Connecté" - "Soit la demande a expiré, soit elle a été refusée, soit il y a eu une non-concordance de vérification." + "Soit la demande a expiré, soit elle a été refusée, soit les éléments à comparer ne correspondaient pas." "Échec de la vérification" "Continuez uniquement si c’est vous qui avez commencé cette vérification." "Vérifiez l’autre appareil pour sécuriser l’historique de vos messages." "Vous pouvez désormais lire ou envoyer des messages en toute sécurité sur votre autre appareil." - "Appareil vérifié" + "Session vérifiée" "Vérification demandée" "Ils ne correspondent pas" "Ils correspondent" "Assurez-vous que l’application est ouverte sur un autre appareil avant de commencer la vérification." "Ouvrez l’application sur un autre appareil vérifié" + "Pour plus de sécurité, vérifiez cet utilisateur en comparant des émojis sur vos appareils. Pour ce faire, utilisez un moyen de communication fiable." + "Vérifier cet utilisateur ?" + "Pour plus de sécurité, cet autre utilisateur souhaite vérifier votre identité. Des émojis à comparer vous seront présentés." "Vous devriez voir une alerte sur l’autre appareil. Démarrez la vérification à partir de là dès maintenant." "Démarrer la vérification sur l’autre appareil" + "En attente de l’autre appareil" + "En attente de l’autre utilisateur" + "Une fois acceptée, vous pourrez poursuivre la vérification." "Pour continuer, acceptez la demande de lancement de la procédure de vérification dans votre autre session." "En attente d’acceptation de la demande" "Déconnexion…" diff --git a/features/verifysession/impl/src/main/res/values-hu/translations.xml b/features/verifysession/impl/src/main/res/values-hu/translations.xml index 7e786dfc6c6..56abe18d674 100644 --- a/features/verifysession/impl/src/main/res/values-hu/translations.xml +++ b/features/verifysession/impl/src/main/res/values-hu/translations.xml @@ -13,16 +13,18 @@ "Valami hibásnak tűnik. A kérés vagy időtúllépésre futott, vagy elutasították." "Erősítse meg, hogy a lenti emodzsik egyeznek-e a másik munkamenetben megjelenítettekkel." "Emodzsik összehasonlítása" + "Ellenőrizze, hogy az alábbi emodzsik megegyeznek-e a másik felhasználó eszközén látható emodzsikkal." "Ellenőrizze, hogy az alábbi számok megegyeznek-e a másik munkamenetben feltüntetett számokkal." "Számok összehasonlítása" "Az új munkamenete most már ellenőrizve van. Eléri a titkosított üzeneteit, és a többi felhasználó is megbízhatónak fogja látni." + "Mostantól megbízhat a felhasználó személyazonosságában, amikor üzeneteket küld vagy fogad." "Adja meg a helyreállítási kulcsot" "A kérés túllépte az időkorlátot, el lett utasítva, vagy ellenőrzési eltérés történt." "Bizonyítsa, hogy valóban Ön az, hogy elérje a titkosított üzeneteinek előzményeit." "Meglévő munkamenet megnyitása" "Ellenőrzés újrapróbálása" "Kész vagyok" - "Várakozás az egyezésre" + "Várakozás az egyezésre…" "Egyedi emodzsik összehasonlítása." "Hasonlítsa össze az egyedi emodzsikat, meggyőződve arról, hogy azonos a sorrendjük." "Bejelentkezve" @@ -37,8 +39,14 @@ "Megegyeznek" "Győződjön meg róla, hogy az alkalmazás nyitva van a másik eszközön, mielőtt innen elindítja az ellenőrzést." "Nyissa meg az alkalmazást egy másik ellenőrzött eszközön" + "A fokozott biztonság érdekében ellenőrizze ezt a felhasználót az eszközökön megjelenő emodzsik összehasonlításával. Ehhez használjon megbízható kommunikációs csatornát." + "Ellenőrzi ezt a felhasználót?" + "A további biztonság érdekében egy másik felhasználó ellenőrizni szeretné személyazonosságát. Meg fog jelenni egy sor emodzsi, melyeket össze kell majd hasonlítania." "A másik eszközön egy felugró ablaknak kell megjelennie. Kezdje el az ellenőrzést onnan." "Ellenőrzés megkezdése a másik eszközön" + "Várakozás a másik eszközre" + "Várakozás a másik felhasználóra" + "Az elfogadása után folytathatja az ellenőrzést." "A folytatáshoz fogadja el az ellenőrzési folyamat indítási kérését a másik munkamenetében." "Várakozás a kérés elfogadására" "Kijelentkezés…" 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 c19ebe602c9..655520e51fb 100644 --- a/features/verifysession/impl/src/main/res/values-in/translations.xml +++ b/features/verifysession/impl/src/main/res/values-in/translations.xml @@ -13,9 +13,11 @@ "Sepertinya ada yang tidak beres. Entah permintaan sudah habis masa berlakunya atau permintaan ditolak." "Konfirmasikan bahwa emoji di bawah ini sesuai dengan yang ditampilkan pada sesi Anda yang lain." "Bandingkan emoji" + "Konfirmasikan bahwa emoji di bawah ini cocok dengan yang ditampilkan di perangkat pengguna lain." "Konfirmasikan bahwa angka-angka di bawah ini sesuai dengan yang ditampilkan pada sesi Anda yang lain." "Bandingkan angka" "Sesi baru Anda sekarang diverifikasi. Ini memiliki akses ke pesan terenkripsi Anda, dan pengguna lain akan melihatnya sebagai tepercaya." + "Sekarang Anda dapat mempercayai identitas pengguna ini saat mengirim atau menerima pesan." "Masukkan kunci pemulihan" "Entah permintaan habis waktu, permintaan ditolak, atau ada ketidakcocokan verifikasi." "Buktikan bahwa ini memang Anda untuk mengakses riwayat pesan terenkripsi Anda." @@ -31,14 +33,20 @@ "Lanjutkan hanya jika Anda memulai verifikasi ini." "Verifikasi perangkat lain untuk menjaga riwayat pesan Anda tetap aman." "Sekarang Anda dapat membaca atau mengirim pesan dengan aman di perangkat Anda yang lain." - "Perangkat diverifikasi" + "Perangkat terverifikasi" "Verifikasi diminta" "Mereka tidak cocok" "Mereka cocok" "Pastikan Anda membuka aplikasi di perangkat lain sebelum memulai verifikasi dari sini." "Buka aplikasi di perangkat terverifikasi lain" + "Untuk keamanan tambahan, verifikasi pengguna ini dengan membandingkan satu set emoji di perangkat Anda. Lakukan ini dengan menggunakan cara tepercaya untuk berkomunikasi." + "Verifikasi pengguna ini?" + "Untuk keamanan tambahan, pengguna lain ingin memverifikasi identitas Anda. Anda akan ditampilkan satu set emoji untuk dibandingkan." "Anda akan melihat popup di perangkat lain. Mulai verifikasi dari sana sekarang." "Mulai verifikasi di perangkat lain" + "Menunggu perangkat lain" + "Menunggu pengguna lain" + "Setelah diterima, Anda akan dapat melanjutkan verifikasi." "Terima permintaan untuk memulai proses verifikasi di sesi Anda yang lain untuk melanjutkan." "Menunggu untuk menerima permintaan" "Mengeluarkan dari akun…" diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml index 322ab0e6d32..bb55310d318 100644 --- a/features/verifysession/impl/src/main/res/values-it/translations.xml +++ b/features/verifysession/impl/src/main/res/values-it/translations.xml @@ -13,9 +13,11 @@ "C\'è qualcosa che non va. La richiesta è scaduta o è stata rifiutata." "Verifica che gli emoji sottostanti corrispondano a quelli mostrati nell\'altra sessione." "Confronta le emoji" + "Conferma che le emoji qui sotto corrispondano a quelle visualizzate sul dispositivo dell\'altro utente." "Conferma che i numeri seguenti corrispondano a quelli mostrati nell\'altra sessione." "Confronta i numeri" "La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile." + "Ora puoi fidarti dell\'identità di questo utente quando invii o ricevi messaggi." "Inserisci la chiave di recupero" "La richiesta è scaduta, è stata rifiutata o c\'è stata una mancata corrispondenza nella verifica." "Dimostra la tua identità per accedere alla cronologia dei messaggi crittografati." @@ -37,8 +39,14 @@ "Corrispondono" "Assicurati di avere l\'app aperta sull\'altro dispositivo prima di iniziare la verifica da qui." "Apri l\'app su un altro dispositivo verificato" + "Per una maggiore sicurezza, verifica questo utente confrontando un set di emoji sui tuoi dispositivi. A tale scopo, utilizza un metodo di comunicazione affidabile." + "Verificare questo utente?" + "Per una maggiore sicurezza, un altro utente desidera verificare la tua identità. Ti verrà mostrato un set di emoji da confrontare." "Dovresti vedere un popup sull\'altro dispositivo. Inizia subito la verifica da lì." "Avvia la verifica sull\'altro dispositivo" + "In attesa dell\'altro dispositivo" + "In attesa dell\'altro utente" + "Una volta accettata, potrai proseguire con la verifica." "Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare." "In attesa di accettare la richiesta" "Disconnessione in corso…" diff --git a/features/verifysession/impl/src/main/res/values-ka/translations.xml b/features/verifysession/impl/src/main/res/values-ka/translations.xml index 89f1a068ea3..5815ecefaad 100644 --- a/features/verifysession/impl/src/main/res/values-ka/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ka/translations.xml @@ -1,11 +1,18 @@ + "ახალი აღდგენის გასაღების შექმნა" + "დაადასტურეთ ეს მოწყობილობა უსაფრთხო მიმოწერისათვის." + "დაამტკიცეთ თქვენი პიროვნება" + "ახლა თქვენ შეძლებთ შეტყობინებების წაკითხვას ან გაგზავნას უსაფრთხოდ, სხვა მომხმარებლებსაც შეუძლიათ ამ მოწყობილობას ენდონ." + "მოწყობილობა დადასტურებულია" + "ველოდებით სხვა მოწყობილობას…" "რაღაცა არასწორადაა. ან მოთხოვნის ვადაა ამოწურული, ან მოთხოვნა უარყოფილი იყო." "დაადასტურეთ, რომ ქვემოთ მოყვანილი ემოჯიები შეესაბამება თქვენს სხვა სესიაზე ნაჩვენებს." "შეადარეთ ემოჯიები" "დაადასტურეთ, რომ ქვემოთ მოცემული ნომრები ემთხვევა თქვენს სხვა სესიაზე ნაჩვენები ნომრებს." "შეადარეთ რიცხვები" "თქვენი ახალი სესია დადასტურებულია. მას აქვს წვდომა დაშიფრულ შეტყობინებებზე და სხვა მომხმარებლები მას სანდოდ ხედავენ." + "შეიყვანეთ აღდგენის გასაღები" "დაამტკიცეთ, რომ ეს თქვენ ხართ, რათა მიიღოთ წვდომა თქვენი დაშიფრული შეტყობინებების ისტორიასთან." "არსებული სესიის გახსნა" "დადასტურების ხელახლა ცდა" @@ -13,6 +20,7 @@ "ველოდებით დამთხვევას" "შეადარეთ ემოციების უნიკალური ნაკრები." "შეადარეთ უნიკალური ემოჯი, დარწმუნდით, რომ ისინი ერთი დ იმავე თანმიმდევრობით გამოჩნდნენ." + "მოწყობილობა დადასტურებულია" "ისინი არ ემთხვევიან ერთმანეთს" "ისინი ემთხვევიან ერთმანეთს" "მიიღეთ დადასტურების მოთხოვნა თქვენს სხვა სესიაში ამ პროცესის გასაგრძელებლად." diff --git a/features/verifysession/impl/src/main/res/values-lt/translations.xml b/features/verifysession/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..0fd7a2e7557 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,18 @@ + + + "Kažkas atrodo ne taip. Baigėsi užklausos skirtasis laikas arba užklausa buvo atmesta." + "Patvirtinkite, kad žemiau esantys jaustukai atitinka tuos, kurie rodomi kitoje sesijoje." + "Palyginkite jaustukus" + "Jūsų nauja sesija dabar patvirtinta. Ji turi prieigą prie jūsų užšifruotų pranešimų, o kiti vartotojai matys ją kaip patikimą." + "Įrodykite, kad tai Jūs, norėdami pasiekti savo užšifruotų pranešimų istoriją." + "Atidaryti esamą sesiją" + "Pakartoti patvirtinimą" + "Aš pasiruošęs" + "Laukiama atitikimo…" + "Palyginkite unikalius jaustukus, įsitikindami, kad jie rodomi ta pačia tvarka." + "Jie nesutampa" + "Jie sutampa" + "Kitoje sesijoje priimkite prašymą pradėti tikrinimo procesą, kad galėtumėte tęsti." + "Laukiama prašymo priėmimo" + "Atsijungiama…" + diff --git a/features/verifysession/impl/src/main/res/values-nb/translations.xml b/features/verifysession/impl/src/main/res/values-nb/translations.xml index da4264871ef..afb8298a20d 100644 --- a/features/verifysession/impl/src/main/res/values-nb/translations.xml +++ b/features/verifysession/impl/src/main/res/values-nb/translations.xml @@ -13,9 +13,11 @@ "Noe virker ikke riktig. Enten ble forespørselen tidsavbrutt eller forespørselen ble avslått." "Bekreft at emojiene nedenfor samsvarer med de som vises på den andre sesjonen din." "Sammenlign emojier" + "Bekreft at emojiene nedenfor stemmer overens med de som vises på den andre brukerens enhet." "Kontroller at tallene nedenfor stemmer overens med dem som vises på den andre sesjonen." "Sammenlign tallene" "Den nye sesjonen din er nå bekreftet. Den har tilgang til de krypterte meldingene dine, og andre brukere vil se den som klarert." + "Nå kan du stole på identiteten til denne brukeren når du sender eller mottar meldinger." "Skriv inn gjenopprettingsnøkkel" "Enten ble forespørselen tidsavbrutt, forespørselen ble avslått eller det var en feil i verifiseringen." "Bevis at det er deg for å få tilgang til den krypterte meldingshistorikken din." @@ -35,6 +37,16 @@ "Verifisering forespurt" "De matcher ikke" "De matcher" + "Sørg for at du har appen åpen på den andre enheten før du starter verifiseringen herfra." + "Åpne appen på en annen bekreftet enhet" + "For ekstra sikkerhet, verifiser denne brukeren ved å sammenligne et sett med emojier på enhetene dine. Gjør dette ved å bruke en pålitelig måte å kommunisere på." + "Vil du verifisere denne brukeren?" + "For ekstra sikkerhet vil en annen bruker bekrefte identiteten din. Du får se et sett med emojier som du må sammenligne." + "Du skal se en popup på den andre enheten. Start bekreftelsen derfra nå." + "Start verifiseringen på den andre enheten" + "Venter på den andre enheten" + "Venter på den andre brukeren" + "Når du er akseptert, kan du fortsette med verifiseringen." "Godta forespørselen om å starte bekreftelsesprosessen i den andre sesjonen for å fortsette." "Venter på å godta forespørselen" "Logger ut…" diff --git a/features/verifysession/impl/src/main/res/values-nl/translations.xml b/features/verifysession/impl/src/main/res/values-nl/translations.xml index 20e588e2dbf..54797b12445 100644 --- a/features/verifysession/impl/src/main/res/values-nl/translations.xml +++ b/features/verifysession/impl/src/main/res/values-nl/translations.xml @@ -24,6 +24,11 @@ "Wachten om te vergelijken" "Vergelijk een unieke combinatie van emoji\'s." "Vergelijk de unieke emoji\'s, ze dienen in dezelfde volgorde te worden weergegeven." + "Ingelogd" + "Ga alleen verder als je deze verificatie hebt gestart." + "Verifieer het andere apparaat om je berichtengeschiedenis veilig te houden." + "Apparaat geverifieerd" + "Verificatieverzocht" "Ze komen niet overeen" "Ze komen overeen" "Accepteer het verzoek tot verificatie in je andere sessie om door te gaan." 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 022a07d6a92..d5cd461b8df 100644 --- a/features/verifysession/impl/src/main/res/values-pl/translations.xml +++ b/features/verifysession/impl/src/main/res/values-pl/translations.xml @@ -13,9 +13,11 @@ "Coś tu nie gra. Albo upłynął limit czasu, albo żądanie zostało odrzucone." "Upewnij się, że emoji poniżej pasują do tych pokazanych na innej sesji." "Porównaj emoji" + "Upewnij się, że emoji poniżej odpowiadają tym na urządzeniu drugiego użytkownika." "Upewnij się, że liczby poniżej pasują do tych wyświetlanych na innej sesji." "Porównaj liczby" "Twoja nowa sesja jest teraz zweryfikowana. Ma ona dostęp do Twoich zaszyfrowanych wiadomości, a inni użytkownicy będą widzieć ją jako zaufaną." + "Teraz możesz zaufać tożsamości tego użytkownika podczas wysyłania lub odbierania wiadomości." "Wprowadź klucz przywracania" "Albo upłynął limit czasu żądania, albo żądanie zostało odrzucone, albo wystąpił błąd weryfikacji." "Udowodnij, że to ty, aby uzyskać dostęp do historii zaszyfrowanych wiadomości." @@ -37,8 +39,14 @@ "Pasują do siebie" "Upewnij się, że aplikacja jest otwarta na drugim urządzeniu przed rozpoczęciem weryfikacji." "Otwórz aplikację na drugim zweryfikowanym urządzeniu" + "Dla dodatkowej ochrony, zweryfikuj tego użytkownika, porównując zestaw emoji na obu urządzeniach. Zrób to, korzystając z zaufanej metody komunikacji." + "Zweryfikować tego użytkownika?" + "Dla dodatkowej ochrony, inny użytkownik chce zweryfikować Twoją tożsamość. Pojawi się unikalny zestaw emoji do porównania." "Powinno wyskoczyć okno na drugim urządzeniu. Rozpocznij tam weryfikację." "Rozpocznij weryfikację na drugim urządzeniu" + "Oczekiwanie na drugie urządzenie" + "Oczekiwanie na drugiego użytkownika" + "Po zaakceptowaniu będziesz mógł kontynuować weryfikację." "Zaakceptuj prośbę o rozpoczęcie procesu weryfikacji w innej sesji, aby kontynuować." "Oczekiwanie na zaakceptowanie prośby" "Wylogowywanie…" diff --git a/features/verifysession/impl/src/main/res/values-pt-rBR/translations.xml b/features/verifysession/impl/src/main/res/values-pt-rBR/translations.xml index 9874cfa2b30..5ffb00abece 100644 --- a/features/verifysession/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/verifysession/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,15 +1,25 @@ + "Não consegue confirmar?" + "Criar uma nova chave de recuperação" + "Verifique este dispositivo para configurar mensagens seguras." + "Confirme sua identidade" "Usar outro dispositivo" + "Use a chave de recuperação" + "Agora você pode ler ou enviar mensagens com segurança, e qualquer pessoa com quem você conversa também pode confiar neste dispositivo." "Dispositivo verificado" "Usar outro dispositivo" + "Aguardando outro dispositivo…" "Algo não parece certo. Ou a solicitação atingiu o tempo limite ou a solicitação foi negada." "Confirme se os emojis abaixo correspondem aos mostrados em sua outra sessão." "Compare os emojis" + "Confirme se os emojis abaixo correspondem aos exibidos no dispositivo do outro usuário." "Confirme se os números abaixo correspondem aos mostrados em sua outra sessão." "Comparar números" "Sua nova sessão está agora verificada. Ela tem acesso às suas mensagens criptografadas e outros usuários a verão como confiável." + "Agora você pode confiar na identidade desse usuário ao enviar ou receber mensagens." "Insira a chave de recuperação" + "Ou a solicitação expirou, a solicitação foi negada ou houve uma incompatibilidade de verificação." "Prove que é você para acessar seu histórico de mensagens criptografadas." "Abrir uma sessão existente" "Repetir verificação" @@ -17,8 +27,26 @@ "Esperando para combinar" "Compare um conjunto único de emojis." "Compare os emojis únicos, garantindo que apareçam na mesma ordem." + "Sessão iniciada" + "Ou a solicitação expirou, a solicitação foi negada ou houve uma incompatibilidade de verificação." + "A verificação falhou" + "Continue somente se você iniciou esta verificação." + "Verifique o outro dispositivo para manter seu histórico de mensagens seguro." + "Agora você pode ler ou enviar mensagens com segurança em seu outro dispositivo." + "Dispositivo verificado" + "Verificação solicitada" "Eles não combinam" "Eles combinam" + "Certifique-se de que você tenha o aplicativo aberto no outro dispositivo antes de iniciar a verificação a partir daqui." + "Abra o aplicativo em outro dispositivo verificado" + "Para maior segurança, verifique esse usuário comparando um conjunto de emojis em seus dispositivos. Faça isso usando uma maneira confiável de se comunicar." + "Verificar este usuário?" + "Para maior segurança, outro usuário deseja verificar sua identidade. Você verá um conjunto de emojis para comparar." + "Você deverá ver um pop-up no outro dispositivo. Você deverá ver uma janela pop-up no outro dispositivo e iniciar a verificação a partir daí." + "Inicie a verificação no outro dispositivo" + "Aguardando o outro dispositivo" + "Aguardando o outro usuário" + "Depois de aceito, você poderá continuar com a verificação." "Aceite a solicitação para iniciar o processo de verificação em sua outra sessão para continuar." "Aguardando para aceitar a solicitação" "Saindo…" 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 967c6df1c99..13860912547 100644 --- a/features/verifysession/impl/src/main/res/values-pt/translations.xml +++ b/features/verifysession/impl/src/main/res/values-pt/translations.xml @@ -13,9 +13,11 @@ "Algo não bateu certo. O pedido ou demorou demasiado tempo ou foi rejeitado." "Confirma se os emojis abaixo correspondem aos apresentados na tua outra sessão." "Compara os emojis" + "Confirma se os emojis abaixo correspondem aos apresentados no dispositivo do outro utilizador." "Confirma se os números abaixo correspondem aos números apresentados na tua outra sessão." "Comparar números" "A tua nova sessão está agora verificada, pelo que tem acesso às tuas mensagens cifradas e os outros utilizadores vão vê-la como de confiança." + "Agora podes confiar na identidade deste utilizador quando envias ou recebes mensagens." "Insere a chave de recuperação" "O pedido expirou, o pedido foi recusado ou houve um erro de verificação." "Prova que és tu para acederes ao teu histórico de mensagens cifradas." @@ -37,8 +39,14 @@ "Correspondem" "Certifica-te de que tens a aplicação aberta no outro dispositivo antes de iniciares a verificação aqui." "Abre a aplicação noutro dispositivo verificado" + "Para maior segurança, verifica este utilizador comparando um conjunto de emojis nos teus dispositivos. Faz isto utilizando uma forma de comunicação de confiança." + "Verificar este utilizador?" + "Para maior segurança, outro utilizador quer verificar a tua identidade. Ser-te-á mostrado um conjunto de emojis para comparares." "Deves ver uma notificação no outro dispositivo. Inicia a verificação a partir daí." "Inicia a verificação no outro dispositivo" + "À espera do outro dispositivo" + "A espera do outro utilizador" + "Uma vez aceite, poderás continuar com a verificação." "Para continuar, aceita o pedido de verificação na tua outra sessão." "À aguardar a aceitação do pedido" "A terminar sessão…" diff --git a/features/verifysession/impl/src/main/res/values-ru/translations.xml b/features/verifysession/impl/src/main/res/values-ru/translations.xml index 61895a94a3a..ace23548374 100644 --- a/features/verifysession/impl/src/main/res/values-ru/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ru/translations.xml @@ -13,9 +13,11 @@ "Похоже, что-то не так. Время ожидания запроса либо истекло, либо запрос был отклонен." "Убедитесь, что приведенные ниже емоджи совпадают с емоджи показанными во время другого сеанса." "Сравните емодзи" + "Убедитесь, что указанные ниже эмодзи соответствуют тем, которые отображаются на устройстве другого пользователя." "Убедитесь, что приведенные ниже числа совпадают с цифрами, показанными в другом сеансе." "Сравните числа" "Ваш новый сеанс подтвержден. У него есть доступ к вашим зашифрованным сообщениям, и другие пользователи увидят его как доверенное." + "Теперь вы можете доверять этому пользователя при отправке или получении сообщений." "Введите ключ восстановления" "Время ожидания подтверждения истекло, запрос был отклонён, или при подтверждении произошло несоответствие." "Чтобы получить доступ к зашифрованной истории сообщений, докажите, что это вы." @@ -37,8 +39,14 @@ "Они совпадают" "Прежде чем начать проверку, убедитесь, что приложение открыто на другом устройстве." "Откройте приложение на другом проверенном устройстве" + "Для дополнительной безопасности проверьте этого пользователя, сравнив набор эмодзи на ваших устройствах. Сделайте это, используя надежный способ коммуникации." + "Проверить этого пользователя?" + "Для дополнительной безопасности другой пользователь хочет проверить вашу личность. Вам будет показан набор эмодзи для сравнения." "Вы должны увидеть всплывающее окно на другом устройстве. Начните проверку оттуда прямо сейчас." "Начать проверку на другом устройстве" + "Ожидание другого устройства" + "Ожидание другого пользователя" + "После одобрения вы сможете продолжить проверку." "Чтобы продолжить, примите запрос на запуск процесса подтверждения в другом сеансе." "Ожидание принятия запроса" "Выполняется выход…" 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 9612618b586..cc5f26e85bb 100644 --- a/features/verifysession/impl/src/main/res/values-sk/translations.xml +++ b/features/verifysession/impl/src/main/res/values-sk/translations.xml @@ -13,9 +13,11 @@ "Zdá sa, že niečo nie je v poriadku. Časový limit žiadosti vypršal alebo bola žiadosť zamietnutá." "Skontrolujte, či sa emotikony uvedené nižšie zhodujú s emotikonmi zobrazenými vo vašej druhej relácii." "Porovnajte emotikony" + "Potvrďte, že emotikony uvedené nižšie zodpovedajú emotikonom zobrazeným na zariadení druhého používateľa." "Skontrolujte, či sa nižšie uvedené čísla zhodujú s číslami zobrazenými na vašej druhej relácii." "Porovnať čísla" "Vaša nová relácia je teraz overená. Má prístup k vašim zašifrovaným správam a ostatní používatelia ju budú vidieť ako dôveryhodnú." + "Teraz môžete dôverovať identite tohto používateľa pri odosielaní alebo prijímaní správ." "Zadajte kľúč na obnovenie" "Buď žiadosť vypršala, žiadosť bola zamietnutá, alebo došlo k nesúladu overovania." "Dokážte, že ste to vy, aby ste získali prístup k histórii vašich zašifrovaných správ." @@ -37,8 +39,14 @@ "Zhodujú sa" "Pred začatím overovania odtiaľto sa uistite, že máte aplikáciu otvorenú v inom zariadení." "Otvorte aplikáciu na inom overenom zariadení" + "Pre väčšiu bezpečnosť overte tohto používateľa porovnaním sady emotikonov vo vašich zariadeniach. Urobte to pomocou dôveryhodného spôsobu komunikácie." + "Overiť tohto používateľa?" + "Kvôli vyššej bezpečnosti chce druhý používateľ overiť vašu identitu. Zobrazí sa vám sada emotikonov na porovnanie." "Na druhom zariadení by sa malo zobraziť vyskakovacie okno. Začnite teraz overovanie odtiaľ." "Spustiť overovanie na druhom zariadení" + "Čaká sa na druhé zariadenie" + "Čaká sa na druhého používateľa" + "Po prijatí budete môcť pokračovať v overovaní." "Ak chcete pokračovať, prijmite žiadosť o spustenie procesu overenia vo vašej druhej relácii." "Čaká sa na prijatie žiadosti" "Prebieha odhlasovanie…" diff --git a/features/verifysession/impl/src/main/res/values-sv/translations.xml b/features/verifysession/impl/src/main/res/values-sv/translations.xml index f790ec399d6..0fd66a82d39 100644 --- a/features/verifysession/impl/src/main/res/values-sv/translations.xml +++ b/features/verifysession/impl/src/main/res/values-sv/translations.xml @@ -13,10 +13,13 @@ "Något verkar inte stämma. Antingen gick tidsgränsen för begäran ut eller så avvisades begäran." "Bekräfta att emojierna nedan matchar de som visas på din andra session." "Jämför emojis" + "Bekräfta att emojierna nedan matchar de som visas på den andra användarens enhet." "Bekräfta att siffrorna nedan matchar de som visas på din andra session." "Jämför siffror" "Din nya session är nu verifierad. Den har tillgång till dina krypterade meddelanden, och andra användare kommer att se den som betrodd." + "Nu kan du lita på användarens identitet när du skickar eller tar emot meddelanden." "Ange återställningsnyckel" + "Antingen överskreds tidsgränsen för begäran, begäran nekades eller så fanns det ett matchningsfel för verifieringen." "Bevisa att det är du för att komma åt din krypterade meddelandehistorik." "Öppna en befintlig session" "Försök att verifiera igen" @@ -25,11 +28,25 @@ "Jämför en unik uppsättning emojis." "Jämför de unika emojierna och se till att de visas i samma ordning." "Inloggad" + "Antingen överskreds tidsgränsen för begäran, begäran nekades eller så fanns det ett matchningsfel för verifieringen." + "Verifiering misslyckades" "Fortsätt bara om du initierade denna verifiering." "Verifiera den andra enheten för att hålla din meddelandehistorik säker." + "Du kan nu läsa eller skicka meddelanden säkert på din andra enhet." + "Enhet verifierad" "Verifiering begärd" "De matchar inte" "De matchar" + "Se till att du har appen öppen på den andra enheten innan du startar verifieringen härifrån." + "Öppna appen på en annan verifierad enhet" + "För extra säkerhet, verifiera den här användaren genom att jämföra en uppsättning emojier på dina enheter. Gör detta med hjälp av ett betrott kommunikationssätt." + "Verifiera den här användaren?" + "För extra säkerhet vill en annan användare verifiera din identitet. Du kommer att visas en uppsättning emojier att jämföra." + "Du bör se en popup på den andra enheten. Starta verifieringen därifrån nu." + "Starta verifieringen på den andra enheten" + "Väntar på den andra enheten" + "Väntar på den andra användaren" + "När det har accepterats kommer du kunna fortsätta verifieringen." "Godkänn begäran om att starta verifieringsprocessen på din andra session för att fortsätta." "Väntar på att acceptera begäran" "Loggar ut …" diff --git a/features/verifysession/impl/src/main/res/values-tr/translations.xml b/features/verifysession/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..a045731bd4f --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,53 @@ + + + "Onaylayamıyor musunuz?" + "Yeni bir kurtarma anahtarı oluştur" + "Güvenli mesajlaşmayı ayarlamak için bu cihazı doğrulayın." + "Kimliğinizi doğrulayın" + "Başka bir cihaz kullan" + "Kurtarma anahtarı kullan" + "Artık mesajları güvenli bir şekilde okuyabilir veya gönderebilirsiniz ve sohbet ettiğiniz herkes de bu cihaza güvenebilir." + "Cihaz doğrulandı" + "Başka bir cihaz kullan" + "Diğer cihazda bekleniyor…" + "Bir şeyler doğru görünmüyor. İstek zaman aşımına uğradı veya istek reddedildi." + "Aşağıdaki emojilerin diğer oturumunuzda gösterilenlerle eşleştiğini onaylayın." + "Emojileri karşılaştırın" + "Aşağıdaki emojilerin diğer kullanıcının cihazında gösterilenlerle eşleştiğini onaylayın." + "Aşağıdaki sayıların diğer oturumunuzda gösterilen sayılarla eşleştiğini onaylayın." + "Sayıları karşılaştır" + "Yeni oturumunuz artık doğrulandı. Şifrelenmiş mesajlarınıza erişebilir ve diğer kullanıcılar oturumu güvenilir olarak görecektir." + "Artık mesaj gönderirken veya alırken bu kullanıcının kimliğine güvenebilirsiniz." + "Kurtarma anahtarını girin" + "İstek zaman aşımına uğradı, istek reddedildi veya bir doğrulama uyuşmazlığı vardı." + "Şifrelenmiş mesaj geçmişinize erişmek için siz olduğunuzu kanıtlayın." + "Mevcut bir oturumu aç" + "Doğrulamayı yeniden dene" + "Hazırım" + "Eşleşme bekleniyor…" + "Benzersiz bir emoji setini karşılaştır." + "Benzersiz emojileri karşılaştırın ve aynı sırayla göründüklerinden emin olun." + "Oturum açıldı" + "İstek zaman aşımına uğradı, istek reddedildi veya bir doğrulama uyuşmazlığı vardı." + "Doğrulama başarısız" + "Yalnızca bu doğrulamayı siz başlattıysanız devam edin." + "Mesaj geçmişinizi güvende tutmak için diğer cihazı doğrulayın." + "Artık diğer cihazınızda güvenli bir şekilde mesaj okuyabilir veya gönderebilirsiniz." + "Cihaz doğrulandı" + "Doğrulama talep edildi" + "Eşleşmiyorlar" + "Eşleşiyorlar" + "Buradan doğrulamaya başlamadan önce uygulamanın diğer cihazda açık olduğundan emin olun." + "Uygulamayı doğrulanmış başka bir cihazda açın" + "Daha fazla güvenlik için, cihazlarınızdaki bir dizi emojiyi karşılaştırarak bu kullanıcıyı doğrulayın. Bunu, iletişim kurmak için güvenilir bir yol kullanarak yapın." + "Bu kullanıcıyı doğrula?" + "Ekstra güvenlik için, başka bir kullanıcı kimliğinizi doğrulamak istiyor. Karşılaştırmanız için size bir dizi emoji gösterilecektir." + "Diğer cihazda bir açılır pencere görmelisiniz. Doğrulamayı şimdi oradan başlatın." + "Diğer cihazda doğrulamayı başlat" + "Diğer cihaz bekleniyor" + "Diğer kullanıcı bekleniyor" + "Kabul edildikten sonra doğrulama işlemine devam edebileceksiniz." + "Devam etmek için diğer oturumunuzda doğrulama işlemini başlatma isteğini kabul edin." + "İsteğin kabul edilmesi bekleniyor" + "Oturum kapatılıyor…" + 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 773adf63189..10c6ae184d8 100644 --- a/features/verifysession/impl/src/main/res/values-uk/translations.xml +++ b/features/verifysession/impl/src/main/res/values-uk/translations.xml @@ -13,9 +13,11 @@ "Щось не так. Або час очікування запиту минув, або в запиті було відмовлено." "Переконайтеся, що емодзі нижче збігаються з тими, що відображаються під час іншого сеансу." "Порівняти емодзі" + "Переконайтеся, що наведені емоджі збігаються з тими, що показані на пристрої іншого користувача." "Переконайтеся, що наведені нижче цифри збігаються з тими, що показані під час вашого іншого сеансу." "Порівняйте цифри" "Ваш новий сеанс підтверджено. Він матиме доступ до ваших зашифрованих повідомлень, й інші користувачі вважатимуть його надійним." + "Тепер ви можете довіряти особистості цього користувача під час надсилання або отримання повідомлень." "Введіть ключ відновлення" "Або час очікування запиту минув, або запит було відхилено, або виникла розбіжність у верифікації." "Доведіть, що це ви, щоб отримати доступ до історії зашифрованих повідомлень." @@ -27,7 +29,7 @@ "Порівняйте унікальні емодзі, переконавшись, що вони показані в однаковому порядку." "Увійшов" "Або час очікування запиту минув, або запит було відхилено, або виникла розбіжність у верифікації." - "Не вдалося перевірити" + "Перевірка не вдалася" "Продовжуйте, лише якщо ви ініціювали цю перевірку." "Перевірте інший пристрій, щоб захистити історію повідомлень." "Тепер ви можете безпечно читати або надсилати повідомлення на іншому пристрої." @@ -37,8 +39,14 @@ "Вони збігаються" "Перш ніж починати перевірку звідси, переконайтеся, що програму відкрито на іншому пристрої." "Відкрийте додаток на іншому перевіреному пристрої" + "Для додаткової безпеки верифікуйте цього користувача, порівнявши набір емоджі на ваших пристроях. Зробіть це, використовуючи надійний спосіб спілкування." + "Верифікувати цього користувача?" + "Для додаткової безпеки інший користувач хоче верифікувати вашу особистість. Вам буде показано набір емоджі для порівняння." "Ви повинні побачити спливаюче вікно на іншому пристрої. Почніть перевірку звідти." "Почніть перевірку на іншому пристрої" + "Очікування іншого пристрою" + "Очікування іншого користувача" + "Після погодження ви зможете продовжити верифікацію." "Щоб продовжити, прийміть запит на початок процесу верифікації в іншому сеансі." "Очікування на прийняття запиту" "Вихід…" diff --git a/features/verifysession/impl/src/main/res/values-ur/translations.xml b/features/verifysession/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..e6f0fcd3672 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,33 @@ + + + "تصدیق نہیں کر سکتے؟" + "ایک نئی بازیابی کلید تخلیق کریں" + "محفوظ پیغام رسانی ترتیب دینے کیلئے اس آلے کی توثیق کریں۔" + "اپنی شناخت کی تصدیق کریں" + "دوسرا آلہ استعمال کریں" + "بازیابی کلید استعمال کریں" + "اب آپ محفوظ طریقے سے پیغامات پڑھ یا بھیج سکتے ہیں، اور جسکے ساتھ آپ گفتگو کرتے ہیں وہ بھی اس آلہ پر بھروسہ کر سکتا ہے۔" + "آلہ توثیق شدہ" + "دوسرا آلہ استعمال کریں" + "دوسرے آلہ پر منتظر…" + "کچھ ٹھیک نہیں لگتا۔ یا تو درخواست کا وقت ختم ہو گیا یا درخواست مسترد کر دی گئی۔" + "تصدیق کریں کہ نیچے ایموجی آپ کے دوسرے جلسے میں دکھائے گئے ایموجیوں سے مماثل ہیں۔" + "ایموجیوں کا موازنہ کریں۔" + "تصدیق کریں کہ نیچے دیے گئے اعداد آپکے دوسرے جلسے میں دکھائے گئے اعداد سے مماثل ہیں۔" + "اعداد کا موازنہ کریں" + "آپ کا نیا جلسہ اب توثیق شدہ ہے۔ اسے آپ کے مرموزکردہ پیغامات تک رسائی حاصل ہے، اور دوسرے صارفین اسے بھروسہ مند کے طور پر دیکھیں گے۔" + "بازیابی کلید درج کریں" + "اپنی مرموزکردہ پیغام کی سرگزشت تک رسائی حاصل کرنے کے لیے ثابت کریں کہ یہ آپ ہی ہیں۔" + "ایک موجودہ جلسہ کھولیں" + "توثیق کی پھر کوشش کریں" + "میں تیار ہوں" + "ملانے کا انتظار" + "رموز تعبیری کے منفرد مجموعہ کا موازنہ کریں۔" + "منفرد ایموجی کا موازنہ کریں، یقینی بناتے ہوئے کہ وہ ایک ہی ترتیب میں دکھائی دیں۔" + "آلہ توثیق شدہ" + "وہ مماثل نہیں ہیں" + "وہ مماثل ہیں" + "جاری رکھنے کیلئے اپنے دوسرے جلسے میں توثیقی عمل شروع کرنے کی درخواست کو قبول کریں۔" + "درخواست قبول کرنے کا منتظر" + "خارج ہورہاہے…" + 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 7eb21b9330b..46e26a2e70d 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 @@ -13,9 +13,11 @@ "似乎出了一點問題。有可能是因為等候逾時,或是請求被拒絕。" "確認顯示在其他工作階段上的表情符號是否和下方的相同。" "比對表情符號" + "確認下方的表情符號與其他使用者裝置上顯示的表情符號相符。" "確認以下數字是否與其他作業階段中顯示的數字相符。" "比較數字" "新的工作階段已完成驗證。它能夠存取您的加密訊息,而其他使用者會將它視為可信任的。" + "現在,您可以在傳送或接收訊息時信任此使用者的身份。" "輸入復原金鑰" "請求逾時、請求被拒或是驗證不符。" "為了存取被加密的歷史訊息,您需要證明這是您本人。" @@ -27,18 +29,24 @@ "表情符號是唯一的,請相互比對,確認它們的排列順序是否相同。" "已登入" "請求逾時、請求被拒或是驗證不符。" - "驗證失敗" + "驗證失敗。" "僅當您啟動此驗證時才繼續。" "驗證其他裝置以保護您的訊息歷史紀錄安全。" "現在,您可以在其他裝置上安全地閱讀或傳送訊息。" - "已驗證裝置" + "裝置已驗證" "已請求驗證" "不一樣" "一樣" "從這裡開始驗證之前,請確保您在其他裝置中開啟了應用程式。" "在另外一個已驗證的裝置上開啟應用程式" + "為了提昇安全性,請透過比較您裝置上的一組表情符號來驗證此使用者。請透過可信的通訊方式來執行此動作。" + "驗證此使用者?" + "為了提昇安全性,另一個使用者希望驗證您的身份。您將會看到一組表情符號以進行比較。" "您應該會在其他裝置上看到一個彈出式視窗。立刻從那裡開始驗證。" "在其他裝置上開始驗證" + "正在等待其他裝置" + "正在等帶齊他使用者" + "接受後,您就可以繼續進行驗證。" "準備開始驗證,請到您的其他工作階段接受請求。" "等待接受請求" "正在登出…" 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 8a606a38f5f..fceaef0572b 100644 --- a/features/verifysession/impl/src/main/res/values-zh/translations.xml +++ b/features/verifysession/impl/src/main/res/values-zh/translations.xml @@ -13,9 +13,11 @@ "发生了一些错误。网络请求超时,或者被服务器拒绝。" "确认下方的表情符号与另一设备上显示的相同。" "比较表情符号" + "请验证下方表情是否与对方设备显示一致" "确认以下数字与其他会话中显示的一致。" "比较数字" "新设备已经成功验证。现在新设备可以访问加密信息,其他用户也会信任这个设备。" + "现在您可以在发送或接收消息时信任该用户的身份。" "输入恢复密钥" "要么请求超时,要么请求被拒绝,要么验证不匹配。" "证明自己的身份以访问加密历史消息。" @@ -39,6 +41,7 @@ "在另一台验证的设备上打开应用" "您应该会在另一台设备上看到一个弹出窗口。现在从那里开始验证。" "在另一台设备上开始验证" + "正在等待其他设备" "请在其他会话中接受验证请求。" "等待接受请求" "正在登出…" diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index db18590cbe2..df9e6284d5d 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -13,9 +13,11 @@ "Something doesn’t seem right. Either the request timed out or the request was denied." "Confirm that the emojis below match those shown on your other session." "Compare emojis" + "Confirm that the emojis below match those shown on the other user’s device." "Confirm that the numbers below match those shown on your other session." "Compare numbers" "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted." + "Now you can trust the identity of this user when sending or receiving messages." "Enter recovery key" "Either the request timed out, the request was denied, or there was a verification mismatch." "Prove it’s you in order to access your encrypted message history." @@ -37,8 +39,14 @@ "They match" "Make sure you have the app open in the other device before starting verification from here." "Open the app on another verified device" + "For extra security, verify this user by comparing a set of emojis on your devices. Do this by using a trusted way to communicate." + "Verify this user?" + "For extra security, another user wants to verify your identity. You’ll be shown a set of emojis to compare." "You should see a popup on the other device. Start the verification from there now." "Start verification on the other device" + "Waiting for the other device" + "Waiting for the other user" + "Once accepted you’ll be able to continue with the verification." "Accept the request to start the verification process in your other session to continue." "Waiting to accept request" "Signing out…" 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 74b62a5654b..8fdf62df4f0 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 @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.FlowId import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.A_USER_ID @@ -25,10 +26,13 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.seconds @ExperimentalCoroutinesApi class IncomingVerificationPresenterTest { @@ -37,7 +41,7 @@ class IncomingVerificationPresenterTest { @Test fun `present - nominal case - incoming verification successful`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val approveVerificationLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } @@ -59,15 +63,20 @@ class IncomingVerificationPresenterTest { isWaiting = false, ) ) + + advanceTimeBy(1.seconds) + resetLambda.assertions().isCalledOnce().with(value(false)) - acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails)) + acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() // User accept the incoming verification initialState.eventSink(IncomingVerificationViewEvents.StartVerification) skipItems(1) val initialWaitingState = awaitItem() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() - advanceUntilIdle() + + advanceTimeBy(1.seconds) + acceptVerificationRequestLambda.assertions().isCalledOnce() // Remote sent the data fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) @@ -100,7 +109,7 @@ class IncomingVerificationPresenterTest { @Test fun `present - emoji not matching case - incoming verification failure`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val declineVerificationLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } @@ -122,15 +131,20 @@ class IncomingVerificationPresenterTest { isWaiting = false, ) ) + + advanceTimeBy(1.seconds) + resetLambda.assertions().isCalledOnce().with(value(false)) - acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails)) + acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() // User accept the incoming verification initialState.eventSink(IncomingVerificationViewEvents.StartVerification) skipItems(1) val initialWaitingState = awaitItem() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() - advanceUntilIdle() + + advanceTimeBy(1.seconds) + acceptVerificationRequestLambda.assertions().isCalledOnce() // Remote sent the data fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) @@ -157,7 +171,7 @@ class IncomingVerificationPresenterTest { @Test fun `present - incoming verification is remotely canceled`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val declineVerificationLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } @@ -185,13 +199,16 @@ class IncomingVerificationPresenterTest { fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidCancel) // The screen is dismissed skipItems(2) + + advanceUntilIdle() + onFinishLambda.assertions().isCalledOnce() } } @Test fun `present - user goes back when comparing emoji - incoming verification failure`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val declineVerificationLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } @@ -213,15 +230,20 @@ class IncomingVerificationPresenterTest { isWaiting = false, ) ) + + advanceTimeBy(1.seconds) + resetLambda.assertions().isCalledOnce().with(value(false)) - acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails)) + acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() // User accept the incoming verification initialState.eventSink(IncomingVerificationViewEvents.StartVerification) skipItems(1) val initialWaitingState = awaitItem() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() - advanceUntilIdle() + + advanceTimeBy(1.seconds) + acceptVerificationRequestLambda.assertions().isCalledOnce() // Remote sent the data fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) @@ -248,7 +270,7 @@ class IncomingVerificationPresenterTest { @Test fun `present - user ignores incoming request`() = runTest { - val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } val acceptVerificationRequestLambda = lambdaRecorder { } val resetLambda = lambdaRecorder { } val fakeSessionVerificationService = FakeSessionVerificationService( @@ -268,24 +290,30 @@ class IncomingVerificationPresenterTest { } } - private val aSessionVerificationRequestDetails = SessionVerificationRequestDetails( - senderId = A_USER_ID, - flowId = FlowId("flowId"), - deviceId = A_DEVICE_ID, - displayName = "a device name", - firstSeenTimestamp = A_TIMESTAMP, + private val anIncomingSessionVerificationRequest = VerificationRequest.Incoming.OtherSession( + details = SessionVerificationRequestDetails( + senderProfile = SessionVerificationRequestDetails.SenderProfile( + userId = A_USER_ID, + displayName = "a device name", + avatarUrl = null, + ), + flowId = FlowId("flowId"), + deviceId = A_DEVICE_ID, + firstSeenTimestamp = A_TIMESTAMP, + ) ) - private fun createPresenter( - sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails, + private fun TestScope.createPresenter( + verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest, navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, service: SessionVerificationService = FakeSessionVerificationService(), dateFormatter: DateFormatter = FakeDateFormatter(), ) = IncomingVerificationPresenter( - sessionVerificationRequestDetails = sessionVerificationRequestDetails, + verificationRequest = verificationRequest, navigator = navigator, sessionVerificationService = service, stateMachine = IncomingVerificationStateMachine(service), dateFormatter = dateFormatter, + sessionCoroutineScope = backgroundScope, ) } 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 new file mode 100644 index 00000000000..06940f37a5f --- /dev/null +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt @@ -0,0 +1,338 @@ +/* + * 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. + */ + +package io.element.android.features.verifysession.impl.outgoing + +import app.cash.turbine.ReceiveTurbine +import com.google.common.truth.Truth.assertThat +import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationState.Step +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus +import io.element.android.libraries.matrix.api.verification.VerificationEmoji +import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.tests.testutils.WarmUpRule +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.runTest +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class OutgoingVerificationPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - Initial state is received`() = runTest { + val presenter = createOutgoingVerificationPresenter( + service = unverifiedSessionService(), + ) + presenter.test { + awaitItem().run { + assertThat(step).isEqualTo(Step.Initial) + } + } + } + + @Test + fun `present - Handles requestVerification for session verification`() = runTest { + val requestSessionVerificationRecorder = lambdaRecorder {} + val startVerificationRecorder = lambdaRecorder {} + val service = unverifiedSessionService( + requestSessionVerificationLambda = requestSessionVerificationRecorder, + startVerificationLambda = startVerificationRecorder, + ) + val presenter = createOutgoingVerificationPresenter( + service = service, + verificationRequest = anOutgoingSessionVerificationRequest(), + ) + presenter.test { + requestVerificationAndAwaitVerifyingState(service) + + requestSessionVerificationRecorder.assertions().isCalledOnce() + startVerificationRecorder.assertions().isCalledOnce() + } + } + + @Test + fun `present - Handles requestVerification for user verification`() = runTest { + val requestUserVerificationRecorder = lambdaRecorder {} + val startVerificationRecorder = lambdaRecorder {} + val service = unverifiedSessionService( + requestUserVerificationLambda = requestUserVerificationRecorder, + startVerificationLambda = startVerificationRecorder, + ) + val presenter = createOutgoingVerificationPresenter( + service = service, + verificationRequest = anOutgoingUserVerificationRequest(), + ) + presenter.test { + requestVerificationAndAwaitVerifyingState(service) + + requestUserVerificationRecorder.assertions().isCalledOnce() + startVerificationRecorder.assertions().isCalledOnce() + } + } + + @Test + fun `present - Cancellation on initial state moves to Exit state`() = runTest { + val presenter = createOutgoingVerificationPresenter( + service = unverifiedSessionService(), + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.step).isEqualTo(Step.Initial) + val eventSink = initialState.eventSink + eventSink(OutgoingVerificationViewEvents.Cancel) + + assertThat(awaitItem().step).isEqualTo(Step.Exit) + } + } + + @Test + fun `present - A failure when verifying cancels it`() = runTest { + val service = unverifiedSessionService( + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, + approveVerificationLambda = { }, + ) + val presenter = createOutgoingVerificationPresenter(service) + presenter.test { + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification) + // Cancelling + assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java) + service.emitVerificationFlowState(VerificationFlowState.DidFail) + // Cancelled + assertThat(awaitItem().step).isEqualTo(Step.Canceled) + } + } + + @Test + fun `present - A fail when requesting verification resets the state to the canceled one`() = runTest { + val service = unverifiedSessionService( + requestSessionVerificationLambda = { }, + ) + val presenter = createOutgoingVerificationPresenter(service) + presenter.test { + awaitItem().eventSink(OutgoingVerificationViewEvents.RequestVerification) + service.emitVerificationFlowState(VerificationFlowState.DidFail) + assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java) + assertThat(awaitItem().step).isEqualTo(Step.Canceled) + } + } + + @Test + fun `present - Canceling the flow once it's verifying cancels it`() = runTest { + val service = unverifiedSessionService( + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, + cancelVerificationLambda = { }, + ) + val presenter = createOutgoingVerificationPresenter(service) + presenter.test { + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(OutgoingVerificationViewEvents.Cancel) + assertThat(awaitItem().step).isEqualTo(Step.Canceled) + } + } + + @Test + fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { + val service = unverifiedSessionService( + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, + ) + val presenter = createOutgoingVerificationPresenter(service) + presenter.test { + requestVerificationAndAwaitVerifyingState(service) + service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList()))) + ensureAllEventsConsumed() + } + } + + @Test + fun `present - Go back after cancellation returns to initial state`() = runTest { + val service = unverifiedSessionService( + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, + ) + val presenter = createOutgoingVerificationPresenter(service) + presenter.test { + val state = requestVerificationAndAwaitVerifyingState(service) + service.emitVerificationFlowState(VerificationFlowState.DidCancel) + assertThat(awaitItem().step).isEqualTo(Step.Canceled) + state.eventSink(OutgoingVerificationViewEvents.Reset) + // Went back to initial state + assertThat(awaitItem().step).isEqualTo(Step.Initial) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - When verification is approved, the flow completes if there is no error`() = runTest { + val emojis = listOf( + VerificationEmoji(number = 30) + ) + val service = unverifiedSessionService( + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, + approveVerificationLambda = { }, + ) + val presenter = createOutgoingVerificationPresenter(service) + presenter.test { + val state = requestVerificationAndAwaitVerifyingState( + service, + SessionVerificationData.Emojis(emojis) + ) + state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification) + assertThat(awaitItem().step).isEqualTo( + Step.Verifying( + SessionVerificationData.Emojis(emojis), + AsyncData.Loading(), + ) + ) + service.emitVerificationFlowState(VerificationFlowState.DidFinish) + service.emitVerifiedStatus(SessionVerifiedStatus.Verified) + assertThat(awaitItem().step).isEqualTo(Step.Completed) + } + } + + @Test + fun `present - When verification is declined, the flow is canceled`() = runTest { + val service = unverifiedSessionService( + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, + declineVerificationLambda = { }, + ) + val presenter = createOutgoingVerificationPresenter(service) + presenter.test { + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(OutgoingVerificationViewEvents.DeclineVerification) + assertThat(awaitItem().step).isEqualTo( + Step.Verifying( + SessionVerificationData.Emojis(emptyList()), + AsyncData.Loading(), + ) + ) + service.emitVerificationFlowState(VerificationFlowState.DidCancel) + assertThat(awaitItem().step).isEqualTo(Step.Canceled) + } + } + + @Test + fun `present - When verification is done using recovery key, the flow is completed`() = runTest { + val service = FakeSessionVerificationService( + resetLambda = { }, + ).apply { + emitNeedsSessionVerification(false) + emitVerifiedStatus(SessionVerifiedStatus.Verified) + emitVerificationFlowState(VerificationFlowState.DidFinish) + } + val presenter = createOutgoingVerificationPresenter( + service = service, + showDeviceVerifiedScreen = true, + ) + presenter.test { + assertThat(awaitItem().step).isEqualTo(Step.Completed) + } + } + + @Test + fun `present - When verification is not needed, the flow is skipped`() = runTest { + val service = FakeSessionVerificationService( + resetLambda = { }, + ).apply { + emitNeedsSessionVerification(false) + emitVerifiedStatus(SessionVerifiedStatus.Verified) + emitVerificationFlowState(VerificationFlowState.DidFinish) + } + val presenter = createOutgoingVerificationPresenter( + service = service, + showDeviceVerifiedScreen = false, + ) + presenter.test { + skipItems(1) + assertThat(awaitItem().step).isEqualTo(Step.Exit) + } + } + + private suspend fun ReceiveTurbine.requestVerificationAndAwaitVerifyingState( + fakeService: FakeSessionVerificationService, + sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), + ): OutgoingVerificationState { + var state = awaitItem() + assertThat(state.step).isEqualTo(Step.Initial) + state.eventSink(OutgoingVerificationViewEvents.RequestVerification) + // Await for other device response: + fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) + state = awaitItem() + assertThat(state.step).isEqualTo(Step.AwaitingOtherDeviceResponse) + // Await for the state to be Ready + state = awaitItem() + assertThat(state.step).isEqualTo(Step.Ready) + state.eventSink(OutgoingVerificationViewEvents.StartSasVerification) + // Await for other device response (again): + fakeService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification) + state = awaitItem() + assertThat(state.step).isEqualTo(Step.AwaitingOtherDeviceResponse) + // Finally, ChallengeReceived: + fakeService.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(sessionVerificationData)) + state = awaitItem() + assertThat(state.step).isInstanceOf(Step.Verifying::class.java) + return state + } + + private suspend fun unverifiedSessionService( + requestSessionVerificationLambda: () -> Unit = { lambdaError() }, + requestUserVerificationLambda: (UserId) -> Unit = { lambdaError() }, + cancelVerificationLambda: () -> Unit = { lambdaError() }, + approveVerificationLambda: () -> Unit = { lambdaError() }, + declineVerificationLambda: () -> Unit = { lambdaError() }, + startVerificationLambda: () -> Unit = { lambdaError() }, + resetLambda: (Boolean) -> Unit = { }, + acknowledgeVerificationRequestLambda: (VerificationRequest.Incoming) -> Unit = { lambdaError() }, + acceptVerificationRequestLambda: () -> Unit = { lambdaError() }, + ): FakeSessionVerificationService { + return FakeSessionVerificationService( + requestCurrentSessionVerificationLambda = requestSessionVerificationLambda, + requestUserVerificationLambda = requestUserVerificationLambda, + cancelVerificationLambda = cancelVerificationLambda, + approveVerificationLambda = approveVerificationLambda, + declineVerificationLambda = declineVerificationLambda, + startVerificationLambda = startVerificationLambda, + resetLambda = resetLambda, + acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda, + acceptVerificationRequestLambda = acceptVerificationRequestLambda, + ).apply { + emitVerifiedStatus(SessionVerifiedStatus.NotVerified) + } + } + + private fun createOutgoingVerificationPresenter( + service: SessionVerificationService, + verificationRequest: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(), + encryptionService: EncryptionService = FakeEncryptionService(), + showDeviceVerifiedScreen: Boolean = false, + ): OutgoingVerificationPresenter { + return OutgoingVerificationPresenter( + showDeviceVerifiedScreen = showDeviceVerifiedScreen, + verificationRequest = verificationRequest, + sessionVerificationService = service, + encryptionService = encryptionService, + ) + } +} 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 new file mode 100644 index 00000000000..c1897d4c6fb --- /dev/null +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright 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. + */ + +package io.element.android.features.verifysession.impl.outgoing + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.verifysession.impl.R +import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.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`() { + val eventsRecorder = EventsRecorder() + rule.setOutgoingVerificationView( + anOutgoingVerificationState( + step = OutgoingVerificationState.Step.Canceled, + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Reset) + } + + @Test + fun `back key pressed - when awaiting response cancels the verification`() { + val eventsRecorder = EventsRecorder() + rule.setOutgoingVerificationView( + anOutgoingVerificationState( + step = OutgoingVerificationState.Step.AwaitingOtherDeviceResponse, + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) + } + + @Test + fun `back key pressed - when ready to verify cancels the verification`() { + val eventsRecorder = EventsRecorder() + rule.setOutgoingVerificationView( + anOutgoingVerificationState( + step = OutgoingVerificationState.Step.Ready, + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) + } + + @Test + fun `back key pressed - when verifying and not loading declines the verification`() { + val eventsRecorder = EventsRecorder() + rule.setOutgoingVerificationView( + anOutgoingVerificationState( + step = OutgoingVerificationState.Step.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, + ), + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) + } + + @Test + fun `back key pressed - when verifying and loading does nothing`() { + val eventsRecorder = EventsRecorder() + rule.setOutgoingVerificationView( + anOutgoingVerificationState( + step = OutgoingVerificationState.Step.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Loading(), + ), + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertEmpty() + } + + @Test + fun `back key pressed - on Completed exits the flow`() { + ensureCalledOnce { callback -> + rule.setOutgoingVerificationView( + onBack = callback, + state = anOutgoingVerificationState( + step = OutgoingVerificationState.Step.Completed, + ), + ) + rule.pressBackKey() + } + } + + @Test + 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 -> + rule.setOutgoingVerificationView( + anOutgoingVerificationState( + step = OutgoingVerificationState.Step.Completed, + eventSink = eventsRecorder + ), + onFinished = callback, + ) + rule.clickOn(CommonStrings.action_continue) + } + } + + @Test + fun `clicking on they match emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setOutgoingVerificationView( + anOutgoingVerificationState( + step = OutgoingVerificationState.Step.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, + ), + eventSink = eventsRecorder + ), + ) + 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`() { + val eventsRecorder = EventsRecorder() + rule.setOutgoingVerificationView( + anOutgoingVerificationState( + step = OutgoingVerificationState.Step.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, + ), + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_session_verification_they_dont_match) + eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) + } + + private fun AndroidComposeTestRule.setOutgoingVerificationView( + state: OutgoingVerificationState, + onLearnMoreClick: () -> Unit = EnsureNeverCalled(), + onFinished: () -> Unit = EnsureNeverCalled(), + onBack: () -> Unit = EnsureNeverCalled(), + ) { + setContent { + OutgoingVerificationView( + state = state, + onLearnMoreClick = onLearnMoreClick, + onFinish = onFinished, + onBack = onBack, + ) + } + } +} diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt deleted file mode 100644 index 0e3b05ecbf2..00000000000 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt +++ /dev/null @@ -1,405 +0,0 @@ -/* - * 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. - */ - -package io.element.android.features.verifysession.impl.outgoing - -import app.cash.turbine.ReceiveTurbine -import com.google.common.truth.Truth.assertThat -import io.element.android.features.logout.api.LogoutUseCase -import io.element.android.features.logout.test.FakeLogoutUseCase -import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.core.meta.BuildMeta -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.verification.SessionVerificationData -import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails -import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus -import io.element.android.libraries.matrix.api.verification.VerificationEmoji -import io.element.android.libraries.matrix.api.verification.VerificationFlowState -import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService -import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService -import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore -import io.element.android.tests.testutils.WarmUpRule -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.test -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -@ExperimentalCoroutinesApi -class VerifySelfSessionPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - Initial state is received`() = runTest { - val presenter = createVerifySelfSessionPresenter( - service = unverifiedSessionService(), - ) - presenter.test { - awaitItem().run { - assertThat(step).isEqualTo(Step.Initial(false)) - assertThat(displaySkipButton).isTrue() - } - } - } - - @Test - fun `present - hides skip verification button on non-debuggable builds`() = runTest { - val buildMeta = aBuildMeta(isDebuggable = false) - val presenter = createVerifySelfSessionPresenter( - service = unverifiedSessionService(), - buildMeta = buildMeta, - ) - presenter.test { - assertThat(awaitItem().displaySkipButton).isFalse() - } - } - - @Test - fun `present - Initial state is received, can use recovery key`() = runTest { - val resetLambda = lambdaRecorder { } - val presenter = createVerifySelfSessionPresenter( - service = unverifiedSessionService( - resetLambda = resetLambda - ), - encryptionService = FakeEncryptionService().apply { - emitRecoveryState(RecoveryState.INCOMPLETE) - } - ) - presenter.test { - assertThat(awaitItem().step).isEqualTo(Step.Initial(true)) - resetLambda.assertions().isCalledOnce().with(value(true)) - } - } - - @Test - fun `present - Initial state is received, can use recovery key and is last device`() = runTest { - val presenter = createVerifySelfSessionPresenter( - service = unverifiedSessionService(), - encryptionService = FakeEncryptionService().apply { - emitIsLastDevice(true) - emitRecoveryState(RecoveryState.INCOMPLETE) - } - ) - presenter.test { - assertThat(awaitItem().step).isEqualTo(Step.Initial(canEnterRecoveryKey = true, isLastDevice = true)) - } - } - - @Test - fun `present - Handles requestVerification`() = runTest { - val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - requestVerificationAndAwaitVerifyingState(service) - } - } - - @Test - fun `present - Cancellation on initial state does nothing`() = runTest { - val presenter = createVerifySelfSessionPresenter( - service = unverifiedSessionService(), - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.step).isEqualTo(Step.Initial(false)) - val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.Cancel) - expectNoEvents() - } - } - - @Test - fun `present - A failure when verifying cancels it`() = runTest { - val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, - approveVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification) - // Cancelling - assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java) - service.emitVerificationFlowState(VerificationFlowState.DidFail) - // Cancelled - assertThat(awaitItem().step).isEqualTo(Step.Canceled) - } - } - - @Test - fun `present - A fail when requesting verification resets the state to the initial one`() = runTest { - val service = unverifiedSessionService( - requestVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - awaitItem().eventSink(VerifySelfSessionViewEvents.UseAnotherDevice) - awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification) - service.emitVerificationFlowState(VerificationFlowState.DidFail) - assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java) - assertThat(awaitItem().step).isEqualTo(Step.Initial(false)) - } - } - - @Test - fun `present - Canceling the flow once it's verifying cancels it`() = runTest { - val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, - cancelVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(VerifySelfSessionViewEvents.Cancel) - assertThat(awaitItem().step).isEqualTo(Step.Canceled) - } - } - - @Test - fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { - val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - requestVerificationAndAwaitVerifyingState(service) - service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList()))) - ensureAllEventsConsumed() - } - } - - @Test - fun `present - Go back after cancellation returns to initial state`() = runTest { - val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - val state = requestVerificationAndAwaitVerifyingState(service) - service.emitVerificationFlowState(VerificationFlowState.DidCancel) - assertThat(awaitItem().step).isEqualTo(Step.Canceled) - state.eventSink(VerifySelfSessionViewEvents.Reset) - // Went back to initial state - assertThat(awaitItem().step).isEqualTo(Step.Initial(false)) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `present - When verification is approved, the flow completes if there is no error`() = runTest { - val emojis = listOf( - VerificationEmoji(number = 30, emoji = "😀", description = "Smiley") - ) - val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, - approveVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - val state = requestVerificationAndAwaitVerifyingState( - service, - SessionVerificationData.Emojis(emojis) - ) - state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification) - assertThat(awaitItem().step).isEqualTo( - Step.Verifying( - SessionVerificationData.Emojis(emojis), - AsyncData.Loading(), - ) - ) - service.emitVerificationFlowState(VerificationFlowState.DidFinish) - assertThat(awaitItem().step).isEqualTo(Step.Completed) - } - } - - @Test - fun `present - When verification is declined, the flow is canceled`() = runTest { - val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, - declineVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(VerifySelfSessionViewEvents.DeclineVerification) - assertThat(awaitItem().step).isEqualTo( - Step.Verifying( - SessionVerificationData.Emojis(emptyList()), - AsyncData.Loading(), - ) - ) - service.emitVerificationFlowState(VerificationFlowState.DidCancel) - assertThat(awaitItem().step).isEqualTo(Step.Canceled) - } - } - - @Test - fun `present - Skip event skips the flow`() = runTest { - val service = unverifiedSessionService( - requestVerificationLambda = { }, - startVerificationLambda = { }, - ) - val presenter = createVerifySelfSessionPresenter(service) - presenter.test { - val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(VerifySelfSessionViewEvents.SkipVerification) - assertThat(awaitItem().step).isEqualTo(Step.Skipped) - } - } - - @Test - fun `present - When verification is done using recovery key, the flow is completed`() = runTest { - val service = FakeSessionVerificationService( - resetLambda = { }, - ).apply { - emitNeedsSessionVerification(false) - emitVerifiedStatus(SessionVerifiedStatus.Verified) - emitVerificationFlowState(VerificationFlowState.DidFinish) - } - val presenter = createVerifySelfSessionPresenter( - service = service, - showDeviceVerifiedScreen = true, - ) - presenter.test { - assertThat(awaitItem().step).isEqualTo(Step.Completed) - } - } - - @Test - fun `present - When verification is not needed, the flow is skipped`() = runTest { - val service = FakeSessionVerificationService( - resetLambda = { }, - ).apply { - emitNeedsSessionVerification(false) - emitVerifiedStatus(SessionVerifiedStatus.Verified) - emitVerificationFlowState(VerificationFlowState.DidFinish) - } - val presenter = createVerifySelfSessionPresenter( - service = service, - showDeviceVerifiedScreen = false, - ) - presenter.test { - skipItems(1) - assertThat(awaitItem().step).isEqualTo(Step.Skipped) - } - } - - @Test - fun `present - When user request to sign out, the sign out use case is invoked`() = runTest { - val service = FakeSessionVerificationService( - resetLambda = { }, - ).apply { - emitNeedsSessionVerification(false) - emitVerifiedStatus(SessionVerifiedStatus.Verified) - emitVerificationFlowState(VerificationFlowState.DidFinish) - } - val signOutLambda = lambdaRecorder { "aUrl" } - val presenter = createVerifySelfSessionPresenter( - service, - logoutUseCase = FakeLogoutUseCase(signOutLambda) - ) - presenter.test { - skipItems(1) - val initialItem = awaitItem() - initialItem.eventSink(VerifySelfSessionViewEvents.SignOut) - assertThat(awaitItem().signOutAction.isLoading()).isTrue() - val finalItem = awaitItem() - assertThat(finalItem.signOutAction.isSuccess()).isTrue() - assertThat(finalItem.signOutAction.dataOrNull()).isEqualTo("aUrl") - signOutLambda.assertions().isCalledOnce().with(value(true)) - } - } - - private suspend fun ReceiveTurbine.requestVerificationAndAwaitVerifyingState( - fakeService: FakeSessionVerificationService, - sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), - ): VerifySelfSessionState { - var state = awaitItem() - assertThat(state.step).isEqualTo(Step.Initial(false)) - state.eventSink(VerifySelfSessionViewEvents.UseAnotherDevice) - state = awaitItem() - assertThat(state.step).isEqualTo(Step.UseAnotherDevice) - state.eventSink(VerifySelfSessionViewEvents.RequestVerification) - // Await for other device response: - fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) - state = awaitItem() - assertThat(state.step).isEqualTo(Step.AwaitingOtherDeviceResponse) - // Await for the state to be Ready - state = awaitItem() - assertThat(state.step).isEqualTo(Step.Ready) - state.eventSink(VerifySelfSessionViewEvents.StartSasVerification) - // Await for other device response (again): - fakeService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification) - state = awaitItem() - assertThat(state.step).isEqualTo(Step.AwaitingOtherDeviceResponse) - // Finally, ChallengeReceived: - fakeService.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(sessionVerificationData)) - state = awaitItem() - assertThat(state.step).isInstanceOf(Step.Verifying::class.java) - return state - } - - private suspend fun unverifiedSessionService( - requestVerificationLambda: () -> Unit = { lambdaError() }, - cancelVerificationLambda: () -> Unit = { lambdaError() }, - approveVerificationLambda: () -> Unit = { lambdaError() }, - declineVerificationLambda: () -> Unit = { lambdaError() }, - startVerificationLambda: () -> Unit = { lambdaError() }, - resetLambda: (Boolean) -> Unit = { }, - acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() }, - acceptVerificationRequestLambda: () -> Unit = { lambdaError() }, - ): FakeSessionVerificationService { - return FakeSessionVerificationService( - requestVerificationLambda = requestVerificationLambda, - cancelVerificationLambda = cancelVerificationLambda, - approveVerificationLambda = approveVerificationLambda, - declineVerificationLambda = declineVerificationLambda, - startVerificationLambda = startVerificationLambda, - resetLambda = resetLambda, - acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda, - acceptVerificationRequestLambda = acceptVerificationRequestLambda, - ).apply { - emitVerifiedStatus(SessionVerifiedStatus.NotVerified) - } - } - - private fun createVerifySelfSessionPresenter( - service: SessionVerificationService, - encryptionService: EncryptionService = FakeEncryptionService(), - buildMeta: BuildMeta = aBuildMeta(), - sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), - logoutUseCase: LogoutUseCase = FakeLogoutUseCase(), - showDeviceVerifiedScreen: Boolean = false, - ): VerifySelfSessionPresenter { - return VerifySelfSessionPresenter( - showDeviceVerifiedScreen = showDeviceVerifiedScreen, - sessionVerificationService = service, - encryptionService = encryptionService, - stateMachine = VerifySelfSessionStateMachine(service, encryptionService), - buildMeta = buildMeta, - sessionPreferencesStore = sessionPreferencesStore, - logoutUseCase = logoutUseCase, - ) - } -} diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt deleted file mode 100644 index e812d98038e..00000000000 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.features.verifysession.impl.outgoing - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.verifysession.impl.R -import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureNeverCalled -import io.element.android.tests.testutils.EnsureNeverCalledWithParam -import io.element.android.tests.testutils.EventsRecorder -import io.element.android.tests.testutils.clickOn -import io.element.android.tests.testutils.ensureCalledOnce -import io.element.android.tests.testutils.ensureCalledOnceWithParam -import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith -import org.robolectric.annotation.Config - -@RunWith(AndroidJUnit4::class) -class VerifySelfSessionViewTest { - @get:Rule val rule = createAndroidComposeRule() - - @Test - fun `back key pressed - when canceled resets the flow`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Canceled, - eventSink = eventsRecorder - ), - ) - rule.pressBackKey() - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Reset) - } - - @Test - fun `back key pressed - when awaiting response cancels the verification`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.AwaitingOtherDeviceResponse, - eventSink = eventsRecorder - ), - ) - rule.pressBackKey() - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel) - } - - @Test - fun `back key pressed - when ready to verify cancels the verification`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Ready, - eventSink = eventsRecorder - ), - ) - rule.pressBackKey() - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel) - } - - @Test - fun `back key pressed - when verifying and not loading declines the verification`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Verifying( - data = aEmojisSessionVerificationData(), - state = AsyncData.Uninitialized, - ), - eventSink = eventsRecorder - ), - ) - rule.pressBackKey() - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) - } - - @Test - fun `back key pressed - when verifying and loading does nothing`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Verifying( - data = aEmojisSessionVerificationData(), - state = AsyncData.Loading(), - ), - eventSink = eventsRecorder - ), - ) - rule.pressBackKey() - eventsRecorder.assertEmpty() - } - - @Test - fun `back key pressed - on Completed step does nothing`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Completed, - eventSink = eventsRecorder - ), - ) - rule.pressBackKey() - eventsRecorder.assertEmpty() - } - - @Test - 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 -> - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Completed, - eventSink = eventsRecorder - ), - onFinished = callback, - ) - rule.clickOn(CommonStrings.action_continue) - } - } - - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on enter recovery key calls the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Initial(true), - eventSink = eventsRecorder - ), - onEnterRecoveryKey = callback, - ) - rule.clickOn(R.string.screen_session_verification_enter_recovery_key) - } - } - - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on learn more invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Initial(true), - eventSink = eventsRecorder - ), - onLearnMoreClick = callback, - ) - rule.clickOn(CommonStrings.action_learn_more) - } - } - - @Test - fun `clicking on they match emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Verifying( - data = aEmojisSessionVerificationData(), - state = AsyncData.Uninitialized, - ), - eventSink = eventsRecorder - ), - ) - rule.clickOn(R.string.screen_session_verification_they_match) - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.ConfirmVerification) - } - - @Test - fun `clicking on they do not match emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Verifying( - data = aEmojisSessionVerificationData(), - state = AsyncData.Uninitialized, - ), - eventSink = eventsRecorder - ), - ) - rule.clickOn(R.string.screen_session_verification_they_dont_match) - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) - } - - @Test - fun `clicking on 'Skip' emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Initial(canEnterRecoveryKey = true), - displaySkipButton = true, - eventSink = eventsRecorder - ), - ) - rule.clickOn(CommonStrings.action_skip) - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.SkipVerification) - } - - @Test - fun `on Skipped step - onFinished callback is called immediately`() { - ensureCalledOnce { callback -> - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - step = VerifySelfSessionState.Step.Skipped, - displaySkipButton = true, - eventSink = EnsureNeverCalledWithParam(), - ), - onFinished = callback, - ) - } - } - - @Test - fun `on success logout - onFinished callback is called immediately`() { - val aUrl = "aUrl" - ensureCalledOnceWithParam(aUrl) { callback -> - rule.setVerifySelfSessionView( - aVerifySelfSessionState( - signOutAction = AsyncAction.Success(aUrl), - eventSink = EnsureNeverCalledWithParam(), - ), - onSuccessLogout = callback, - ) - } - } - - private fun AndroidComposeTestRule.setVerifySelfSessionView( - state: VerifySelfSessionState, - onLearnMoreClick: () -> Unit = EnsureNeverCalled(), - onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), - onFinished: () -> Unit = EnsureNeverCalled(), - onResetKey: () -> Unit = EnsureNeverCalled(), - onSuccessLogout: (String?) -> Unit = EnsureNeverCalledWithParam(), - ) { - setContent { - VerifySelfSessionView( - state = state, - onLearnMoreClick = onLearnMoreClick, - onEnterRecoveryKey = onEnterRecoveryKey, - onFinish = onFinished, - onResetKey = onResetKey, - onSuccessLogout = onSuccessLogout, - ) - } - } -} diff --git a/features/viewfolder/api/build.gradle.kts b/features/viewfolder/api/build.gradle.kts index b24ad7490ec..4df696b2b25 100644 --- a/features/viewfolder/api/build.gradle.kts +++ b/features/viewfolder/api/build.gradle.kts @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { diff --git a/features/viewfolder/api/src/main/kotlin/io/element/android/features/viewfolder/api/TextFileViewer.kt b/features/viewfolder/api/src/main/kotlin/io/element/android/features/viewfolder/api/TextFileViewer.kt new file mode 100644 index 00000000000..fa1d3511e48 --- /dev/null +++ b/features/viewfolder/api/src/main/kotlin/io/element/android/features/viewfolder/api/TextFileViewer.kt @@ -0,0 +1,20 @@ +/* + * 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.viewfolder.api + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import kotlinx.collections.immutable.ImmutableList + +fun interface TextFileViewer { + @Composable + fun Render( + lines: ImmutableList, + modifier: Modifier, + ) +} diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultTextFileViewer.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultTextFileViewer.kt new file mode 100644 index 00000000000..55743f54cc8 --- /dev/null +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultTextFileViewer.kt @@ -0,0 +1,33 @@ +/* + * 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.viewfolder.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.viewfolder.api.TextFileViewer +import io.element.android.features.viewfolder.impl.file.ColorationMode +import io.element.android.features.viewfolder.impl.file.FileContent +import io.element.android.libraries.di.AppScope +import kotlinx.collections.immutable.ImmutableList +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultTextFileViewer @Inject constructor() : TextFileViewer { + @Composable + override fun Render( + lines: ImmutableList, + modifier: Modifier + ) { + FileContent( + lines = lines, + colorationMode = ColorationMode.None, + modifier = modifier + ) + } +} diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContent.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContent.kt new file mode 100644 index 00000000000..8a233d45064 --- /dev/null +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContent.kt @@ -0,0 +1,152 @@ +/* + * 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.viewfolder.impl.file + +import androidx.compose.foundation.clickable +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.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +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.libraries.androidutils.system.copyToClipboard +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun FileContent( + lines: ImmutableList, + colorationMode: ColorationMode, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + ) { + if (lines.isEmpty()) { + item { + Spacer(Modifier.size(80.dp)) + Text( + text = stringResource(CommonStrings.common_empty_file), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.fillMaxWidth() + ) + } + } else { + itemsIndexed( + items = lines, + ) { index, line -> + LineRow( + lineNumber = index + 1, + line = line, + colorationMode = colorationMode, + ) + } + } + } +} + +@Composable +private fun LineRow( + lineNumber: Int, + line: String, + colorationMode: ColorationMode, +) { + val context = LocalContext.current + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = { + context.copyToClipboard( + text = line, + toastMessage = context.getString(CommonStrings.common_line_copied_to_clipboard), + ) + }) + ) { + Text( + modifier = Modifier + .widthIn(min = 36.dp) + .padding(horizontal = 4.dp), + text = "$lineNumber", + textAlign = TextAlign.End, + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodyMdMedium, + ) + val color = ElementTheme.colors.textSecondary + val width = 0.5.dp.value + Text( + modifier = Modifier + .weight(1f) + .drawWithContent { + // Using .height(IntrinsicSize.Min) on the Row does not work well inside LazyColumn + drawLine( + color = color, + start = Offset(0f, 0f), + end = Offset(0f, size.height), + strokeWidth = width + ) + drawContent() + } + .padding(horizontal = 4.dp), + text = line, + color = line.toColor(colorationMode), + style = ElementTheme.typography.fontBodyMdRegular + ) + } +} + +/** + * Convert a line to a color. + * Ex for logcat: + * `01-23 13:14:50.740 25818 25818 D org.matrix.rust.sdk: elementx: SyncIndicator = Hide | RustRoomListService.kt:81` + * ^ use this char to determine the color + * Ex for Rust logs: + * `2024-01-26T10:22:26.947416Z WARN elementx: Restore with non-empty map | MatrixClientsHolder.kt:68` + * ^ use this char to determine the color, see [LogLevel] + */ +@Composable +private fun String.toColor(colorationMode: ColorationMode): Color { + return when (colorationMode) { + ColorationMode.Logcat -> when (getOrNull(31)) { + 'D' -> colorDebug + 'I' -> colorInfo + 'W' -> colorWarning + 'E' -> colorError + 'A' -> colorError + else -> ElementTheme.colors.textPrimary + } + ColorationMode.RustLogs -> when (getOrNull(32)) { + 'E' -> ElementTheme.colors.textPrimary + 'G' -> colorDebug + 'O' -> colorInfo + 'N' -> colorWarning + 'R' -> colorError + else -> ElementTheme.colors.textPrimary + } + ColorationMode.None -> ElementTheme.colors.textPrimary + } +} + +private val colorDebug = Color(0xFF299999) +private val colorInfo = Color(0xFFABC023) +private val colorWarning = Color(0xFFBBB529) +private val colorError = Color(0xFFFF6B68) diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContentReader.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContentReader.kt index a7a9f12456b..e94bde4a53f 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContentReader.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContentReader.kt @@ -9,6 +9,7 @@ package io.element.android.features.viewfolder.impl.file import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import kotlinx.coroutines.withContext import java.io.File @@ -23,7 +24,7 @@ class DefaultFileContentReader @Inject constructor( private val dispatchers: CoroutineDispatchers, ) : FileContentReader { override suspend fun getLines(path: String): Result> = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { File(path).readLines() } } diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileSave.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileSave.kt index 4337ffd13f6..78648c67387 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileSave.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileSave.kt @@ -16,6 +16,7 @@ import androidx.annotation.RequiresApi import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.androidutils.system.toast 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.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -40,7 +41,7 @@ class DefaultFileSave @Inject constructor( path: String, ) { withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { saveOnDiskUsingMediaStore(path) } else { diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileShare.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileShare.kt index 393139d1821..471f2df5d4e 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileShare.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileShare.kt @@ -13,6 +13,7 @@ import android.net.Uri import androidx.core.content.FileProvider import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.AppScope @@ -37,7 +38,7 @@ class DefaultFileShare @Inject constructor( override suspend fun share( path: String, ) { - runCatching { + runCatchingExceptions { val file = File(path) val shareableUri = file.toShareableUri() val shareMediaIntent = Intent(Intent.ACTION_SEND) diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt index bae8166f49d..4f3b84734fa 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt @@ -7,46 +7,26 @@ package io.element.android.features.viewfolder.impl.file -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.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.async.AsyncFailure import io.element.android.libraries.designsystem.components.async.AsyncLoading 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.aliasScreenTitle 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.TopAppBar import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalMaterial3Api::class) @@ -63,12 +43,7 @@ fun ViewFileView( navigationIcon = { BackButton(onClick = onBackClick) }, - title = { - Text( - text = state.name, - style = ElementTheme.typography.aliasScreenTitle, - ) - }, + titleStr = state.name, actions = { IconButton( onClick = { @@ -114,125 +89,6 @@ fun ViewFileView( ) } -@Composable -private fun FileContent( - lines: ImmutableList, - colorationMode: ColorationMode, - modifier: Modifier = Modifier, -) { - LazyColumn( - modifier = modifier - ) { - if (lines.isEmpty()) { - item { - Spacer(Modifier.size(80.dp)) - Text( - text = "Empty file", - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.fillMaxWidth() - ) - } - } else { - itemsIndexed( - items = lines, - ) { index, line -> - LineRow( - lineNumber = index + 1, - line = line, - colorationMode = colorationMode, - ) - } - } - } -} - -@Composable -private fun LineRow( - lineNumber: Int, - line: String, - colorationMode: ColorationMode, -) { - val context = LocalContext.current - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = { - context.copyToClipboard( - line, - "Line copied to clipboard", - ) - }) - ) { - Text( - modifier = Modifier - .widthIn(min = 36.dp) - .padding(horizontal = 4.dp), - text = "$lineNumber", - textAlign = TextAlign.End, - color = ElementTheme.colors.textSecondary, - style = ElementTheme.typography.fontBodyMdMedium, - ) - val color = ElementTheme.colors.textSecondary - val width = 0.5.dp.value - Text( - modifier = Modifier - .weight(1f) - .drawWithContent { - // Using .height(IntrinsicSize.Min) on the Row does not work well inside LazyColumn - drawLine( - color = color, - start = Offset(0f, 0f), - end = Offset(0f, size.height), - strokeWidth = width - ) - drawContent() - } - .padding(horizontal = 4.dp), - text = line, - color = line.toColor(colorationMode), - style = ElementTheme.typography.fontBodyMdRegular - ) - } -} - -/** - * Convert a line to a color. - * Ex for logcat: - * `01-23 13:14:50.740 25818 25818 D org.matrix.rust.sdk: elementx: SyncIndicator = Hide | RustRoomListService.kt:81` - * ^ use this char to determine the color - * Ex for Rust logs: - * `2024-01-26T10:22:26.947416Z WARN elementx: Restore with non-empty map | MatrixClientsHolder.kt:68` - * ^ use this char to determine the color, see [LogLevel] - */ -@Composable -private fun String.toColor(colorationMode: ColorationMode): Color { - return when (colorationMode) { - ColorationMode.Logcat -> when (getOrNull(31)) { - 'D' -> colorDebug - 'I' -> colorInfo - 'W' -> colorWarning - 'E' -> colorError - 'A' -> colorError - else -> ElementTheme.colors.textPrimary - } - ColorationMode.RustLogs -> when (getOrNull(32)) { - 'E' -> ElementTheme.colors.textPrimary - 'G' -> colorDebug - 'O' -> colorInfo - 'N' -> colorWarning - 'R' -> colorError - else -> ElementTheme.colors.textPrimary - } - ColorationMode.None -> ElementTheme.colors.textPrimary - } -} - -private val colorDebug = Color(0xFF299999) -private val colorInfo = Color(0xFFABC023) -private val colorWarning = Color(0xFFBBB529) -private val colorError = Color(0xFFFF6B68) - @PreviewsDayNight @Composable internal fun ViewFileViewPreview(@PreviewParameter(ViewFileStateProvider::class) state: ViewFileState) = ElementPreview { diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt index d247d87a17a..99de3badb84 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Description import androidx.compose.material.icons.outlined.Folder import androidx.compose.material.icons.outlined.SubdirectoryArrowLeft import androidx.compose.material3.ExperimentalMaterial3Api @@ -27,12 +26,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.viewfolder.impl.model.Item import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.aliasScreenTitle 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.Scaffold @@ -54,12 +53,7 @@ fun ViewFolderView( navigationIcon = { BackButton(onClick = onBackClick) }, - title = { - Text( - text = state.path, - style = ElementTheme.typography.aliasScreenTitle, - ) - } + titleStr = state.path, ) }, content = { padding -> @@ -130,7 +124,7 @@ private fun ItemRow( } is Item.File -> { ListItem( - leadingContent = ListItemContent.Icon(IconSource.Vector(Icons.Outlined.Description)), + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Document())), headlineContent = { Text( text = item.name, diff --git a/gradle.properties b/gradle.properties index fce836543af..513fcac9556 100644 --- a/gradle.properties +++ b/gradle.properties @@ -40,7 +40,7 @@ signing.element.nightly.keyPassword=Secret # Customise the Lint version to use a more recent version than the one bundled with AGP # https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html -android.experimental.lint.version=8.9.0-alpha08 +android.experimental.lint.version=8.12.0-alpha07 # Enable test fixture for all modules by default android.experimental.enableTestFixtures=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2dd8ecae8cc..fa90db9e3fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,57 +3,54 @@ [versions] # Project -android_gradle_plugin = "8.8.0" -kotlin = "2.1.10" -kotlinpoet = "2.0.0" -ksp = "2.1.10-1.0.29" -firebaseAppDistribution = "5.1.0" +android_gradle_plugin = "8.11.0" +kotlin = "2.2.0" +kotlinpoet = "2.2.0" +ksp = "2.2.0-2.0.2" +firebaseAppDistribution = "5.1.1" # AndroidX -core = "1.15.0" -# Warning: there is an issue with 1.1.0 and 1.1.1, that I cannot repro on unit test. -# To repro with the application: -# Clear the storage of the application and run the app. Nearly each time, there is an infinite loading -# due to the DefaultMigrationStore not behaving as expected. -# Stick to 1.0.0 for now, and ensure that this scenario cannot be reproduced when upgrading the version. -datastore = "1.0.0" -constraintlayout = "2.2.0" -constraintlayout_compose = "1.1.0" -lifecycle = "2.8.7" -activity = "1.10.0" -media3 = "1.5.1" -camera = "1.4.1" +core = "1.16.0" +datastore = "1.1.7" +constraintlayout = "2.2.1" +constraintlayout_compose = "1.1.1" +lifecycle = "2.9.1" +activity = "1.10.1" +media3 = "1.7.1" +camera = "1.4.2" # Compose -compose_bom = "2025.01.01" +compose_bom = "2025.06.01" composecompiler = "1.5.15" # Coroutines -coroutines = "1.10.1" +coroutines = "1.10.2" # Accompanist -accompanist = "0.37.0" +accompanist = "0.37.3" # Test test_core = "1.6.1" # Jetbrain -datetime = "0.6.1" -serialization_json = "1.8.0" +datetime = "0.7.0" +serialization_json = "1.9.0" #other -coil = "2.7.0" -showkase = "1.0.3" -appyx = "1.6.0" -sqldelight = "2.0.2" -wysiwyg = "2.38.2" -telephoto = "0.14.0" +detekt = "1.23.8" +coil = "3.2.0" +showkase = "1.0.4" +appyx = "1.7.1" +sqldelight = "2.1.0" +wysiwyg = "2.38.4" +telephoto = "0.16.0" +haze = "1.6.6" # Dependency analysis -dependencyAnalysis = "2.8.0" +dependencyAnalysis = "2.19.0" # DI -dagger = "2.55" +dagger = "2.56.2" anvil = "0.4.1" # Auto service @@ -68,19 +65,20 @@ kover = "0.9.1" android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" } compose_compiler_plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } # https://developer.android.com/studio/write/java8-support#library-desugaring-versions -android_desugar = "com.android.tools:desugar_jdk_libs:2.1.4" +android_desugar = "com.android.tools:desugar_jdk_libs:2.1.5" anvil_gradle_plugin = { module = "dev.zacsweers.anvil:gradle-plugin", version.ref = "anvil" } kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" } 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" } -gms_google_services = "com.google.gms:google-services:4.4.2" +gms_google_services = "com.google.gms:google-services:4.4.3" # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:33.8.0" +google_firebase_bom = "com.google.firebase:firebase-bom:33.16.0" firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" } autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" } ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } +google_tink = "com.google.crypto.tink:tink-android:1.18.0" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } @@ -88,7 +86,7 @@ androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" } androidx_annotationjvm = "androidx.annotation:annotation-jvm:1.9.1" androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } -androidx_exifinterface = "androidx.exifinterface:exifinterface:1.3.7" +androidx_exifinterface = "androidx.exifinterface:exifinterface:1.4.1" androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } androidx_constraintlayout_compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayout_compose" } androidx_camera_lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } @@ -100,7 +98,6 @@ androidx_browser = "androidx.browser:browser:1.8.0" androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx_splash = "androidx.core:core-splashscreen:1.0.1" -androidx_security_crypto = "androidx.security:security-crypto:1.1.0-alpha06" androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "media3" } androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05" @@ -109,7 +106,7 @@ androidx_activity_activity = { module = "androidx.activity:activity", version.re 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.12.1" +androidx_webkit = "androidx.webkit:webkit:1.14.0" androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" } androidx_compose_material3 = { module = "androidx.compose.material3:material3" } @@ -139,7 +136,7 @@ network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" } network_okhttp_okhttp = { module = "com.squareup.okhttp3:okhttp" } network_okhttp = { module = "com.squareup.okhttp3:okhttp" } network_mockwebserver = { module = "com.squareup.okhttp3:mockwebserver" } -network_retrofit_bom = "com.squareup.retrofit2:retrofit-bom:2.11.0" +network_retrofit_bom = "com.squareup.retrofit2:retrofit-bom:3.0.0" network_retrofit = { module = "com.squareup.retrofit2:retrofit" } network_retrofit_converter_serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization" } @@ -149,58 +146,63 @@ test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" } test_arch_core = "androidx.arch.core:core-testing:2.2.0" test_junit = "junit:junit:4.13.2" test_runner = "androidx.test:runner:1.6.2" -test_mockk = "io.mockk:mockk:1.13.16" +test_mockk = "io.mockk:mockk:1.14.4" test_konsist = "com.lemonappdev:konsist:0.17.3" -test_turbine = "app.cash.turbine:turbine:1.2.0" +test_turbine = "app.cash.turbine:turbine:1.2.1" test_truth = "com.google.truth:truth:1.4.4" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18" -test_robolectric = "org.robolectric:robolectric:4.14.1" +test_robolectric = "org.robolectric:robolectric:4.15.1" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } -test_composable_preview_scanner = "com.github.sergio-sastre.ComposablePreviewScanner:android:0.1.2" +test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.6.1" +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" } # Others -coil = { module = "io.coil-kt:coil", version.ref = "coil" } -coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } -coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } -coil_test = { module = "io.coil-kt:coil-test", version.ref = "coil" } -compound = { module = "io.element.android:compound-android", version = "0.2.0" } +coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } +coil_network_okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } +coil_compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil_gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" } +coil_test = { module = "io.coil-kt.coil3:coil-test", version.ref = "coil" } +compound = { module = "io.element.android:compound-android", version = "25.4.4" } datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" } -kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8" +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.18.3" +jsoup = "org.jsoup:jsoup:1.21.1" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } -molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" +molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.77" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.7.3" matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } 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:android-database-sqlcipher:4.5.4" -sqlite = "androidx.sqlite:sqlite-ktx:2.4.0" -unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0" +sqlcipher = "net.zetetic:sqlcipher-android:4.9.0" +sqlite = "androidx.sqlite:sqlite-ktx:2.5.2" +unifiedpush = "org.unifiedpush.android:connector:3.0.10" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2" vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", 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:11.8.0" +maplibre = "org.maplibre.gl:android-sdk:11.11.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" -opusencoder = "io.element.android:opusencoder:1.1.0" -zxing_cpp = "io.github.zxing-cpp:android:2.2.0" +opusencoder = "io.element.android:opusencoder:1.2.0" +zxing_cpp = "io.github.zxing-cpp:android:2.3.0" +haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } +haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } # Analytics -posthog = "com.posthog:posthog-android:3.11.1" -sentry = "io.sentry:sentry-android:8.1.0" +posthog = "com.posthog:posthog-android:3.19.0" +sentry = "io.sentry:sentry-android:8.16.0" # main branch can be tested replacing the version with main-SNAPSHOT matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0" # Emojibase -matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.3.4" +matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.4.2" sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0" # Di @@ -210,6 +212,9 @@ dagger_compiler = { module = "com.google.dagger:dagger-compiler", version.ref = anvil_compiler_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref = "anvil" } anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" } +# Element Call +element_call_embedded = "io.element.android:element-call-embedded:0.13.1" + # Auto services google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } google_autoservice_annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoservice" } @@ -232,15 +237,15 @@ kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" } -detekt = "io.gitlab.arturbosch.detekt:1.23.7" -ktlint = "org.jlleitschuh.gradle.ktlint:12.1.2" +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +ktlint = "org.jlleitschuh.gradle.ktlint:12.3.0" dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12" -dependencycheck = "org.owasp.dependencycheck:12.0.2" +dependencycheck = "org.owasp.dependencycheck:12.1.3" dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" } paparazzi = "app.cash.paparazzi:1.3.5" sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" } knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" } -sonarqube = "org.sonarqube:6.0.1.5171" -licensee = "app.cash.licensee:1.12.0" +sonarqube = "org.sonarqube:6.2.0.5505" +licensee = "app.cash.licensee:1.13.0" compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d..1b33c55baab 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d71047787f8..3735f265b95 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionSha256Sum=7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3b0d4..23d15a93670 100755 --- a/gradlew +++ b/gradlew @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9b42019c791..5eed7ee8452 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts index 02c8061aa2e..1aa23a09e83 100644 --- a/libraries/androidutils/build.gradle.kts +++ b/libraries/androidutils/build.gradle.kts @@ -31,7 +31,6 @@ dependencies { implementation(libs.androidx.activity.activity) implementation(libs.androidx.recyclerview) implementation(libs.androidx.exifinterface) - implementation(libs.androidx.security.crypto) api(libs.androidx.browser) testImplementation(projects.tests.testutils) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt index d4292a27731..5617d459f09 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt @@ -9,12 +9,12 @@ package io.element.android.libraries.androidutils.browser import android.app.Activity import android.content.ActivityNotFoundException -import android.net.Uri import android.os.Bundle import android.provider.Browser import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsSession +import androidx.core.net.toUri import io.element.android.libraries.androidutils.system.openUrlInExternalApp import java.util.Locale @@ -53,12 +53,12 @@ fun Activity.openUrlInChromeCustomTab( // Disable download button intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true) // Disable bookmark button - intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true) + intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_STAR_BUTTON", true) intent.putExtra(Browser.EXTRA_HEADERS, Bundle().apply { putString("Accept-Language", Locale.getDefault().toLanguageTag()) }) } - .launchUrl(this, Uri.parse(url)) + .launchUrl(this, url.toUri()) } catch (activityNotFoundException: ActivityNotFoundException) { openUrlInExternalApp(url) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt index 8697ae9da7a..eaa0b578bc6 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt @@ -12,6 +12,7 @@ import android.content.Context import android.net.Uri import android.provider.OpenableColumns import androidx.core.net.toFile +import io.element.android.libraries.core.extensions.runCatchingExceptions fun Context.getMimeType(uri: Uri): String? = when (uri.scheme) { ContentResolver.SCHEME_CONTENT -> contentResolver.getType(uri) @@ -32,14 +33,14 @@ fun Context.getFileSize(uri: Uri): Long { } ?: 0 } -private fun Context.getContentFileSize(uri: Uri): Long? = runCatching { +private fun Context.getContentFileSize(uri: Uri): Long? = runCatchingExceptions { contentResolver.query(uri, null, null, null, null)?.use { cursor -> cursor.moveToFirst() return@use cursor.getColumnIndexOrThrow(OpenableColumns.SIZE).let(cursor::getLong) } }.getOrNull() -private fun Context.getContentFileName(uri: Uri): String? = runCatching { +private fun Context.getContentFileName(uri: Uri): String? = runCatchingExceptions { contentResolver.query(uri, null, null, null, null)?.use { cursor -> cursor.moveToFirst() return@use cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME).let(cursor::getString) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/EncryptedFileFactory.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/EncryptedFileFactory.kt deleted file mode 100644 index 64f88e5d500..00000000000 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/EncryptedFileFactory.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.androidutils.file - -import android.content.Context -import androidx.security.crypto.EncryptedFile -import androidx.security.crypto.MasterKey -import java.io.File - -class EncryptedFileFactory( - private val context: Context, -) { - fun create(file: File): EncryptedFile { - // We need to use the same key for all the encrypted files. - val masterKey = MasterKey.Builder(context) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build() - return EncryptedFile.Builder( - context, - file, - masterKey, - EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB - ).build() - } -} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt index ef55ed2e08c..414b1231290 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt @@ -17,7 +17,7 @@ import java.util.UUID fun File.safeDelete() { if (exists().not()) return tryOrNull( - onError = { + onException = { Timber.e(it, "Error, unable to delete file $path") }, operation = { @@ -30,7 +30,7 @@ fun File.safeDelete() { fun File.safeRenameTo(dest: File) { tryOrNull( - onError = { + onException = { Timber.e(it, "Error, unable to rename file $path to ${dest.path}") }, operation = { diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index ed4fd31d4c0..8e45262da1e 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -19,6 +19,7 @@ import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.annotation.RequiresApi import androidx.core.content.pm.PackageInfoCompat +import androidx.core.net.toUri import io.element.android.libraries.androidutils.R import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat import io.element.android.libraries.core.mimetype.MimeTypes @@ -121,7 +122,7 @@ fun Context.startInstallFromSourceIntent( noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES) - .setData(Uri.parse("package:$packageName")) + .setData("package:$packageName".toUri()) try { activityResultLauncher.launch(intent) } catch (activityNotFoundException: ActivityNotFoundException) { @@ -165,7 +166,7 @@ fun Context.openUrlInExternalApp( url: String, errorMessage: String = getString(R.string.error_no_compatible_app_found), ) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + val intent = Intent(Intent.ACTION_VIEW, url.toUri()) if (this !is Activity) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt new file mode 100644 index 00000000000..c0cb5a766a6 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/text/LinkifyHelper.kt @@ -0,0 +1,109 @@ +/* + * 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.androidutils.text + +import android.text.Spannable +import android.text.style.URLSpan +import android.text.util.Linkify +import androidx.core.text.getSpans +import androidx.core.text.toSpannable +import androidx.core.text.util.LinkifyCompat +import io.element.android.libraries.core.extensions.runCatchingExceptions +import timber.log.Timber +import kotlin.collections.component1 +import kotlin.collections.component2 + +/** + * Helper class to linkify text while preserving existing URL spans. + * + * It also checks the linkified results to make sure URLs spans are not including trailing punctuation. + */ +object LinkifyHelper { + fun linkify( + text: CharSequence, + @LinkifyCompat.LinkifyMask linkifyMask: Int = Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES, + ): CharSequence { + // Convert the text to a Spannable to be able to add URL spans, return the original text if it's not possible (in tests, i.e.) + val spannable = text.toSpannable() ?: return text + + // Get all URL spans, as they will be removed by LinkifyCompat.addLinks + val oldURLSpans = spannable.getSpans(0, text.length).associateWith { + val start = spannable.getSpanStart(it) + val end = spannable.getSpanEnd(it) + Pair(start, end) + } + // Find and set as URLSpans any links present in the text + val addedNewLinks = LinkifyCompat.addLinks(spannable, linkifyMask) + + // Process newly added URL spans + if (addedNewLinks) { + val newUrlSpans = spannable.getSpans(0, spannable.length) + for (urlSpan in newUrlSpans) { + val start = spannable.getSpanStart(urlSpan) + val end = spannable.getSpanEnd(urlSpan) + + // Try to avoid including trailing punctuation in the link. + // Since this might fail in some edge cases, we catch the exception and just use the original end index. + val newEnd = runCatchingExceptions { + adjustLinkifiedUrlSpanEndIndex(spannable, start, end) + }.onFailure { + Timber.e(it, "Failed to adjust end index for link span") + }.getOrNull() ?: end + + // Adapt the url in the URL span to the new end index too if needed + if (end != newEnd) { + val url = spannable.subSequence(start, newEnd).toString() + spannable.removeSpan(urlSpan) + spannable.setSpan(URLSpan(url), start, newEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + spannable.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + } + + // Restore old spans, remove new ones if there is a conflict + for ((urlSpan, location) in oldURLSpans) { + val (start, end) = location + val addedConflictingSpans = spannable.getSpans(start, end) + if (addedConflictingSpans.isNotEmpty()) { + for (span in addedConflictingSpans) { + spannable.removeSpan(span) + } + } + + spannable.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return spannable + } + + private fun adjustLinkifiedUrlSpanEndIndex(spannable: Spannable, start: Int, end: Int): Int { + var end = end + + // Trailing punctuation found, adjust the end index + while (spannable[end - 1] in sequenceOf('.', ',', ';', ':', '!', '?', '…') && end > start) { + end-- + } + + // If the last character is a closing parenthesis, check if it's part of a pair + if (spannable[end - 1] == ')' && end > start) { + val linkifiedTextLastPath = spannable.substring(start, end).substringAfterLast('/') + val closingParenthesisCount = linkifiedTextLastPath.count { it == ')' } + val openingParenthesisCount = linkifiedTextLastPath.count { it == '(' } + // If it's not part of a pair, remove it from the link span by adjusting the end index + end -= closingParenthesisCount - openingParenthesisCount + } + return end + } +} + +/** + * Linkify the text with the default mask (WEB_URLS, PHONE_NUMBERS, EMAIL_ADDRESSES). + */ +fun CharSequence.safeLinkify(): CharSequence { + return LinkifyHelper.linkify(this, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES) +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt index 30919d52282..8f6766cdd6c 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt @@ -6,36 +6,29 @@ */ package io.element.android.libraries.androidutils.throttler -import android.os.SystemClock +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean /** * Simple ThrottleFirst * See https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.png */ -class FirstThrottler(private val minimumInterval: Long = 800) { - private var lastDate = 0L +class FirstThrottler( + private val minimumInterval: Long = 800, + private val coroutineScope: CoroutineScope, +) { + private val canHandle = AtomicBoolean(true) - sealed interface CanHandleResult { - data object Yes : CanHandleResult - data class No(val shouldWaitMillis: Long) : CanHandleResult - - fun waitMillis(): Long { - return when (this) { - Yes -> 0 - is No -> shouldWaitMillis + fun canHandle(): Boolean { + return canHandle.getAndSet(false).also { result -> + if (result) { + coroutineScope.launch { + delay(minimumInterval) + canHandle.set(true) + } } } } - - fun canHandle(): CanHandleResult { - val now = SystemClock.elapsedRealtime() - val delaySinceLast = now - lastDate - if (delaySinceLast > minimumInterval) { - lastDate = now - return CanHandleResult.Yes - } - - // Too early - return CanHandleResult.No(minimumInterval - delaySinceLast) - } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt index 1081b6a3fb0..e2dacf58e7b 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt @@ -7,8 +7,10 @@ package io.element.android.libraries.androidutils.ui +import android.os.Build import android.view.View import android.view.ViewTreeObserver +import android.view.WindowInsets import android.view.inputmethod.InputMethodManager import androidx.core.content.getSystemService import kotlinx.coroutines.suspendCancellableCoroutine @@ -23,8 +25,18 @@ fun View.showKeyboard(andRequestFocus: Boolean = false) { if (andRequestFocus) { requestFocus() } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + windowInsetsController?.show(WindowInsets.Type.ime()) + } else { + val imm = context?.getSystemService() + imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) + } +} + +fun View.isKeyboardVisible(): Boolean { val imm = context?.getSystemService() - imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) + return imm?.isAcceptingText == true } suspend fun View.awaitWindowFocus() = suspendCancellableCoroutine { continuation -> diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt index 411dbda5111..bef2a501831 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt @@ -8,7 +8,8 @@ package io.element.android.libraries.androidutils.uri import android.net.Uri +import androidx.core.net.toUri const val IGNORED_SCHEMA = "ignored" -fun createIgnoredUri(path: String): Uri = Uri.parse("$IGNORED_SCHEMA://$path") +fun createIgnoredUri(path: String): Uri = "$IGNORED_SCHEMA://$path".toUri() diff --git a/libraries/androidutils/src/main/res/values-cy/translations.xml b/libraries/androidutils/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..edd5b610d85 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-cy/translations.xml @@ -0,0 +1,4 @@ + + + "Heb ganfod ap cydnaws i drin y weithred hon." + diff --git a/libraries/androidutils/src/main/res/values-da/translations.xml b/libraries/androidutils/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..73a3ca6b018 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-da/translations.xml @@ -0,0 +1,4 @@ + + + "Der blev ikke fundet nogen kompatibel app til at håndtere denne handling." + diff --git a/libraries/androidutils/src/main/res/values-eu/translations.xml b/libraries/androidutils/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..3ea09b64b2f --- /dev/null +++ b/libraries/androidutils/src/main/res/values-eu/translations.xml @@ -0,0 +1,4 @@ + + + "Ez da ekintza hau kudeatzeko aplikazio bateragarririk aurkitu." + diff --git a/libraries/androidutils/src/main/res/values-fa/translations.xml b/libraries/androidutils/src/main/res/values-fa/translations.xml new file mode 100644 index 00000000000..b6212e73f3b --- /dev/null +++ b/libraries/androidutils/src/main/res/values-fa/translations.xml @@ -0,0 +1,4 @@ + + + "هیچ برنامه سازگاری برای انجام این عمل یافت نشد." + diff --git a/libraries/androidutils/src/main/res/values-lt/translations.xml b/libraries/androidutils/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..69e6db16fa4 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-lt/translations.xml @@ -0,0 +1,4 @@ + + + "Nerasta suderinamos programos, kuri galėtų atlikti šį veiksmą." + diff --git a/libraries/androidutils/src/main/res/values-tr/translations.xml b/libraries/androidutils/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..070ef9d3b43 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-tr/translations.xml @@ -0,0 +1,4 @@ + + + "Bu eylemi gerçekleştirecek uyumlu bir uygulama bulunamadı." + diff --git a/libraries/androidutils/src/main/res/values-ur/translations.xml b/libraries/androidutils/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..69f18dbf116 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-ur/translations.xml @@ -0,0 +1,4 @@ + + + "اس کارروائی کو سنبھالنے کے لیے کوئی مطابقت پذیر اطلاقیہ نہیں ملا۔" + diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt new file mode 100644 index 00000000000..69dcaa32d20 --- /dev/null +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt @@ -0,0 +1,124 @@ +/* + * 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.androidutils.text + +import android.telephony.TelephonyManager +import android.text.style.URLSpan +import androidx.core.text.getSpans +import androidx.core.text.toSpannable +import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.WarmUpRule +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import org.robolectric.shadow.api.Shadow.newInstanceOf + +@RunWith(RobolectricTestRunner::class) +class LinkifierHelperTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `linkification finds URL`() { + val text = "A url https://matrix.org" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("https://matrix.org") + } + + @Test + fun `linkification finds partial URL`() { + val text = "A partial url matrix.org/test" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("http://matrix.org/test") + } + + @Test + fun `linkification finds domain`() { + val text = "A domain matrix.org" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("http://matrix.org") + } + + @Test + fun `linkification finds email`() { + val text = "An email address john@doe.com" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("mailto:john@doe.com") + } + + @Test + @Config(sdk = [30]) + fun `linkification finds phone`() { + val text = "Test phone number +34950123456" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("tel:+34950123456") + } + + @Test + @Config(sdk = [30]) + fun `linkification finds phone in Germany`() { + // For some reason the linkification of phone numbers in Germany is very lenient and any number will fit here + val telephonyManager = shadowOf(newInstanceOf(TelephonyManager::class.java)) + telephonyManager.setSimCountryIso("DE") + + val text = "Test phone number 1234" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("tel:1234") + } + + @Test + fun `linkification handles trailing dot`() { + val text = "A url https://matrix.org." + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("https://matrix.org") + } + + @Test + fun `linkification handles trailing punctuation`() { + val text = "A url https://matrix.org!?; Check it out!" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("https://matrix.org") + } + + @Test + fun `linkification handles parenthesis surrounding URL`() { + val text = "A url (this one (https://github.com/element-hq/element-android/issues/1234))" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android/issues/1234") + } + + @Test + fun `linkification handles parenthesis in URL`() { + val text = "A url: (https://github.com/element-hq/element-android/READ(ME))" + val result = LinkifyHelper.linkify(text) + val urlSpans = result.toSpannable().getSpans() + assertThat(urlSpans.size).isEqualTo(1) + assertThat(urlSpans.first().url).isEqualTo("https://github.com/element-hq/element-android/READ(ME)") + } +} diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottlerTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottlerTest.kt new file mode 100644 index 00000000000..f33a3f237a8 --- /dev/null +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottlerTest.kt @@ -0,0 +1,32 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.androidutils.throttler + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class FirstThrottlerTest { + @Test + fun `throttle canHandle returns the expected result`() = runTest { + val throttler = FirstThrottler( + minimumInterval = 300, + coroutineScope = backgroundScope, + ) + assertThat(throttler.canHandle()).isTrue() + assertThat(throttler.canHandle()).isFalse() + advanceTimeBy(200) + assertThat(throttler.canHandle()).isFalse() + advanceTimeBy(110) + assertThat(throttler.canHandle()).isTrue() + } +} diff --git a/libraries/architecture/build.gradle.kts b/libraries/architecture/build.gradle.kts index 31c3d60757e..8db06089f3b 100644 --- a/libraries/architecture/build.gradle.kts +++ b/libraries/architecture/build.gradle.kts @@ -15,6 +15,7 @@ android { dependencies { api(projects.libraries.di) + api(projects.libraries.core) api(libs.dagger) api(libs.appyx.core) api(libs.androidx.lifecycle.runtime) diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt index 6c4d286c2fd..f080b64d015 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.architecture import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import io.element.android.libraries.core.extensions.runCatchingExceptions import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -90,7 +91,7 @@ suspend inline fun MutableState>.runCatchingUpdatingState( state = this, errorTransform = errorTransform, resultBlock = { - runCatching { + runCatchingExceptions { block() } }, @@ -103,7 +104,7 @@ suspend inline fun (suspend () -> T).runCatchingUpdatingState( state = state, errorTransform = errorTransform, resultBlock = { - runCatching { + runCatchingExceptions { this() } }, diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt index c425597ba75..b7f22cbe233 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.architecture import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import io.element.android.libraries.core.extensions.runCatchingExceptions import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -93,7 +94,7 @@ suspend inline fun MutableState>.runCatchingUpdatingState( state = this, errorTransform = errorTransform, resultBlock = { - runCatching { + runCatchingExceptions { block() } }, @@ -106,7 +107,7 @@ suspend inline fun (suspend () -> T).runCatchingUpdatingState( state = state, errorTransform = errorTransform, resultBlock = { - runCatching { + runCatchingExceptions { this() } }, diff --git a/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncDataKtTest.kt b/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncDataKtTest.kt index 1818e3e99e2..1551360da2b 100644 --- a/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncDataKtTest.kt +++ b/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncDataKtTest.kt @@ -38,15 +38,15 @@ class AsyncDataKtTest { val result = runUpdatingState(state) { delay(1) - Result.failure(MyThrowable("hello")) + Result.failure(MyException("hello")) } assertThat(result.isFailure).isTrue() - assertThat(result.exceptionOrNull()).isEqualTo(MyThrowable("hello")) + assertThat(result.exceptionOrNull()).isEqualTo(MyException("hello")) assertThat(state.popFirst()).isEqualTo(AsyncData.Uninitialized) assertThat(state.popFirst()).isEqualTo(AsyncData.Loading(null)) - assertThat(state.popFirst()).isEqualTo(AsyncData.Failure(MyThrowable("hello"))) + assertThat(state.popFirst()).isEqualTo(AsyncData.Failure(MyException("hello"))) state.assertNoMoreValues() } @@ -54,17 +54,17 @@ class AsyncDataKtTest { fun `updates state when block returns failure transforming the error`() = runTest { val state = TestableMutableState>(AsyncData.Uninitialized) - val result = runUpdatingState(state, { MyThrowable(it.message + " world") }) { + val result = runUpdatingState(state, { MyException(it.message + " world") }) { delay(1) - Result.failure(MyThrowable("hello")) + Result.failure(MyException("hello")) } assertThat(result.isFailure).isTrue() - assertThat(result.exceptionOrNull()).isEqualTo(MyThrowable("hello world")) + assertThat(result.exceptionOrNull()).isEqualTo(MyException("hello world")) assertThat(state.popFirst()).isEqualTo(AsyncData.Uninitialized) assertThat(state.popFirst()).isEqualTo(AsyncData.Loading(null)) - assertThat(state.popFirst()).isEqualTo(AsyncData.Failure(MyThrowable("hello world"))) + assertThat(state.popFirst()).isEqualTo(AsyncData.Failure(MyException("hello world"))) state.assertNoMoreValues() } } @@ -101,4 +101,4 @@ private class TestableMutableState( /** * An exception that is also a data class so we can compare it using equals. */ -private data class MyThrowable(val myMessage: String) : Throwable(myMessage) +private data class MyException(val myMessage: String) : Exception(myMessage) diff --git a/libraries/audio/api/build.gradle.kts b/libraries/audio/api/build.gradle.kts new file mode 100644 index 00000000000..3a827637872 --- /dev/null +++ b/libraries/audio/api/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * 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. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.audio.api" +} diff --git a/libraries/audio/api/src/main/kotlin/io/element/android/libraries/audio/api/AudioFocus.kt b/libraries/audio/api/src/main/kotlin/io/element/android/libraries/audio/api/AudioFocus.kt new file mode 100644 index 00000000000..c363114856d --- /dev/null +++ b/libraries/audio/api/src/main/kotlin/io/element/android/libraries/audio/api/AudioFocus.kt @@ -0,0 +1,32 @@ +/* + * 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.audio.api + +enum class AudioFocusRequester { + ElementCall, + VoiceMessage, + MediaViewer, +} + +interface AudioFocus { + /** + * Request audio focus for the given requester. + * @param requester The mode for which to request audio focus. + * @param onFocusLost Callback to be invoked when the audio focus is lost. + * @return true if the audio focus was successfully requested, false otherwise. + */ + fun requestAudioFocus( + requester: AudioFocusRequester, + onFocusLost: () -> Unit, + ) + + /** + * Release the audio focus. + */ + fun releaseAudioFocus() +} diff --git a/libraries/audio/impl/build.gradle.kts b/libraries/audio/impl/build.gradle.kts new file mode 100644 index 00000000000..61a4cd7ebae --- /dev/null +++ b/libraries/audio/impl/build.gradle.kts @@ -0,0 +1,25 @@ +import extension.setupAnvil + +/* + * 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. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.audio.impl" +} + +setupAnvil() + +dependencies { + api(projects.libraries.audio.api) + + implementation(libs.androidx.corektx) + implementation(libs.dagger) + implementation(projects.libraries.di) +} 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 new file mode 100644 index 00000000000..057339d60c4 --- /dev/null +++ b/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt @@ -0,0 +1,106 @@ +/* + * 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.audio.impl + +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.os.Build +import androidx.core.content.getSystemService +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.audio.api.AudioFocus +import io.element.android.libraries.audio.api.AudioFocusRequester +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultAudioFocus @Inject constructor( + @ApplicationContext private val context: Context, +) : AudioFocus { + private val audioManager = requireNotNull(context.getSystemService()) + + private var audioFocusRequest: AudioFocusRequest? = null + private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null + + @Suppress("DEPRECATION") + override fun requestAudioFocus( + requester: AudioFocusRequester, + onFocusLost: () -> Unit, + ) { + val listener = AudioManager.OnAudioFocusChangeListener { + when (it) { + AudioManager.AUDIOFOCUS_GAIN -> { + // Do nothing + } + AudioManager.AUDIOFOCUS_LOSS, + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + onFocusLost() + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val audioAttributes = AudioAttributes.Builder() + .setUsage(requester.toAudioUsage()) + .build() + val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setAudioAttributes(audioAttributes) + .setOnAudioFocusChangeListener(listener) + .setWillPauseWhenDucked(requester.willPausedWhenDucked()) + .build() + audioManager.requestAudioFocus(request) + audioFocusRequest = request + } else { + audioManager.requestAudioFocus( + listener, + requester.toAudioStream(), + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, + ) + audioFocusChangeListener = listener + } + } + + @Suppress("DEPRECATION") + override fun releaseAudioFocus() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + audioFocusRequest?.let { audioManager.abandonAudioFocusRequest(it) } + } else { + audioFocusChangeListener?.let { audioManager.abandonAudioFocus(it) } + } + } +} + +private fun AudioFocusRequester.toAudioUsage(): Int { + return when (this) { + AudioFocusRequester.ElementCall, + AudioFocusRequester.VoiceMessage -> AudioAttributes.USAGE_VOICE_COMMUNICATION + AudioFocusRequester.MediaViewer -> AudioAttributes.USAGE_MEDIA + } +} + +private fun AudioFocusRequester.toAudioStream(): Int { + return when (this) { + AudioFocusRequester.ElementCall, + AudioFocusRequester.VoiceMessage -> AudioManager.STREAM_VOICE_CALL + AudioFocusRequester.MediaViewer -> AudioManager.STREAM_MUSIC + } +} + +private fun AudioFocusRequester.willPausedWhenDucked(): Boolean { + return when (this) { + // (note that for Element Call, there is no action when the focus is lost) + AudioFocusRequester.ElementCall, + AudioFocusRequester.VoiceMessage -> true + // For the MediaViewer, we let the system automatically handle the ducking + // https://developer.android.com/media/optimize/audio-focus#automatic-ducking + AudioFocusRequester.MediaViewer -> false + } +} diff --git a/libraries/audio/test/build.gradle.kts b/libraries/audio/test/build.gradle.kts new file mode 100644 index 00000000000..57ffffbee7f --- /dev/null +++ b/libraries/audio/test/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.audio.test" +} + +dependencies { + api(projects.libraries.audio.api) + implementation(projects.tests.testutils) +} diff --git a/libraries/audio/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeAudioFocus.kt b/libraries/audio/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeAudioFocus.kt new file mode 100644 index 00000000000..bb628e4cf99 --- /dev/null +++ b/libraries/audio/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeAudioFocus.kt @@ -0,0 +1,28 @@ +/* + * 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.mediaplayer.test + +import io.element.android.libraries.audio.api.AudioFocus +import io.element.android.libraries.audio.api.AudioFocusRequester +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeAudioFocus( + private val requestAudioFocusResult: (AudioFocusRequester, () -> Unit) -> Unit = { _, _ -> lambdaError() }, + private val releaseAudioFocusResult: () -> Unit = { lambdaError() }, +) : AudioFocus { + override fun requestAudioFocus( + requester: AudioFocusRequester, + onFocusLost: () -> Unit, + ) { + requestAudioFocusResult(requester, onFocusLost) + } + + override fun releaseAudioFocus() { + releaseAudioFocusResult() + } +} diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/SuspendLazy.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/SuspendLazy.kt new file mode 100644 index 00000000000..e5442c49407 --- /dev/null +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/SuspendLazy.kt @@ -0,0 +1,25 @@ +/* + * 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.core.coroutine + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +fun suspendLazy(coroutineContext: CoroutineContext = EmptyCoroutineContext, block: suspend () -> T): Lazy> { + return lazy(LazyThreadSafetyMode.NONE) { + val deferred = CompletableDeferred() + CoroutineScope(coroutineContext).launch { + deferred.complete(block()) + } + deferred + } +} diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt index 222dcb8f30a..0610fa04e4d 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt @@ -7,11 +7,20 @@ package io.element.android.libraries.core.data -inline fun tryOrNull(onError: ((Throwable) -> Unit) = { }, operation: () -> A): A? { +import kotlin.coroutines.cancellation.CancellationException + +/** + * Can be used to catch [Exception]s in a block of code, returning `null` if an exception occurs. + * + * If the block throws a [CancellationException], it will be rethrown. + */ +inline fun tryOrNull(onException: ((Exception) -> Unit) = { }, operation: () -> A): A? { return try { operation() - } catch (any: Throwable) { - onError.invoke(any) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + onException.invoke(e) null } } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt index c6091c5ffe2..abd6e10d1ff 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt @@ -89,3 +89,31 @@ fun String.withoutAccents(): String { return Normalizer.normalize(this, Normalizer.Form.NFD) .replace("\\p{Mn}+".toRegex(), "") } + +private const val RTL_OVERRIDE_CHAR = '\u202E' +private const val LTR_OVERRIDE_CHAR = '\u202D' + +fun String.ensureEndsLeftToRight() = if (containsRtLOverride()) "$this$LTR_OVERRIDE_CHAR" else this + +fun String.containsRtLOverride() = contains(RTL_OVERRIDE_CHAR) + +fun String.filterDirectionOverrides() = filterNot { it == RTL_OVERRIDE_CHAR || it == LTR_OVERRIDE_CHAR } + +/** + * This works around https://github.com/element-hq/element-x-android/issues/2105. + * @param maxLength Max characters to retrieve. Defaults to `500`. + * @param ellipsize Whether to add an ellipsis (`…`) char at the end or not. Defaults to `false`. + * @return The string truncated to [maxLength] characters, with an optional ellipsis if larger. + */ +fun String.toSafeLength( + maxLength: Int = 500, + ellipsize: Boolean = false, +): String { + return if (ellipsize) { + ellipsize(maxLength) + } else if (length > maxLength) { + take(maxLength) + } else { + this + } +} diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt index 3524a579b6d..e46f37cffbb 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt @@ -7,6 +7,61 @@ package io.element.android.libraries.core.extensions +import kotlin.coroutines.cancellation.CancellationException + +/** + * Can be used to catch exceptions in a block of code and return a [Result]. + * If the block throws a [CancellationException], it will be rethrown. + * If it throws any other exception, it will be wrapped in a [Result.failure]. + * + * [Error]s are not caught by this function, as they are not meant to be caught in normal application flow. + */ +inline fun runCatchingExceptions( + block: () -> T +): Result { + return try { + Result.success(block()) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.failure(e) + } +} + +/** + * Can be used to catch exceptions in a block of code and return a [Result]. + * If the block throws a [CancellationException], it will be rethrown. + * If it throws any other exception, it will be wrapped in a [Result.failure]. + * + * [Error]s are not caught by this function, as they are not meant to be caught in normal application flow. + */ +inline fun T.runCatchingExceptions( + block: T.() -> R +): Result { + return try { + Result.success(block()) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.failure(e) + } +} + +/** + * Can be used to transform a [Result] into another [Result] by applying a [block] to the value if it is successful. + * If the original [Result] is a failure, the exception will be wrapped in a new [Result.failure]. + * + * This is a safer version of [Result.mapCatching]. + */ +inline fun Result.mapCatchingExceptions( + block: (T) -> R, +): Result { + return fold( + onSuccess = { value -> runCatchingExceptions { block(value) } }, + onFailure = { exception -> Result.failure(exception) } + ) +} + /** * Can be used to transform some Throwable into some other. */ @@ -33,12 +88,16 @@ inline fun Result.flatMap(transform: (T) -> Result): Result { * @return The result of the transform or a caught exception wrapped in a [Result]. */ inline fun Result.flatMapCatching(transform: (T) -> Result): Result { - return mapCatching(transform).fold( + return mapCatchingExceptions(transform).fold( onSuccess = { it }, onFailure = { Result.failure(it) } ) } +/** + * Can be used to execute a block of code after the [Result] has been processed, regardless of whether it was successful or not. + * The block receives the exception if there was one, or `null` if the result was successful. + */ inline fun Result.finally(block: (exception: Throwable?) -> Unit): Result { onSuccess { block(null) } onFailure(block) diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt index 82c1cb2c716..357fce2febe 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt @@ -24,6 +24,7 @@ object MimeTypes { const val Jpeg = "image/jpeg" const val Gif = "image/gif" const val WebP = "image/webp" + const val Svg = "image/svg+xml" const val Videos = "video/*" const val Mp4 = "video/mp4" diff --git a/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/BasicExtensionsTest.kt b/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/BasicExtensionsTest.kt index a31b24adb7b..b454412fb7f 100644 --- a/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/BasicExtensionsTest.kt +++ b/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/BasicExtensionsTest.kt @@ -8,6 +8,8 @@ package io.element.android.libraries.core.extensions import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class BasicExtensionsTest { @@ -43,4 +45,32 @@ class BasicExtensionsTest { val output = input.ellipsize(5) assertEquals(input, output) } + + @Test + fun `given text with RtL unicode override, when checking contains RtL Override, then returns true`() { + val textWithRtlOverride = "hello\u202Eworld" + val result = textWithRtlOverride.containsRtLOverride() + assertTrue(result) + } + + @Test + fun `given text without RtL unicode override, when checking contains RtL Override, then returns false`() { + val textWithRtlOverride = "hello world" + val result = textWithRtlOverride.containsRtLOverride() + assertFalse(result) + } + + @Test + fun `given text with RtL unicode override, when ensuring ends LtR, then appends a LtR unicode override`() { + val textWithRtlOverride = "123\u202E456" + val result = textWithRtlOverride.ensureEndsLeftToRight() + assertEquals("$textWithRtlOverride\u202D", result) + } + + @Test + fun `given text with unicode direction overrides, when filtering direction overrides, then removes all overrides`() { + val textWithDirectionOverrides = "123\u202E456\u202d789" + val result = textWithDirectionOverrides.filterDirectionOverrides() + assertEquals("123456789", result) + } } diff --git a/libraries/dateformatter/impl/build.gradle.kts b/libraries/dateformatter/impl/build.gradle.kts index 4a619af5b7a..ed8c7731bdd 100644 --- a/libraries/dateformatter/impl/build.gradle.kts +++ b/libraries/dateformatter/impl/build.gradle.kts @@ -22,6 +22,14 @@ android { } } + kotlin { + compilerOptions { + optIn = listOf( + "kotlin.time.ExperimentalTime" + ) + } + } + dependencies { implementation(libs.dagger) implementation(projects.libraries.core) diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt index ecf5399302c..bc8d8aedc0f 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt @@ -33,8 +33,8 @@ class DefaultDateFormatterDay @Inject constructor( return if (useRelative) { val dayDiff = today.date.toEpochDays() - dateToFormat.date.toEpochDays() when (dayDiff) { - 0 -> dateFormatters.getRelativeDay(timestamp, "Today") - 1 -> dateFormatters.getRelativeDay(timestamp, "Yesterday") + 0L -> dateFormatters.getRelativeDay(timestamp, "Today") + 1L -> dateFormatters.getRelativeDay(timestamp, "Yesterday") else -> if (dayDiff < 7) { dateFormatters.formatDateWithDay(dateToFormat) } else { diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt index d44c6d5c355..9e5ffd6afec 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.dateformatter.impl import android.text.format.DateUtils import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn -import kotlinx.datetime.Clock import kotlinx.datetime.LocalDateTime import kotlinx.datetime.toInstant import kotlinx.datetime.toJavaLocalDate @@ -20,6 +19,7 @@ import java.time.Period import java.util.Locale import javax.inject.Inject import kotlin.math.absoluteValue +import kotlin.time.Clock @SingleIn(AppScope::class) class DateFormatters @Inject constructor( diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt index c2cd062082a..20862fd68a7 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt @@ -7,11 +7,11 @@ package io.element.android.libraries.dateformatter.impl -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime import kotlinx.datetime.toLocalDateTime import javax.inject.Inject +import kotlin.time.Clock +import kotlin.time.Instant class LocalDateTimeProvider @Inject constructor( private val clock: Clock, diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt index 1fe22483a41..a7da8ea6cec 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt @@ -12,9 +12,9 @@ import dagger.Module import dagger.Provides import io.element.android.libraries.dateformatter.impl.TimezoneProvider import io.element.android.libraries.di.AppScope -import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import java.util.Locale +import kotlin.time.Clock @Module @ContributesTo(AppScope::class) diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt index b88badcc988..79b1022c14a 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt @@ -28,7 +28,7 @@ import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.allBooleans -import kotlinx.datetime.Instant +import kotlin.time.Instant @Preview @Composable diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt index 88afcb554fb..d0c25f8b603 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt @@ -16,9 +16,9 @@ import io.element.android.libraries.dateformatter.impl.DateFormatters import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter import io.element.android.libraries.dateformatter.impl.DefaultDateFormatterDay import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider -import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import java.util.Locale +import kotlin.time.Instant /** * Create DefaultDateFormatter and set current time to the provided date. diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt index a86fb044555..8151e072629 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt @@ -7,8 +7,8 @@ package io.element.android.libraries.dateformatter.impl.previews -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant +import kotlin.time.Clock +import kotlin.time.Instant class PreviewClock : Clock { private var instant: Instant = Instant.fromEpochMilliseconds(0) diff --git a/libraries/dateformatter/impl/src/main/res/values-bg/translations.xml b/libraries/dateformatter/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 00000000000..b03020fec2d --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,4 @@ + + + "Този месец" + diff --git a/libraries/dateformatter/impl/src/main/res/values-cy/translations.xml b/libraries/dateformatter/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..0535f86e7b4 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s am %2$s" + "Y mis hwn" + diff --git a/libraries/dateformatter/impl/src/main/res/values-da/translations.xml b/libraries/dateformatter/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..b74fc5a8a00 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s kl. %2$s" + "Denne måned" + diff --git a/libraries/dateformatter/impl/src/main/res/values-de/translations.xml b/libraries/dateformatter/impl/src/main/res/values-de/translations.xml index f09a5ce4b8e..e98c51e8514 100644 --- a/libraries/dateformatter/impl/src/main/res/values-de/translations.xml +++ b/libraries/dateformatter/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,5 @@ - "%1$sbei %2$s" + "%1$s um %2$s" "Diesen Monat" diff --git a/libraries/dateformatter/impl/src/main/res/values-el/translations.xml b/libraries/dateformatter/impl/src/main/res/values-el/translations.xml index 3b337d1e290..63df4a0070e 100644 --- a/libraries/dateformatter/impl/src/main/res/values-el/translations.xml +++ b/libraries/dateformatter/impl/src/main/res/values-el/translations.xml @@ -1,4 +1,5 @@ + "%1$s στις %2$s" "Αυτό το μήνα" diff --git a/libraries/dateformatter/impl/src/main/res/values-es/translations.xml b/libraries/dateformatter/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..04d9da0d3be --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s a las %2$s" + "Este mes" + diff --git a/libraries/dateformatter/impl/src/main/res/values-eu/translations.xml b/libraries/dateformatter/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..5e690a750ed --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,4 @@ + + + "Hilabete hau" + diff --git a/libraries/dateformatter/impl/src/main/res/values-fa/translations.xml b/libraries/dateformatter/impl/src/main/res/values-fa/translations.xml new file mode 100644 index 00000000000..da6566ef3ae --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s در %2$s" + "این ماه" + diff --git a/libraries/dateformatter/impl/src/main/res/values-in/translations.xml b/libraries/dateformatter/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..4771c4e658d --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s pada %2$s" + "Bulan ini" + diff --git a/libraries/dateformatter/impl/src/main/res/values-nb/translations.xml b/libraries/dateformatter/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..a31d53cb43a --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s kl. %2$s" + "Denne måneden" + diff --git a/libraries/dateformatter/impl/src/main/res/values-pt-rBR/translations.xml b/libraries/dateformatter/impl/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..7c72997e333 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s às %2$s" + "Este mês" + diff --git a/libraries/dateformatter/impl/src/main/res/values-pt/translations.xml b/libraries/dateformatter/impl/src/main/res/values-pt/translations.xml new file mode 100644 index 00000000000..7c72997e333 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-pt/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s às %2$s" + "Este mês" + diff --git a/libraries/dateformatter/impl/src/main/res/values-sv/translations.xml b/libraries/dateformatter/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 00000000000..58f22f89ab2 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,5 @@ + + + "%1$svid %2$s" + "Denna månad" + diff --git a/libraries/dateformatter/impl/src/main/res/values-tr/translations.xml b/libraries/dateformatter/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..5276e6c91f9 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s %2$s" + "Bu ay" + diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt index 355ab6815c0..0e52e1de373 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt @@ -7,16 +7,17 @@ package io.element.android.libraries.dateformatter.impl +import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.libraries.dateformatter.api.DateFormatterMode -import kotlinx.datetime.Instant import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config +import kotlin.time.Instant @RunWith(AndroidJUnit4::class) -@Config(qualifiers = "fr") +@Config(qualifiers = "fr", sdk = [Build.VERSION_CODES.TIRAMISU]) class DefaultDateFormatterFrTest { @Test fun `test null`() { @@ -34,7 +35,7 @@ class DefaultDateFormatterFrTest { assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("1 janvier 1970 à 00:00") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Janvier 1970") assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("1 janvier 1970") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01/01/1970") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("00:00") } @@ -46,7 +47,7 @@ class DefaultDateFormatterFrTest { assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("1 janvier 1970 à 00:00") assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Janvier 1970") assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("1 janvier 1970") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01/01/1970") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("00:00") } @@ -241,7 +242,7 @@ class DefaultDateFormatterFrTest { assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1979 à 18:35") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1979") assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("6 avril 1979") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06/04/1979") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35") } @@ -254,7 +255,7 @@ class DefaultDateFormatterFrTest { assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6 avril 1979 à 18:35") assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Avril 1979") assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("6 avril 1979") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06/04/1979") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35") } } diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt index 27ae9c3238a..136bbc12930 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt @@ -7,16 +7,17 @@ package io.element.android.libraries.dateformatter.impl +import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.libraries.dateformatter.api.DateFormatterMode -import kotlinx.datetime.Instant import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config +import kotlin.time.Instant @RunWith(AndroidJUnit4::class) -@Config(qualifiers = "en") +@Config(qualifiers = "en", sdk = [Build.VERSION_CODES.TIRAMISU]) class DefaultDateFormatterTest { @Test fun `test null`() { @@ -34,7 +35,7 @@ class DefaultDateFormatterTest { assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("January 1, 1970 at 12:00 AM") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("January 1970") assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("January 1, 1970") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01/01/1970") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("12:00 AM") } @@ -46,7 +47,7 @@ class DefaultDateFormatterTest { assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("January 1, 1970 at 12:00 AM") assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("January 1970") assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("January 1, 1970") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01/01/1970") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("12:00 AM") } @@ -58,7 +59,7 @@ class DefaultDateFormatterTest { val formatter = createFormatter(now) assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980") - assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April") + assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday, April 6") assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM") } @@ -84,7 +85,7 @@ class DefaultDateFormatterTest { val formatter = createFormatter(now) assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980") - assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April") + assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday, April 6") assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM") } @@ -110,7 +111,7 @@ class DefaultDateFormatterTest { val formatter = createFormatter(now) assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:34 PM") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980") - assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April") + assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday, April 6") assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:34 PM") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:34 PM") } @@ -136,7 +137,7 @@ class DefaultDateFormatterTest { val formatter = createFormatter(now) assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 5:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980") - assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April") + assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday, April 6") assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5:35 PM") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("5:35 PM") } @@ -162,8 +163,8 @@ class DefaultDateFormatterTest { val formatter = createFormatter(now) assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 5, 1980 at 6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980") - assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Saturday 5 April") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5 Apr") + assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Saturday, April 5") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("Apr 5") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM") } @@ -188,8 +189,8 @@ class DefaultDateFormatterTest { val formatter = createFormatter(now) assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 4, 1980 at 6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980") - assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Friday 4 April") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("4 Apr") + assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Friday, April 4") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("Apr 4") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM") } @@ -202,7 +203,7 @@ class DefaultDateFormatterTest { assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Friday at 6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month") assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Friday") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("4 Apr") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Apr 4") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM") } @@ -214,8 +215,8 @@ class DefaultDateFormatterTest { val formatter = createFormatter(now) assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("March 6, 1980 at 6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("March 1980") - assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Thursday 6 March") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 Mar") + assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Thursday, March 6") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("Mar 6") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM") } @@ -225,10 +226,10 @@ class DefaultDateFormatterTest { val dat = "1980-03-06T18:35:24.00Z" val ts = Instant.parse(dat).toEpochMilliseconds() val formatter = createFormatter(now) - assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Thursday 6 March at 6:35 PM") + assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Thursday, March 6 at 6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("March 1980") - assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Thursday 6 March") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 Mar") + assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Thursday, March 6") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Mar 6") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM") } @@ -241,7 +242,7 @@ class DefaultDateFormatterTest { assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1979 at 6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1979") assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("April 6, 1979") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("04/06/1979") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM") } @@ -254,7 +255,7 @@ class DefaultDateFormatterTest { assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("April 6, 1979 at 6:35 PM") assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("April 1979") assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("April 6, 1979") - assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979") + assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("04/06/1979") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM") } } diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt index 6ab588022d3..20f36a0dd65 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt @@ -8,9 +8,9 @@ package io.element.android.libraries.dateformatter.impl import io.element.android.tests.testutils.InstrumentationStringProvider -import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import java.util.Locale +import kotlin.time.Instant /** * Create DefaultDateFormatter and set current time to the provided date. diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt index 047e87e62ab..c3d1f01d4f9 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt @@ -7,8 +7,8 @@ package io.element.android.libraries.dateformatter.impl -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant +import kotlin.time.Clock +import kotlin.time.Instant class FakeClock : Clock { private var instant: Instant = Instant.fromEpochMilliseconds(0) diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 01e8d56bb04..d95566ebb5e 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -31,7 +31,10 @@ android { implementation(libs.androidx.compose.material3.adaptive) implementation(libs.coil.compose) implementation(libs.vanniktech.blurhash) + implementation(projects.features.enterprise.api) + implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) + implementation(projects.libraries.core) 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/ColorUtil.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ColorUtil.kt index 317abe76ba4..debcd57c67e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ColorUtil.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ColorUtil.kt @@ -7,24 +7,42 @@ package io.element.android.libraries.designsystem -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import io.element.android.compound.theme.ElementTheme @Composable fun Boolean.toEnabledColor(): Color { return if (this) { - MaterialTheme.colorScheme.primary + ElementTheme.colors.textPrimary } else { - MaterialTheme.colorScheme.primary.copy(alpha = 0.40f) + ElementTheme.colors.textDisabled } } @Composable fun Boolean.toSecondaryEnabledColor(): Color { return if (this) { - MaterialTheme.colorScheme.secondary + ElementTheme.colors.textSecondary } else { - MaterialTheme.colorScheme.secondary.copy(alpha = 0.40f) + ElementTheme.colors.textDisabled + } +} + +@Composable +fun Boolean.toIconEnabledColor(): Color { + return if (this) { + ElementTheme.colors.iconPrimary + } else { + ElementTheme.colors.iconDisabled + } +} + +@Composable +fun Boolean.toIconSecondaryEnabledColor(): Color { + return if (this) { + ElementTheme.colors.iconSecondary + } else { + ElementTheme.colors.iconDisabled } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt index 32b96b62604..483c9aa3465 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt @@ -17,7 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -36,18 +36,21 @@ private const val MAX_COUNT_STRING = "+$MAX_COUNT" * @param count The number to display. If the number is greater than [MAX_COUNT], the counter will display [MAX_COUNT_STRING]. * If the number is less than 1, the counter will not be displayed. * @param modifier The modifier to apply to this layout. + * @param textStyle The style to apply to the text inside the counter. + * @param isCritical If true, the counter will use a critical color scheme, otherwise it will use an accent color scheme. */ @Composable fun CounterAtom( count: Int, modifier: Modifier = Modifier, + textStyle: TextStyle = CounterAtomDefaults.textStyle, + isCritical: Boolean = false, ) { if (count < 1) return val countAsText = when (count) { in 0..MAX_COUNT -> count.toString() else -> MAX_COUNT_STRING } - val textStyle = ElementTheme.typography.fontBodyMdMedium val textMeasurer = rememberTextMeasurer() // Measure the maximum count string size val textLayoutResult = textMeasurer.measure( @@ -58,19 +61,30 @@ fun CounterAtom( val squareSize = maxOf(textSize.width, textSize.height) Box( modifier = modifier - .size(squareSize.toDp() + 1.dp) - .clip(CircleShape) - .background(ElementTheme.colors.iconSuccessPrimary) + .size(squareSize.toDp() + 1.dp) + .clip(CircleShape) + .background( + if (isCritical) { + ElementTheme.colors.iconCriticalPrimary + } else { + ElementTheme.colors.iconAccentPrimary + } + ) ) { Text( modifier = Modifier.align(Alignment.Center), text = countAsText, style = textStyle, - color = Color.White, + color = ElementTheme.colors.textOnSolidPrimary, ) } } +object CounterAtomDefaults { + val textStyle: TextStyle + @Composable get() = ElementTheme.typography.fontBodyMdMedium +} + @PreviewsDayNight @Composable internal fun CounterAtomPreview() = ElementPreview { @@ -79,5 +93,6 @@ internal fun CounterAtomPreview() = ElementPreview { CounterAtom(count = 4) CounterAtom(count = 99) CounterAtom(count = 100) + CounterAtom(count = 4, isCritical = true) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index f8b2a265d33..d66204afc45 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.designsystem.atomic.atoms import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -38,7 +37,7 @@ fun ElementLogoAtom( size: ElementLogoAtomSize, modifier: Modifier = Modifier, useBlurredShadow: Boolean = canUseBlurMaskFilter(), - darkTheme: Boolean = isSystemInDarkTheme(), + darkTheme: Boolean = ElementTheme.isLightTheme.not(), ) { val blur = if (darkTheme) 160.dp else 24.dp val shadowColor = if (darkTheme) size.shadowColorDark else size.shadowColorLight diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/MatrixBadgeAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/MatrixBadgeAtom.kt index 6437c0a822d..51507064eb8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/MatrixBadgeAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/MatrixBadgeAtom.kt @@ -18,8 +18,6 @@ import io.element.android.libraries.designsystem.theme.badgeNegativeBackgroundCo import io.element.android.libraries.designsystem.theme.badgeNegativeContentColor import io.element.android.libraries.designsystem.theme.badgeNeutralBackgroundColor import io.element.android.libraries.designsystem.theme.badgeNeutralContentColor -import io.element.android.libraries.designsystem.theme.badgePositiveBackgroundColor -import io.element.android.libraries.designsystem.theme.badgePositiveContentColor object MatrixBadgeAtom { data class MatrixBadgeData( @@ -31,7 +29,8 @@ object MatrixBadgeAtom { enum class Type { Positive, Neutral, - Negative + Negative, + Info, } @Composable @@ -39,19 +38,22 @@ object MatrixBadgeAtom { data: MatrixBadgeData, ) { val backgroundColor = when (data.type) { - Type.Positive -> ElementTheme.colors.badgePositiveBackgroundColor + Type.Positive -> ElementTheme.colors.bgBadgeAccent Type.Neutral -> ElementTheme.colors.badgeNeutralBackgroundColor Type.Negative -> ElementTheme.colors.badgeNegativeBackgroundColor + Type.Info -> ElementTheme.colors.bgBadgeInfo } val textColor = when (data.type) { - Type.Positive -> ElementTheme.colors.badgePositiveContentColor + Type.Positive -> ElementTheme.colors.textBadgeAccent Type.Neutral -> ElementTheme.colors.badgeNeutralContentColor Type.Negative -> ElementTheme.colors.badgeNegativeContentColor + Type.Info -> ElementTheme.colors.textBadgeInfo } val iconColor = when (data.type) { - Type.Positive -> ElementTheme.colors.iconSuccessPrimary + Type.Positive -> ElementTheme.colors.textBadgeAccent Type.Neutral -> ElementTheme.colors.iconSecondary Type.Negative -> ElementTheme.colors.iconCriticalPrimary + Type.Info -> ElementTheme.colors.textBadgeInfo } Badge( text = data.text, @@ -93,8 +95,20 @@ internal fun MatrixBadgeAtomNegativePreview() = ElementPreview { MatrixBadgeAtom.View( MatrixBadgeAtom.MatrixBadgeData( text = "Not trusted", - icon = CompoundIcons.Error(), + icon = CompoundIcons.ErrorSolid(), type = MatrixBadgeAtom.Type.Negative, ) ) } + +@PreviewsDayNight +@Composable +internal fun MatrixBadgeAtomInfoPreview() = ElementPreview { + MatrixBadgeAtom.View( + MatrixBadgeAtom.MatrixBadgeData( + text = "Not encrypted", + icon = CompoundIcons.LockOff(), + type = MatrixBadgeAtom.Type.Info, + ) + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RedIndicatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RedIndicatorAtom.kt index 86537f14354..354eada12ae 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RedIndicatorAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RedIndicatorAtom.kt @@ -33,7 +33,7 @@ fun RedIndicatorAtom( Box( modifier = modifier .size(size) - .border(borderSize, ElementTheme.materialColors.background, CircleShape) + .border(borderSize, ElementTheme.colors.bgCanvasDefault, CircleShape) .padding(borderSize / 2) .clip(CircleShape) .background(color) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt index 7816a59e488..71dbd93ec56 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt @@ -13,9 +13,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Home -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -24,6 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon @@ -45,7 +43,7 @@ fun RoundedIconAtom( size: RoundedIconAtomSize = RoundedIconAtomSize.Big, resourceId: Int? = null, imageVector: ImageVector? = null, - tint: Color = MaterialTheme.colorScheme.secondary, + tint: Color = ElementTheme.colors.iconSecondary, backgroundTint: Color = ElementTheme.colors.temporaryColorBgSpecial, ) { Box( @@ -95,11 +93,11 @@ internal fun RoundedIconAtomPreview() = ElementPreview { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { RoundedIconAtom( size = RoundedIconAtomSize.Medium, - imageVector = Icons.Filled.Home, + imageVector = CompoundIcons.HomeSolid(), ) RoundedIconAtom( size = RoundedIconAtomSize.Big, - imageVector = Icons.Filled.Home, + imageVector = CompoundIcons.HomeSolid(), ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt index 30944ead690..07e2454dbcc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt @@ -27,6 +27,7 @@ import io.element.android.compound.theme.ElementTheme 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.anAvatarData import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -39,7 +40,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun ComposerAlertMolecule( - avatar: AvatarData, + avatar: AvatarData?, content: AnnotatedString, onSubmitClick: () -> Unit, modifier: Modifier = Modifier, @@ -58,7 +59,7 @@ fun ComposerAlertMolecule( ) val startColor = if (isCritical) ElementTheme.colors.bgCriticalSubtle else ElementTheme.colors.bgInfoSubtle val brush = Brush.verticalGradient( - listOf(startColor, ElementTheme.materialColors.background), + listOf(startColor, ElementTheme.colors.bgCanvasDefault), ) Box( modifier = Modifier @@ -71,14 +72,21 @@ fun ComposerAlertMolecule( Row( horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - Avatar( - avatarData = avatar, - ) + if (avatar != null) { + Avatar( + avatarData = avatar, + avatarType = AvatarType.User, + ) + } Text( text = content, modifier = Modifier.weight(1f), style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textPrimary, + color = if (isCritical) { + ElementTheme.colors.textCriticalPrimary + } else { + ElementTheme.colors.textPrimary + }, textAlign = TextAlign.Start, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt index d07f47b43cf..a89a21adbd6 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt @@ -11,10 +11,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -48,10 +49,13 @@ fun IconTitleSubtitleMolecule( Text( text = title, modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .semantics { + heading() + }, textAlign = TextAlign.Center, style = ElementTheme.typography.fontHeadingMdBold, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, ) if (subTitle != null) { Spacer(Modifier.height(8.dp)) @@ -60,7 +64,7 @@ fun IconTitleSubtitleMolecule( modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt index 2eb2f133e15..0b3ca7fd8bc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt @@ -8,7 +8,6 @@ package io.element.android.libraries.designsystem.atomic.molecules import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -20,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color 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 @@ -62,7 +62,7 @@ fun InfoListItemMolecule( @Composable internal fun InfoListItemMoleculePreview() { ElementPreview { - val color = if (isSystemInDarkTheme()) Color.DarkGray else Color.LightGray + val color = ElementTheme.colors.bgSubtleSecondary Column( modifier = Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(8.dp), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt index 28a44579610..aeb8b5ba65b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt @@ -111,7 +111,6 @@ internal fun InfoListOrganismPreview() = ElementPreview { InfoListItem(message = "A bottom item"), ) InfoListOrganism( - items, - backgroundColor = ElementTheme.materialColors.surfaceVariant, + items = items, ) } 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 4a7c3f2dd5c..bf549ecda2e 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 @@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults 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.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -63,6 +65,7 @@ fun FlowStepPage( } }, title = {}, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) ) }, header = { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt index 8a95340342d..5100abad64e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt @@ -9,20 +9,24 @@ package io.element.android.libraries.designsystem.atomic.pages import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.consumeWindowInsets 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.lazy.LazyColumn -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview @@ -32,7 +36,7 @@ import io.element.android.libraries.designsystem.theme.components.Text /** * @param modifier Classical modifier. - * @param paddingValues padding values to apply to the content. + * @param contentPadding padding values to apply to the content. * @param containerColor color of the container. Set to [Color.Transparent] if you provide a background in the [modifier]. * @param isScrollable if the whole content should be scrollable. * @param background optional background component. @@ -45,8 +49,8 @@ import io.element.android.libraries.designsystem.theme.components.Text @Composable fun HeaderFooterPage( modifier: Modifier = Modifier, - paddingValues: PaddingValues = PaddingValues(20.dp), - containerColor: Color = MaterialTheme.colorScheme.background, + contentPadding: PaddingValues = PaddingValues(20.dp), + containerColor: Color = ElementTheme.colors.bgCanvasDefault, isScrollable: Boolean = false, background: @Composable () -> Unit = {}, topBar: @Composable () -> Unit = {}, @@ -54,64 +58,69 @@ fun HeaderFooterPage( footer: @Composable () -> Unit = {}, content: @Composable () -> Unit = {}, ) { - val topBar = remember { movableContentOf(topBar) } - val header = remember { movableContentOf(header) } - val footer = remember { movableContentOf(footer) } - val content = remember { movableContentOf(content) } Scaffold( modifier = modifier, topBar = topBar, containerColor = containerColor, - ) { padding -> + ) { insetsPadding -> + val layoutDirection = LocalLayoutDirection.current + val contentInsetsPadding = remember(insetsPadding, layoutDirection) { + PaddingValues( + start = insetsPadding.calculateStartPadding(layoutDirection), + top = insetsPadding.calculateTopPadding(), + end = insetsPadding.calculateEndPadding(layoutDirection), + ) + } + val footerInsetsPadding = remember(insetsPadding, layoutDirection) { + PaddingValues( + start = insetsPadding.calculateStartPadding(layoutDirection), + end = insetsPadding.calculateEndPadding(layoutDirection), + bottom = insetsPadding.calculateBottomPadding(), + ) + } Box { background() - if (isScrollable) { - // Render in a LazyColumn - LazyColumn( + + // Render in a Column + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues = contentPadding) + .consumeWindowInsets(insetsPadding) + .imePadding(), + ) { + // Content + Column( modifier = Modifier - .padding(paddingValues = paddingValues) - .padding(padding) - .consumeWindowInsets(padding) - .imePadding() + .fillMaxWidth() + .run { + if (isScrollable) { + verticalScroll(rememberScrollState()) + // Make sure the scrollable content takes the full available height + .height(IntrinsicSize.Max) + } else { + Modifier + } + } + // Apply insets here so if the content is scrollable it can get below the top app bar if needed + .padding(contentInsetsPadding) + .weight(1f, fill = true), ) { // Header - item { - header() - } - // Content - item { + header() + Box { content() } - // Footer - item { - Box(modifier = Modifier.padding(horizontal = 16.dp)) { - footer() - } - } } - } else { - // Render in a Column - Column( + + // Footer + Box( modifier = Modifier - .padding(paddingValues = paddingValues) - .padding(padding) - .consumeWindowInsets(padding) - .imePadding() + .padding(start = 16.dp, end = 16.dp, top = 16.dp) + .fillMaxWidth() + .padding(footerInsetsPadding) ) { - // Header - header() - // Content - Column( - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - ) { - content() - } - // Footer - Box(modifier = Modifier.padding(horizontal = 16.dp)) { - footer() - } + footer() } } } @@ -124,8 +133,7 @@ internal fun HeaderFooterPagePreview() = ElementPreview { HeaderFooterPage( content = { Box( - Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center ) { Text( @@ -160,3 +168,46 @@ internal fun HeaderFooterPagePreview() = ElementPreview { } ) } + +@PreviewsDayNight +@Composable +internal fun HeaderFooterPageScrollablePreview() = ElementPreview { + HeaderFooterPage( + content = { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Content", + style = ElementTheme.typography.fontHeadingXlBold + ) + } + }, + header = { + Box( + Modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Header", + style = ElementTheme.typography.fontHeadingXlBold + ) + } + }, + footer = { + Box( + Modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Footer", + style = ElementTheme.typography.fontHeadingXlBold + ) + } + }, + isScrollable = true, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/LightGradientBackground.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/LightGradientBackground.kt index 274518f831c..d33fa9f211e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/LightGradientBackground.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/LightGradientBackground.kt @@ -9,13 +9,13 @@ package io.element.android.libraries.designsystem.background import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.center import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RadialGradientShader import androidx.compose.ui.graphics.ShaderBrush +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -25,7 +25,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight @Composable fun LightGradientBackground( modifier: Modifier = Modifier, - backgroundColor: Color = MaterialTheme.colorScheme.background, + backgroundColor: Color = ElementTheme.colors.bgCanvasDefault, firstColor: Color = Color(0x1E0DBD8B), secondColor: Color = Color(0x001273EB), ratio: Float = 642 / 775f, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/OnboardingBackground.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/OnboardingBackground.kt index 8ea58deb40c..b4830e0904a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/OnboardingBackground.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/OnboardingBackground.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.designsystem.background import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -32,13 +33,17 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight @Suppress("ModifierMissing") @Composable fun OnboardingBackground() { - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .background(ElementTheme.colors.bgCanvasDefault) + ) { val isLightTheme = ElementTheme.isLightTheme Canvas( modifier = Modifier - .fillMaxWidth() - .height(220.dp) - .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(220.dp) + .align(Alignment.BottomCenter) ) { val gradientBrush = ShaderBrush( LinearGradientShader( 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 ee0a13466e5..ae88faa2057 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 @@ -118,7 +118,7 @@ private fun InformativeAnnouncement( AnnouncementSurface(modifier = modifier) { Row { Icon( - imageVector = if (isError) CompoundIcons.Error() else CompoundIcons.Info(), + imageVector = if (isError) CompoundIcons.ErrorSolid() else CompoundIcons.Info(), tint = if (isError) ElementTheme.colors.iconCriticalPrimary else ElementTheme.colors.iconPrimary, contentDescription = null, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Badge.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Badge.kt index d49e0a41e09..5e2160a01f7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Badge.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Badge.kt @@ -25,8 +25,6 @@ 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.badgePositiveBackgroundColor -import io.element.android.libraries.designsystem.theme.badgePositiveContentColor import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text @@ -76,9 +74,9 @@ internal fun BadgePreview() { Badge( text = "Trusted", icon = CompoundIcons.Verified(), - backgroundColor = ElementTheme.colors.badgePositiveBackgroundColor, - textColor = ElementTheme.colors.badgePositiveContentColor, - iconColor = ElementTheme.colors.iconSuccessPrimary, + backgroundColor = ElementTheme.colors.bgBadgeAccent, + textColor = ElementTheme.colors.textBadgeAccent, + iconColor = ElementTheme.colors.textBadgeAccent, ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt index 7d5d805ff8e..f469555ee45 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt @@ -33,6 +33,7 @@ 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.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.ui.strings.CommonStrings @@ -78,6 +79,11 @@ object BigIcon { * A success style with a tinted background. */ data object SuccessSolid : Style + + /** + * A loading style with the default background color. + */ + data object Loading : Style } /** @@ -101,31 +107,7 @@ object BigIcon { Style.Success -> Color.Transparent Style.AlertSolid -> ElementTheme.colors.bgCriticalSubtle Style.SuccessSolid -> ElementTheme.colors.bgSuccessSubtle - } - val icon = when (style) { - is Style.Default -> style.vectorIcon - Style.Alert, - Style.AlertSolid -> CompoundIcons.Error() - Style.Success, - Style.SuccessSolid -> CompoundIcons.CheckCircleSolid() - } - val contentDescription = when (style) { - is Style.Default -> style.contentDescription - Style.Alert, - Style.AlertSolid -> stringResource(CommonStrings.common_error) - Style.Success, - Style.SuccessSolid -> stringResource(CommonStrings.common_success) - } - val iconTint = when (style) { - is Style.Default -> if (style.useCriticalTint) { - ElementTheme.colors.iconCriticalPrimary - } else { - ElementTheme.colors.iconSecondary - } - Style.Alert, - Style.AlertSolid -> ElementTheme.colors.iconCriticalPrimary - Style.Success, - Style.SuccessSolid -> ElementTheme.colors.iconSuccessPrimary + Style.Loading -> ElementTheme.colors.bgSubtleSecondary } Box( modifier = modifier @@ -134,12 +116,50 @@ object BigIcon { .background(backgroundColor), contentAlignment = Alignment.Center, ) { - Icon( - modifier = Modifier.size(32.dp), - tint = iconTint, - imageVector = icon, - contentDescription = contentDescription - ) + if (style is Style.Loading) { + CircularProgressIndicator( + modifier = Modifier.size(27.dp), + color = ElementTheme.colors.iconSecondary, + trackColor = Color.Transparent, + strokeWidth = 3.dp, + ) + } else { + val icon = when (style) { + is Style.Default -> style.vectorIcon + Style.Alert, + Style.AlertSolid -> CompoundIcons.ErrorSolid() + Style.Success, + Style.SuccessSolid -> CompoundIcons.CheckCircleSolid() + Style.Loading -> error("This should never be reached") + } + val contentDescription = when (style) { + is Style.Default -> style.contentDescription + Style.Alert, + Style.AlertSolid -> stringResource(CommonStrings.common_error) + Style.Success, + Style.SuccessSolid -> stringResource(CommonStrings.common_success) + Style.Loading -> error("This should never be reached") + } + val iconTint = when (style) { + is Style.Default -> if (style.useCriticalTint) { + ElementTheme.colors.iconCriticalPrimary + } else { + ElementTheme.colors.iconSecondary + } + Style.Alert, + Style.AlertSolid -> ElementTheme.colors.iconCriticalPrimary + Style.Success, + Style.SuccessSolid -> ElementTheme.colors.iconSuccessPrimary + Style.Loading -> error("This should never be reached") + } + + Icon( + modifier = Modifier.size(32.dp), + tint = iconTint, + imageVector = icon, + contentDescription = contentDescription + ) + } } } } @@ -173,6 +193,7 @@ internal class BigIconStyleProvider : PreviewParameterProvider { BigIcon.Style.AlertSolid, BigIcon.Style.Default(Icons.Filled.CatchingPokemon, useCriticalTint = true), BigIcon.Style.Success, - BigIcon.Style.SuccessSolid + BigIcon.Style.SuccessSolid, + BigIcon.Style.Loading, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt index 5e3aebd6565..fd4b380fdcc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.designsystem.components import android.graphics.Bitmap import android.graphics.Typeface -import android.graphics.drawable.BitmapDrawable import android.os.Build import android.text.TextPaint import androidx.annotation.FloatRange @@ -60,13 +59,11 @@ import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -85,16 +82,19 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toSize -import coil.imageLoader -import coil.request.DefaultRequestOptions -import coil.request.ImageRequest +import coil3.SingletonImageLoader +import coil3.request.ImageRequest +import coil3.request.allowHardware +import coil3.toBitmap import com.airbnb.android.showkase.annotation.ShowkaseComposable import com.vanniktech.blurhash.BlurHash import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.libraries.designsystem.R 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 +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.PreviewGroup import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -103,6 +103,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar 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.CommonDrawables import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -328,7 +329,7 @@ fun Modifier.avatarBloom( ImageRequest.Builder(context) .data(avatarData) // Allow cache and default dispatchers - .defaults(DefaultRequestOptions()) + .defaults(ImageRequest.Defaults()) // Needed to be able to read pixels from the Bitmap for the hash .allowHardware(false) // Reduce size so it loads faster for large avatars @@ -340,9 +341,11 @@ fun Modifier.avatarBloom( var blurHash by rememberSaveable(avatarData) { mutableStateOf(null) } LaunchedEffect(avatarData) { withContext(Dispatchers.IO) { - val drawable = - context.imageLoader.execute(painterRequest).drawable ?: return@withContext - val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@withContext + val bitmap = SingletonImageLoader.get(context) + .execute(painterRequest) + .image + ?.toBitmap() + ?: return@withContext blurHash = BlurHash.encode( bitmap = bitmap, componentX = BloomDefaults.HASH_COMPONENTS, @@ -368,7 +371,7 @@ fun Modifier.avatarBloom( val initialsBitmap = initialsBitmap( width = BloomDefaults.ENCODE_SIZE_PX.toDp(), height = BloomDefaults.ENCODE_SIZE_PX.toDp(), - text = avatarData.initial, + text = avatarData.initialLetter, textColor = avatarColors.foreground, backgroundColor = avatarColors.background, ) @@ -463,7 +466,9 @@ internal fun BloomPreview() { var topAppBarHeight by remember { mutableIntStateOf(-1) } val topAppBarState = rememberTopAppBarState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState) - ElementPreview { + ElementPreview( + drawableFallbackForImages = CommonDrawables.sample_avatar, + ) { Scaffold( modifier = Modifier .fillMaxSize() @@ -477,7 +482,7 @@ internal fun BloomPreview() { } .bloom( hash = blurhash, - background = ElementTheme.materialColors.background, + background = ElementTheme.colors.bgCanvasDefault, blurSize = DpSize(430.dp, 430.dp), offset = DpOffset(24.dp, 24.dp), clipToSize = if (topAppBarHeight > 0) DpSize(430.dp, topAppBarHeight.toDp()) else DpSize.Zero, @@ -487,14 +492,14 @@ internal fun BloomPreview() { scrolledContainerColor = Color.Black.copy(alpha = 0.05f), ), navigationIcon = { - Image( - modifier = Modifier - .padding(start = 8.dp) - .size(32.dp) - .clip(CircleShape), - painter = painterResource(id = R.drawable.sample_avatar), - contentScale = ContentScale.Crop, - contentDescription = null + Avatar( + avatarData = AvatarData( + id = "sample-avatar", + name = "sample", + url = "aURL", + size = AvatarSize.CurrentUserTopBar, + ), + avatarType = AvatarType.User, ) }, actions = { @@ -554,9 +559,9 @@ internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorIntProvider::cl // Workaround to display a very subtle bloom for avatars with very soft colors Color(0xFFF9F9F9) } else { - ElementTheme.materialColors.background + ElementTheme.colors.bgCanvasDefault }, - bottomSoftEdgeColor = ElementTheme.materialColors.background, + bottomSoftEdgeColor = ElementTheme.colors.bgCanvasDefault, blurSize = DpSize(256.dp, 256.dp), ), contentAlignment = Alignment.Center diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt index cfa0c1fd8a2..3638556da3c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt @@ -135,7 +135,7 @@ fun ClickableLinkText( fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString { val original = this - val spannable = SpannableString(this.text) + val spannable = SpannableString.valueOf(this.text) LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES) val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java) @@ -147,6 +147,7 @@ fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString { if (original.getLinkAnnotations(start, end).isEmpty() && original.getStringAnnotations("URL", start, end).isEmpty()) { // Prevent linkifying domains in user or room handles (@user:domain.com, #room:domain.com) if (start > 0 && !spannable[start - 1].isWhitespace()) continue + addStyle( start = start, end = end, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt new file mode 100644 index 00000000000..66b404f554e --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt @@ -0,0 +1,280 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components + +import android.annotation.SuppressLint +import android.content.Context +import android.view.MotionEvent +import android.view.ViewGroup +import android.widget.EditText +import androidx.appcompat.app.ActionBar.LayoutParams +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectVerticalDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +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.fillMaxWidth +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import kotlinx.coroutines.launch +import timber.log.Timber +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +@Composable +fun ExpandableBottomSheetLayout( + sheetDragHandle: @Composable BoxScope.() -> Unit, + bottomSheetContent: @Composable ColumnScope.() -> Unit, + state: ExpandableBottomSheetLayoutState, + maxBottomSheetContentHeight: Dp, + isSwipeGestureEnabled: Boolean, + modifier: Modifier = Modifier, + sheetShape: Shape = RectangleShape, + backgroundColor: Color = Color.Transparent, + content: @Composable () -> Unit, +) { + var minBottomContentHeightPx by remember { mutableIntStateOf(0) } + var currentBottomContentHeightPx by remember { mutableIntStateOf(minBottomContentHeightPx) } + val maxBottomContentHeightPx = with(LocalDensity.current) { maxBottomSheetContentHeight.roundToPx() } + var calculatedMaxBottomContentHeightPx by remember(maxBottomContentHeightPx) { mutableIntStateOf(maxBottomContentHeightPx) } + val animatable = remember { Animatable(0f) } + + fun calculatePercentage(currentPos: Int, minPos: Int, maxPos: Int): Float { + val currentProgress = currentPos - minPos + if (currentProgress < 0) { + Timber.e("Invalid current progress: $currentProgress, minPos: $minPos, maxPos: $maxPos") + return 0f + } + val total = (maxPos - minPos).toFloat() + if (total <= 0) { + Timber.e("Invalid total space: $total, minPos: $minPos, maxPos: $maxPos") + return 0f + } + return currentProgress / total + } + + LaunchedEffect(animatable.value) { + if (animatable.isRunning && animatable.value != animatable.targetValue) { + currentBottomContentHeightPx = animatable.value.roundToInt() + state.internalDraggingPercentage = calculatePercentage( + currentPos = currentBottomContentHeightPx, + minPos = minBottomContentHeightPx, + maxPos = calculatedMaxBottomContentHeightPx, + ) + } + } + + val coroutineScope = rememberCoroutineScope() + + val composables = @Composable { + content() + Column( + modifier = Modifier + .clip(sheetShape) + .background(backgroundColor) + .run { + if (isSwipeGestureEnabled) { + pointerInput(maxBottomSheetContentHeight) { + detectVerticalDragGestures( + onVerticalDrag = { _, dragAmount -> + val calculatedHeight = max(minBottomContentHeightPx, currentBottomContentHeightPx - dragAmount.roundToInt()) + val newHeight = min(calculatedMaxBottomContentHeightPx, calculatedHeight) + state.internalPosition = when (newHeight) { + calculatedMaxBottomContentHeightPx -> ExpandableBottomSheetLayoutState.Position.EXPANDED + minBottomContentHeightPx -> ExpandableBottomSheetLayoutState.Position.COLLAPSED + else -> ExpandableBottomSheetLayoutState.Position.DRAGGING + } + state.internalDraggingPercentage = calculatePercentage( + currentPos = newHeight, + minPos = minBottomContentHeightPx, + maxPos = calculatedMaxBottomContentHeightPx, + ) + currentBottomContentHeightPx = newHeight + }, + onDragEnd = { + coroutineScope.launch { + val middle = (calculatedMaxBottomContentHeightPx + minBottomContentHeightPx) / 2 + animatable.snapTo(currentBottomContentHeightPx.toFloat()) + + val destination = if (currentBottomContentHeightPx > middle) { + state.internalPosition = ExpandableBottomSheetLayoutState.Position.EXPANDED + calculatedMaxBottomContentHeightPx + } else { + state.internalPosition = ExpandableBottomSheetLayoutState.Position.COLLAPSED + minBottomContentHeightPx + }.toFloat() + + animatable.animateTo(destination) + } + } + ) + } + } else { + this + } + } + ) { + Box(Modifier.fillMaxWidth()) { + sheetDragHandle() + } + bottomSheetContent() + } + } + Layout( + content = composables, + modifier = modifier, + measurePolicy = { measurables, constraints -> + calculatedMaxBottomContentHeightPx = min(constraints.maxHeight, maxBottomContentHeightPx) + + val contentMeasurables = measurables[0] + val bottomContentMeasurables = measurables[1] + + val minIntrinsicHeight = bottomContentMeasurables.minIntrinsicHeight(constraints.maxWidth) + val lastMinBottomContentHeightPx = minBottomContentHeightPx + minBottomContentHeightPx = min(minIntrinsicHeight, calculatedMaxBottomContentHeightPx) + + val isExpanded = state.position == ExpandableBottomSheetLayoutState.Position.EXPANDED + if (lastMinBottomContentHeightPx != minBottomContentHeightPx && !isExpanded) { + currentBottomContentHeightPx = minBottomContentHeightPx + } + + val measuredBottomContent = bottomContentMeasurables.measure( + Constraints.fixed( + constraints.maxWidth, + max(minBottomContentHeightPx, currentBottomContentHeightPx) + ) + ) + + var remainingHeight = constraints.maxHeight - currentBottomContentHeightPx + if (remainingHeight < 0) { + Timber.e("Remaining height is negative: $remainingHeight, resetting to 0") + remainingHeight = 0 + } + + val contentPlaceable = contentMeasurables.measure( + Constraints.fixed(constraints.maxWidth, remainingHeight) + ) + + layout(constraints.maxWidth, constraints.maxHeight) { + contentPlaceable.place(0, 0) + measuredBottomContent.place(IntOffset(0, constraints.maxHeight - currentBottomContentHeightPx), zIndex = 10f) + } + } + ) +} + +@Preview(showBackground = true) +@Composable +@Suppress("UnusedPrivateMember") +internal fun ExpandableBottomSheetLayoutPreview() { + ExpandableBottomSheetLayout( + sheetDragHandle = { + Box( + modifier = + Modifier + .padding(vertical = 6.dp) + .clip(RoundedCornerShape(6.dp)) + .align(Alignment.Center) + .size(100.dp, 8.dp) + .background(Color.Gray) + ) + }, + content = { + Box(Modifier.fillMaxWidth()) { + Text("This is the main content", modifier = Modifier.padding(16.dp).align(Alignment.Center)) + } + }, + bottomSheetContent = { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = true) + .padding(horizontal = 10.dp) + .clip(RoundedCornerShape(10.dp)) + .background(Color.Blue) + ) { + AndroidView( + modifier = Modifier + .fillMaxWidth() + .background(Color.LightGray), + factory = { context -> + PreviewEditText(context).apply { + val initialText = "1111\n2222\n3333\n4444\n5555\n6666" + setText(initialText) + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + } + } + ) + } + Text("A footer", modifier = Modifier.padding(vertical = 6.dp, horizontal = 16.dp)) + }, + maxBottomSheetContentHeight = 1800.dp, + isSwipeGestureEnabled = true, + backgroundColor = Color.White, + state = rememberExpandableBottomSheetLayoutState(), + sheetShape = RoundedCornerShape(20.dp, 20.dp, 0.dp, 0.dp), + modifier = Modifier + .windowInsetsPadding(WindowInsets.statusBars) + .windowInsetsPadding(WindowInsets.ime) + .fillMaxSize() + .background(Color.Red.copy(alpha = 0.2f)), + ) +} + +// This is just for preview purposes +@SuppressLint("AppCompatCustomView") +private class PreviewEditText(context: Context) : EditText(context) { + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + parent?.requestDisallowInterceptTouchEvent(true) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + super.onTouchEvent(event) + parent?.requestDisallowInterceptTouchEvent(true) + return true + } + + override fun dispatchTouchEvent(event: MotionEvent?): Boolean { + return super.dispatchTouchEvent(event) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayoutState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayoutState.kt new file mode 100644 index 00000000000..eda4cd73b2a --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayoutState.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +/** + * Creates and remembers an [ExpandableBottomSheetLayoutState]. + */ +@Composable +fun rememberExpandableBottomSheetLayoutState(): ExpandableBottomSheetLayoutState { + return remember { ExpandableBottomSheetLayoutState() } +} + +/** + * State for the [ExpandableBottomSheetLayout]. + * + * This state holds the current position of the bottom sheet layout and the percentage of the layout that is being dragged. + */ +@Stable +class ExpandableBottomSheetLayoutState { + internal var internalPosition: Position by mutableStateOf(Position.COLLAPSED) + internal var internalDraggingPercentage: Float by mutableFloatStateOf( + if (internalPosition == Position.EXPANDED) 1f else 0f + ) + + /** + * The current position of the bottom sheet layout. + */ + val position = internalPosition + + /** + * The percentage of the bottom sheet layout that is currently being dragged. + * This value ranges from `0f` for [Position.COLLAPSED] to `1f` for [Position.EXPANDED]. + */ + val draggingPercentage = internalDraggingPercentage + + /** + * The position of the bottom sheet layout. + */ + enum class Position { + /** The bottom sheet is collapsed to its minimum visible height. */ + COLLAPSED, + + /** The bottom sheet is being dragged by user input. */ + DRAGGING, + + /** The bottom sheet is expanded to its maximum visible height. */ + EXPANDED + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledCheckbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledCheckbox.kt index 554050511e6..3f25a69eb06 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledCheckbox.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledCheckbox.kt @@ -9,11 +9,11 @@ package io.element.android.libraries.designsystem.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.theme.components.Checkbox @@ -38,7 +38,7 @@ fun LabelledCheckbox( ) Text( text = text, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt index 8e276613596..f5d8a50ce03 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup @@ -64,13 +65,13 @@ fun ProgressDialog( when (type) { is ProgressDialogType.Indeterminate -> { CircularProgressIndicator( - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.iconPrimary ) } is ProgressDialogType.Determinate -> { CircularProgressIndicator( progress = { type.progress }, - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.iconPrimary ) } } @@ -93,7 +94,7 @@ private fun ProgressDialogContent( onCancelClick: () -> Unit = {}, progressIndicator: @Composable () -> Unit = { CircularProgressIndicator( - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.iconPrimary ) } ) { @@ -114,7 +115,7 @@ private fun ProgressDialogContent( Spacer(modifier = Modifier.height(22.dp)) Text( text = text, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, ) } if (showCancelButton) { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt index 99928104e8f..db7ef624830 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt @@ -7,129 +7,57 @@ package io.element.android.libraries.designsystem.components.avatar -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect 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.platform.LocalInspectionMode -import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter -import coil.compose.SubcomposeAsyncImage -import coil.compose.SubcomposeAsyncImageContent -import io.element.android.compound.theme.ElementTheme -import io.element.android.libraries.designsystem.colors.AvatarColorsProvider +import io.element.android.libraries.designsystem.components.avatar.internal.RoomAvatar +import io.element.android.libraries.designsystem.components.avatar.internal.SpaceAvatar +import io.element.android.libraries.designsystem.components.avatar.internal.UserAvatar import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup -import io.element.android.libraries.designsystem.preview.debugPlaceholderAvatar -import io.element.android.libraries.designsystem.text.toSp import io.element.android.libraries.designsystem.theme.components.Text -import timber.log.Timber +import io.element.android.libraries.designsystem.utils.CommonDrawables @Composable fun Avatar( avatarData: AvatarData, + avatarType: AvatarType, modifier: Modifier = Modifier, contentDescription: String? = null, // If not null, will be used instead of the size from avatarData forcedAvatarSize: Dp? = null, + // If true, will show initials even if avatarData.url is not null + hideImage: Boolean = false, ) { - val commonModifier = modifier - .size(forcedAvatarSize ?: avatarData.size.dp) - .clip(CircleShape) - if (avatarData.url.isNullOrBlank()) { - InitialsAvatar( + when (avatarType) { + is AvatarType.Room -> RoomAvatar( avatarData = avatarData, + avatarType = avatarType, + modifier = modifier, + hideAvatarImage = hideImage, forcedAvatarSize = forcedAvatarSize, - modifier = commonModifier, - ) - } else { - ImageAvatar( - avatarData = avatarData, - forcedAvatarSize = forcedAvatarSize, - modifier = commonModifier, contentDescription = contentDescription, ) - } -} - -@Composable -private fun ImageAvatar( - avatarData: AvatarData, - forcedAvatarSize: Dp?, - modifier: Modifier = Modifier, - contentDescription: String? = null, -) { - if (LocalInspectionMode.current) { - // For compose previews, use debugPlaceholderAvatar() - // instead of falling back to initials avatar on load failure - AsyncImage( - model = avatarData, + AvatarType.User -> UserAvatar( + avatarData = avatarData, + modifier = modifier, contentDescription = contentDescription, - placeholder = debugPlaceholderAvatar(), - modifier = modifier + forcedAvatarSize = forcedAvatarSize, + hideImage = hideImage, ) - } else { - SubcomposeAsyncImage( - model = avatarData, + is AvatarType.Space -> SpaceAvatar( + avatarData = avatarData, + avatarType = avatarType, + modifier = modifier, + hideAvatarImage = hideImage, + forcedAvatarSize = forcedAvatarSize, contentDescription = contentDescription, - contentScale = ContentScale.Crop, - modifier = modifier - ) { - when (val state = painter.state) { - is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent() - is AsyncImagePainter.State.Error -> { - SideEffect { - Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}") - } - InitialsAvatar( - avatarData = avatarData, - forcedAvatarSize = forcedAvatarSize, - ) - } - else -> InitialsAvatar( - avatarData = avatarData, - forcedAvatarSize = forcedAvatarSize, - ) - } - } - } -} - -@Composable -private fun InitialsAvatar( - avatarData: AvatarData, - forcedAvatarSize: Dp?, - modifier: Modifier = Modifier, -) { - val avatarColors = AvatarColorsProvider.provide(avatarData.id) - Box( - modifier.background(color = avatarColors.background) - ) { - val fontSize = (forcedAvatarSize ?: avatarData.size.dp).toSp() / 2 - val originalFont = ElementTheme.typography.fontHeadingMdBold - val ratio = fontSize.value / originalFont.fontSize.value - val lineHeight = originalFont.lineHeight * ratio - Text( - modifier = Modifier - .clearAndSetSemantics {} - .align(Alignment.Center), - text = avatarData.initial, - style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp), - color = avatarColors.foreground, ) } } @@ -137,12 +65,17 @@ private fun InitialsAvatar( @Preview(group = PreviewGroup.Avatars) @Composable internal fun AvatarPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) = - ElementThemedPreview { + ElementThemedPreview( + drawableFallbackForImages = CommonDrawables.sample_avatar, + ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - Avatar(avatarData) + Avatar( + avatarData = avatarData, + avatarType = AvatarType.User, + ) Text(text = avatarData.size.name + " " + avatarData.size.dp) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt index a3cd6f75e39..2e711ee6eb8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt @@ -8,6 +8,8 @@ package io.element.android.libraries.designsystem.components.avatar import androidx.compose.runtime.Immutable +import io.element.android.libraries.core.data.tryOrNull +import java.text.BreakIterator @Immutable data class AvatarData( @@ -16,7 +18,7 @@ data class AvatarData( val url: String? = null, val size: AvatarSize, ) { - val initial by lazy { + val initialLetter by lazy { // For roomIds, use "#" as initial (name?.takeIf { it.isNotBlank() } ?: id.takeIf { !it.startsWith("!") } ?: "#") .let { dn -> @@ -27,24 +29,36 @@ data class AvatarData( startIndex++ } - var length = 1 - var first = dn[startIndex] + var next = dn[startIndex] // LEFT-TO-RIGHT MARK - if (dn.length >= 2 && 0x200e == first.code) { + if (dn.length >= 2 && 0x200e == next.code) { startIndex++ - first = dn[startIndex] + next = dn[startIndex] } - // check if it’s the start of a surrogate pair - if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) { - val second = dn[startIndex + 1] - if (second.code in 0xDC00..0xDFFF) { - length++ + while (next.isWhitespace()) { + if (dn.length > startIndex + 1) { + startIndex++ + next = dn[startIndex] + } else { + break } } - dn.substring(startIndex, startIndex + length) + val fullCharacterIterator = BreakIterator.getCharacterInstance() + fullCharacterIterator.setText(dn) + val glyphBoundary = tryOrNull { fullCharacterIterator.following(startIndex) } + ?.takeIf { it in startIndex..dn.length } + + when { + // Use the found boundary + glyphBoundary != null -> dn.substring(startIndex, glyphBoundary) + // If no boundary was found, default to the next char if possible + startIndex + 1 < dn.length -> dn.substring(startIndex, startIndex + 1) + // Return a fallback character otherwise + else -> "#" + } } .uppercase() } 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 ea9e749baf6..3c209df2cf2 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 @@ -11,7 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider open class AvatarDataProvider : PreviewParameterProvider { override val values: Sequence - get() = AvatarSize.values() + get() = AvatarSize.entries .asSequence() .map { sequenceOf( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarShape.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarShape.kt new file mode 100644 index 00000000000..c4c2c77766d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarShape.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar + +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp + +@Composable +fun AvatarType.User.avatarShape() = CircleShape + +@Composable +fun AvatarType.Room.avatarShape() = CircleShape + +@Composable +fun AvatarType.Space.avatarShape(avatarSize: Dp) = RoundedCornerShape(avatarSize * 0.25f) + +@Composable +fun AvatarType.avatarShape(avatarSize: Dp): Shape { + return when (this) { + is AvatarType.Space -> avatarShape(avatarSize) + is AvatarType.Room -> avatarShape() + is AvatarType.User -> avatarShape() + } +} 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 d0950d29605..0f84b82fd18 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 @@ -59,4 +59,8 @@ enum class AvatarSize(val dp: Dp) { KnockRequestBanner(32.dp), MediaSender(32.dp), + + DmCreationConfirmation(64.dp), + + UserVerification(52.dp), } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarType.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarType.kt new file mode 100644 index 00000000000..6d99574a557 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarType.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +package io.element.android.libraries.designsystem.components.avatar + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +sealed interface AvatarType { + data object User : AvatarType + + data class Room( + val isTombstoned: Boolean = false, + val heroes: ImmutableList = persistentListOf(), + ) : AvatarType + + data class Space( + val isTombstoned: Boolean = false, + ) : AvatarType +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/CompositeAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/CompositeAvatar.kt deleted file mode 100644 index 9e40e08e2ad..00000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/CompositeAvatar.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.designsystem.components.avatar - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.offset -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.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.preview.ElementThemedPreview -import io.element.android.libraries.designsystem.preview.PreviewGroup -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toPersistentList -import java.util.Collections -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin - -@Composable -fun CompositeAvatar( - avatarData: AvatarData, - heroes: ImmutableList, - modifier: Modifier = Modifier, - contentDescription: String? = null, -) { - if (avatarData.url != null || heroes.isEmpty()) { - Avatar(avatarData, modifier, contentDescription) - } else { - val limitedHeroes = heroes.take(4) - val numberOfHeroes = limitedHeroes.size - if (numberOfHeroes == 4) { - // Swap 2 and 3 so that the 4th hero is at the bottom right - Collections.swap(limitedHeroes, 2, 3) - } - when (numberOfHeroes) { - 0 -> { - error("Unsupported number of heroes: 0") - } - 1 -> { - Avatar(heroes[0], modifier, contentDescription) - } - else -> { - val angle = 2 * Math.PI / numberOfHeroes - val offsetRadius = when (numberOfHeroes) { - 2 -> avatarData.size.dp.value / 4.2 - 3 -> avatarData.size.dp.value / 4.0 - 4 -> avatarData.size.dp.value / 3.1 - else -> error("Unsupported number of heroes: $numberOfHeroes") - } - val heroAvatarSize = when (numberOfHeroes) { - 2 -> avatarData.size.dp / 2.2f - 3 -> avatarData.size.dp / 2.4f - 4 -> avatarData.size.dp / 2.2f - else -> error("Unsupported number of heroes: $numberOfHeroes") - } - val angleOffset = when (numberOfHeroes) { - 2 -> PI - 3 -> 7 * PI / 6 - 4 -> 13 * PI / 4 - else -> error("Unsupported number of heroes: $numberOfHeroes") - } - Box( - modifier = modifier - .size(avatarData.size.dp) - .semantics { - this.contentDescription = contentDescription.orEmpty() - }, - contentAlignment = Alignment.Center, - ) { - limitedHeroes.forEachIndexed { index, heroAvatar -> - val xOffset = (offsetRadius * cos(angle * index.toDouble() + angleOffset)).dp - val yOffset = (offsetRadius * sin(angle * index.toDouble() + angleOffset)).dp - Box( - modifier = Modifier - .size(heroAvatarSize) - .offset( - x = xOffset, - y = yOffset, - ) - ) { - Avatar( - heroAvatar, - forcedAvatarSize = heroAvatarSize, - ) - } - } - } - } - } - } -} - -@Preview(group = PreviewGroup.Avatars) -@Composable -internal fun CompositeAvatarPreview() = ElementThemedPreview { - val mainAvatar = anAvatarData( - id = "Zac", - name = "Zac", - size = AvatarSize.RoomListItem, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - repeat(6) { nbOfHeroes -> - CompositeAvatar( - avatarData = mainAvatar, - heroes = List(nbOfHeroes) { aHeroAvatarData(it) }.toPersistentList(), - ) - } - } -} - -private fun aHeroAvatarData(i: Int) = anAvatarData( - id = ('A' + i).toString(), - name = ('A' + i).toString() -) 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 index 7b4e6dee865..f4d05d3fcdc 100644 --- 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 @@ -23,6 +23,7 @@ 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 @@ -30,6 +31,7 @@ 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 @@ -55,6 +57,8 @@ fun DmAvatars( // Draw user avatar and cut top end corner Avatar( avatarData = userAvatarData, + avatarType = AvatarType.User, + contentDescription = userAvatarData.url?.let { stringResource(CommonStrings.a11y_your_avatar) }, modifier = Modifier .align(Alignment.BottomStart) .graphicsLayer { @@ -78,17 +82,25 @@ fun DmAvatars( ) } .clip(CircleShape) - .clickable(enabled = userAvatarData.url != null) { + .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 = otherUserAvatarData.url?.let { stringResource(CommonStrings.a11y_other_user_avatar) }, modifier = Modifier .align(Alignment.TopEnd) .clip(CircleShape) - .clickable(enabled = otherUserAvatarData.url != null) { + .clickable( + enabled = otherUserAvatarData.url != null, + onClickLabel = stringResource(CommonStrings.action_view), + ) { otherUserAvatarData.url?.let { openOtherAvatarPreview(it) } } .testTag(TestTags.memberDetailAvatar) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/AvatarCluster.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/AvatarCluster.kt new file mode 100644 index 00000000000..c1009880530 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/AvatarCluster.kt @@ -0,0 +1,149 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.designsystem.components.avatar.internal + +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.offset +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.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.avatar.anAvatarData +import io.element.android.libraries.designsystem.components.avatar.avatarShape +import io.element.android.libraries.designsystem.preview.ElementThemedPreview +import io.element.android.libraries.designsystem.preview.PreviewGroup +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import java.util.Collections +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin + +private const val MAX_AVATAR_COUNT = 4 + +@Composable +internal fun AvatarCluster( + avatars: ImmutableList, + avatarType: AvatarType, + modifier: Modifier = Modifier, + hideAvatarImages: Boolean = false, + contentDescription: String? = null, +) { + val limitedAvatars = avatars.take(MAX_AVATAR_COUNT) + val numberOfAvatars = limitedAvatars.size + if (numberOfAvatars == 4) { + // Swap 2 and 3 so that the 4th avatar is at the bottom right + Collections.swap(limitedAvatars, 2, 3) + } + when (numberOfAvatars) { + 0 -> { + error("Unsupported number of avatars: 0") + } + 1 -> { + InitialOrImageAvatar( + avatarData = limitedAvatars[0], + hideAvatarImage = hideAvatarImages, + avatarShape = avatarType.avatarShape(limitedAvatars[0].size.dp), + forcedAvatarSize = null, + modifier = modifier, + contentDescription = contentDescription, + ) + } + else -> { + val size = limitedAvatars.first().size + val angle = 2 * Math.PI / numberOfAvatars + val offsetRadius = when (numberOfAvatars) { + 2 -> size.dp.value / 4.2 + 3 -> size.dp.value / 4.0 + 4 -> size.dp.value / 3.1 + else -> error("Unsupported number of heroes: $numberOfAvatars") + } + val heroAvatarSize = when (numberOfAvatars) { + 2 -> size.dp / 2.2f + 3 -> size.dp / 2.4f + 4 -> size.dp / 2.2f + else -> error("Unsupported number of heroes: $numberOfAvatars") + } + val angleOffset = when (numberOfAvatars) { + 2 -> PI + 3 -> 7 * PI / 6 + 4 -> 13 * PI / 4 + else -> error("Unsupported number of heroes: $numberOfAvatars") + } + Box( + modifier = modifier + .size(size.dp) + .semantics { + this.contentDescription = contentDescription.orEmpty() + }, + contentAlignment = Alignment.Center, + ) { + limitedAvatars.forEachIndexed { index, heroAvatar -> + val xOffset = (offsetRadius * cos(angle * index.toDouble() + angleOffset)).dp + val yOffset = (offsetRadius * sin(angle * index.toDouble() + angleOffset)).dp + Box( + modifier = Modifier + .size(heroAvatarSize) + .offset( + x = xOffset, + y = yOffset, + ) + ) { + InitialOrImageAvatar( + avatarData = heroAvatar, + hideAvatarImage = hideAvatarImages, + avatarShape = avatarType.avatarShape(heroAvatarSize), + forcedAvatarSize = heroAvatarSize, + modifier = Modifier, + contentDescription = contentDescription, + ) + } + } + } + } + } +} + +@Preview(group = PreviewGroup.Avatars) +@Composable +internal fun AvatarClusterPreview() = ElementThemedPreview { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + listOf( + AvatarType.User, + AvatarType.Room(), + AvatarType.Space(), + ).forEach { avatarType -> + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + for (ngOfAvatars in 1..5) { + AvatarCluster( + avatars = List(ngOfAvatars) { anAvatarData(it) }.toPersistentList(), + avatarType = avatarType, + ) + } + } + } + } +} + +private fun anAvatarData(i: Int) = anAvatarData( + id = ('A' + i).toString(), + name = ('A' + i).toString() +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt new file mode 100644 index 00000000000..db30a948300 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/ImageAvatar.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar.internal + +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import coil3.compose.AsyncImagePainter +import coil3.compose.SubcomposeAsyncImage +import coil3.compose.SubcomposeAsyncImageContent +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import timber.log.Timber + +@Composable +internal fun ImageAvatar( + avatarData: AvatarData, + avatarShape: Shape, + forcedAvatarSize: Dp?, + modifier: Modifier = Modifier, + contentDescription: String? = null, +) { + val size = forcedAvatarSize ?: avatarData.size.dp + SubcomposeAsyncImage( + model = avatarData, + contentDescription = contentDescription, + contentScale = ContentScale.Companion.Crop, + modifier = modifier + .size(size) + .clip(avatarShape) + ) { + val collectedState by painter.state.collectAsState() + when (val state = collectedState) { + is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent() + is AsyncImagePainter.State.Error -> { + SideEffect { + Timber.Forest.e( + state.result.throwable, + "Error loading avatar $state\n${state.result}" + ) + } + InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = forcedAvatarSize, + contentDescription = contentDescription, + ) + } + else -> InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = forcedAvatarSize, + contentDescription = contentDescription, + ) + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/InitialLetterAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/InitialLetterAvatar.kt new file mode 100644 index 00000000000..6503917b239 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/InitialLetterAvatar.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar.internal + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import io.element.android.libraries.designsystem.colors.AvatarColorsProvider +import io.element.android.libraries.designsystem.components.avatar.AvatarData + +@Composable +internal fun InitialLetterAvatar( + avatarData: AvatarData, + avatarShape: Shape, + forcedAvatarSize: Dp?, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + val avatarColors = AvatarColorsProvider.provide(avatarData.id) + TextAvatar( + text = avatarData.initialLetter, + size = forcedAvatarSize ?: avatarData.size.dp, + avatarShape = avatarShape, + colors = avatarColors, + contentDescription = contentDescription, + modifier = modifier + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/InitialOrImageAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/InitialOrImageAvatar.kt new file mode 100644 index 00000000000..3111048d3b5 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/InitialOrImageAvatar.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar.internal + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import io.element.android.libraries.designsystem.components.avatar.AvatarData + +@Composable +internal fun InitialOrImageAvatar( + avatarData: AvatarData, + hideAvatarImage: Boolean, + forcedAvatarSize: Dp?, + avatarShape: Shape, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + when { + avatarData.url.isNullOrBlank() || hideAvatarImage -> InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = forcedAvatarSize, + modifier = modifier, + contentDescription = contentDescription, + ) + else -> ImageAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = forcedAvatarSize, + modifier = modifier, + contentDescription = contentDescription, + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/RoomAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/RoomAvatar.kt new file mode 100644 index 00000000000..9fd2ff8cca3 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/RoomAvatar.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar.internal + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.avatar.avatarShape + +@Composable +internal fun RoomAvatar( + avatarData: AvatarData, + avatarType: AvatarType.Room, + modifier: Modifier = Modifier, + hideAvatarImage: Boolean = false, + forcedAvatarSize: Dp? = null, + contentDescription: String? = null, +) { + when { + avatarType.isTombstoned -> { + TombstonedRoomAvatar( + size = forcedAvatarSize ?: avatarData.size.dp, + modifier = modifier, + avatarShape = avatarType.avatarShape(), + contentDescription = contentDescription + ) + } + avatarData.url != null || avatarType.heroes.isEmpty() -> { + InitialOrImageAvatar( + avatarData = avatarData, + hideAvatarImage = hideAvatarImage, + avatarShape = avatarType.avatarShape(), + forcedAvatarSize = forcedAvatarSize, + modifier = modifier, + contentDescription = contentDescription, + ) + } + else -> { + AvatarCluster( + avatars = avatarType.heroes, + // Note: even for a room avatar, we use UserAvatarType here to display the avatar of heroes + avatarType = AvatarType.User, + modifier = modifier, + hideAvatarImages = hideAvatarImage, + contentDescription = contentDescription + ) + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/SpaceAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/SpaceAvatar.kt new file mode 100644 index 00000000000..b8880744c99 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/SpaceAvatar.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar.internal + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.avatar.anAvatarData +import io.element.android.libraries.designsystem.components.avatar.avatarShape +import io.element.android.libraries.designsystem.preview.ElementThemedPreview +import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.designsystem.utils.CommonDrawables + +@Composable +internal fun SpaceAvatar( + avatarData: AvatarData, + avatarType: AvatarType.Space, + modifier: Modifier = Modifier, + forcedAvatarSize: Dp? = null, + hideAvatarImage: Boolean = false, + contentDescription: String? = null, +) { + val size = forcedAvatarSize ?: avatarData.size.dp + when { + avatarType.isTombstoned -> TombstonedRoomAvatar( + size = size, + avatarShape = avatarType.avatarShape(size), + modifier = modifier, + contentDescription = contentDescription, + ) + else -> InitialOrImageAvatar( + avatarData = avatarData, + hideAvatarImage = hideAvatarImage, + avatarShape = avatarType.avatarShape(size), + forcedAvatarSize = forcedAvatarSize, + modifier = modifier, + contentDescription = contentDescription, + ) + } +} + +@Preview(group = PreviewGroup.Avatars) +@Composable +internal fun SpaceAvatarPreview() = + ElementThemedPreview( + drawableFallbackForImages = CommonDrawables.sample_avatar, + ) { + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + SpaceAvatar( + avatarData = anAvatarData(), + avatarType = AvatarType.Space(), + ) + SpaceAvatar( + avatarData = anAvatarData(), + avatarType = AvatarType.Space( + isTombstoned = true, + ), + ) + } + } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/TextAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/TextAvatar.kt new file mode 100644 index 00000000000..f44319fece8 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/TextAvatar.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar.internal + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.compound.theme.AvatarColors +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.avatar.avatarShape +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.designsystem.text.toSp +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +internal fun TextAvatar( + text: String, + size: Dp, + colors: AvatarColors, + contentDescription: String?, + avatarShape: Shape, + modifier: Modifier = Modifier, +) { + Box( + modifier + .size(size) + .clip(avatarShape) + .background(color = colors.background) + ) { + val fontSize = size.toSp() / 2 + val originalFont = ElementTheme.typography.fontHeadingMdBold + val ratio = fontSize.value / originalFont.fontSize.value + val lineHeight = originalFont.lineHeight * ratio + Text( + modifier = Modifier + .clearAndSetSemantics { + contentDescription?.let { + this.contentDescription = it + } + } + .align(Alignment.Center), + text = text, + style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp), + color = colors.foreground, + ) + } +} + +@Preview(group = PreviewGroup.Avatars) +@Composable +internal fun TextAvatarPreview() = ElementPreview { + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + listOf( + AvatarType.User, + AvatarType.Room(), + AvatarType.Space(), + ).forEach { avatarType -> + TextAvatar( + text = "AB", + size = 40.dp, + colors = AvatarColors( + background = ElementTheme.colors.bgSubtlePrimary, + foreground = ElementTheme.colors.iconPrimary, + ), + avatarShape = avatarType.avatarShape(40.dp), + contentDescription = null, + ) + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/TombstonedRoomAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/TombstonedRoomAvatar.kt new file mode 100644 index 00000000000..b712ff9fb1e --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/TombstonedRoomAvatar.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar.internal + +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.AvatarColors +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewGroup + +@Composable +internal fun TombstonedRoomAvatar( + size: Dp, + avatarShape: Shape, + modifier: Modifier = Modifier, + contentDescription: String? = null, +) { + TextAvatar( + text = "!", + size = size, + colors = AvatarColors( + background = ElementTheme.colors.bgSubtlePrimary, + foreground = ElementTheme.colors.iconTertiary + ), + modifier = modifier, + avatarShape = avatarShape, + contentDescription = contentDescription, + ) +} + +@Preview(group = PreviewGroup.Avatars) +@Composable +internal fun TombstonedRoomAvatarPreview() = ElementPreview { + TombstonedRoomAvatar( + size = 52.dp, + avatarShape = CircleShape, + contentDescription = null, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/UserAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/UserAvatar.kt new file mode 100644 index 00000000000..ad0275593d7 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/UserAvatar.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar.internal + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.avatar.avatarShape + +@Composable +internal fun UserAvatar( + avatarData: AvatarData, + modifier: Modifier = Modifier, + contentDescription: String? = null, + forcedAvatarSize: Dp? = null, + hideImage: Boolean = false, +) { + InitialOrImageAvatar( + avatarData = avatarData, + hideAvatarImage = hideImage, + avatarShape = AvatarType.User.avatarShape(), + modifier = modifier, + contentDescription = contentDescription, + forcedAvatarSize = forcedAvatarSize, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/UserAvatarPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/UserAvatarPreview.kt similarity index 78% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/UserAvatarPreview.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/UserAvatarPreview.kt index 8797c2750a1..d4a629a0527 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/UserAvatarPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/UserAvatarPreview.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.designsystem.components.avatar +package io.element.android.libraries.designsystem.components.avatar.internal import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -16,6 +16,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.element.android.compound.theme.avatarColors +import io.element.android.libraries.designsystem.components.avatar.Avatar +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.theme.components.Text @@ -33,7 +36,10 @@ internal fun UserAvatarColorsPreview() = ElementPreview { verticalAlignment = Alignment.CenterVertically, ) { // Note: it's OK, since the hash of "0" is 0, the hash of "1" is 1, etc. - Avatar(anAvatarData(id = "$it")) + Avatar( + avatarData = anAvatarData(id = "$it"), + avatarType = AvatarType.User, + ) Text(text = "Color index $it") } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt index df2be400c8d..c22197f22f0 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt @@ -20,7 +20,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import coil.compose.AsyncImage +import coil3.compose.AsyncImage @Composable fun BlurHashAsyncImage( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/GradientFloatingActionButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/GradientFloatingActionButton.kt index e693b6ad641..d58e1813538 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/GradientFloatingActionButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/GradientFloatingActionButton.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.LinearGradientShader import androidx.compose.ui.graphics.RadialGradientShader @@ -36,10 +37,12 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer 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.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.internal.LightColorTokens 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.Icon @OptIn(CoreColorToken::class) @@ -50,6 +53,16 @@ fun GradientFloatingActionButton( shape: Shape = RoundedCornerShape(25), content: @Composable () -> Unit, ) { + val color1 = if (LocalBuildMeta.current.isEnterpriseBuild) { + ElementTheme.colors.textActionAccent + } else { + LightColorTokens.colorGreen700 + } + val color2 = if (LocalBuildMeta.current.isEnterpriseBuild) { + ElementTheme.colors.textActionAccent + } else { + LightColorTokens.colorBlue900 + } val linearShaderBrush = remember { object : ShaderBrush() { override fun createShader(size: Size): Shader { @@ -57,8 +70,8 @@ fun GradientFloatingActionButton( from = Offset(size.width, size.height), to = Offset(size.width, 0f), colors = listOf( - LightColorTokens.colorBlue900, - LightColorTokens.colorGreen700, + color2, + color1, ), ) } @@ -71,8 +84,8 @@ fun GradientFloatingActionButton( center = size.center, radius = size.width / 2, colors = listOf( - LightColorTokens.colorGreen700, - LightColorTokens.colorBlue900, + color1, + color2, ) ) } @@ -85,8 +98,8 @@ fun GradientFloatingActionButton( .graphicsLayer(shape = shape, clip = false) .clip(shape) .drawBehind { - drawRect(brush = radialShaderBrush, alpha = 0.4f) drawRect(brush = linearShaderBrush) + drawRect(brush = radialShaderBrush, alpha = 0.4f, blendMode = BlendMode.Overlay) } .clickable( enabled = true, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt index b8b4a312596..537c1f639fa 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -58,17 +57,16 @@ fun MainActionButton( .widthIn(min = 76.dp, max = 96.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - val tintColor = if (enabled) LocalContentColor.current else MaterialTheme.colorScheme.secondary Icon( imageVector = imageVector, contentDescription = contentDescription, - tint = tintColor, + tint = if (enabled) LocalContentColor.current else ElementTheme.colors.iconDisabled, ) Spacer(modifier = Modifier.height(14.dp)) Text( title, style = ElementTheme.typography.fontBodyMdMedium.copy(hyphens = Hyphens.Auto), - color = tintColor, + color = if (enabled) LocalContentColor.current else ElementTheme.colors.textDisabled, overflow = TextOverflow.Visible, textAlign = TextAlign.Center, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt index d9e9471531c..a53955afc2b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt @@ -42,6 +42,7 @@ import io.element.android.compound.tokens.generated.internal.DarkColorTokens import io.element.android.compound.tokens.generated.internal.LightColorTokens 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.ButtonSize import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.lowHorizontalPaddingValue @@ -66,17 +67,24 @@ fun SuperButton( } } val isLightTheme = ElementTheme.isLightTheme - val colors = remember(isLightTheme) { - if (isLightTheme) { - listOf( - LightColorTokens.colorBlue900, - LightColorTokens.colorGreen1100, - ) - } else { - listOf( - DarkColorTokens.colorBlue900, - DarkColorTokens.colorGreen1100, - ) + val colors = if (LocalBuildMeta.current.isEnterpriseBuild) { + listOf( + ElementTheme.colors.textActionAccent, + ElementTheme.colors.textActionAccent, + ) + } else { + remember(isLightTheme) { + if (isLightTheme) { + listOf( + LightColorTokens.colorBlue900, + LightColorTokens.colorGreen1100, + ) + } else { + listOf( + DarkColorTokens.colorBlue900, + DarkColorTokens.colorGreen1100, + ) + } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt index 0f8f1e43d88..944fca1cf4a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.designsystem.components.dialogs +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -38,6 +39,7 @@ fun ListDialog( cancelText: String = stringResource(CommonStrings.action_cancel), submitText: String = stringResource(CommonStrings.action_ok), enabled: Boolean = true, + applyPaddingToContents: Boolean = true, listItems: LazyListScope.() -> Unit, ) { val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let { @@ -61,6 +63,7 @@ fun ListDialog( onSubmitClick = onSubmit, enabled = enabled, listItems = listItems, + applyPaddingToContents = applyPaddingToContents, ) } } @@ -72,8 +75,9 @@ private fun ListDialogContent( onSubmitClick: () -> Unit, cancelText: String, submitText: String, - title: String? = null, - enabled: Boolean = true, + title: String?, + enabled: Boolean, + applyPaddingToContents: Boolean, subtitle: @Composable (() -> Unit)? = null, ) { SimpleAlertDialogContent( @@ -84,10 +88,13 @@ private fun ListDialogContent( onCancelClick = onDismissRequest, onSubmitClick = onSubmitClick, enabled = enabled, - applyPaddingToContents = false, + applyPaddingToContents = applyPaddingToContents, ) { + // No start padding if padding is already applied to the content + val horizontalPadding = if (applyPaddingToContents) 0.dp else 8.dp LazyColumn( - modifier = Modifier.padding(start = 8.dp) + modifier = Modifier.padding(horizontal = horizontalPadding), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { listItems() } } } @@ -111,6 +118,8 @@ internal fun ListDialogContentPreview() { onSubmitClick = {}, cancelText = "Cancel", submitText = "Save", + enabled = true, + applyPaddingToContents = true, ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/TextFieldDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/TextFieldDialog.kt new file mode 100644 index 00000000000..bb31edd313d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/TextFieldDialog.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.dialogs + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.list.TextFieldListItem +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun TextFieldDialog( + title: String, + onSubmit: (String) -> Unit, + onDismissRequest: () -> Unit, + value: String?, + placeholder: String?, + modifier: Modifier = Modifier, + validation: (String?) -> Boolean = { true }, + onValidationErrorMessage: String? = null, + autoSelectOnDisplay: Boolean = true, + maxLines: Int = 1, + content: String? = null, + label: String? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + submitText: String = stringResource(CommonStrings.action_ok), +) { + val focusRequester = remember { FocusRequester() } + var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue( + value.orEmpty(), + selection = TextRange(value.orEmpty().length) + ) + ) + } + var error by rememberSaveable { mutableStateOf(if (!validation(value.orEmpty())) onValidationErrorMessage else null) } + var canRequestFocus by rememberSaveable { mutableStateOf(false) } + val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } } + ListDialog( + title = title, + onSubmit = { onSubmit(textFieldContents.text) }, + onDismissRequest = onDismissRequest, + enabled = canSubmit, + submitText = submitText, + modifier = modifier, + ) { + if (content != null) { + item { + Text( + text = content, + style = ElementTheme.materialTypography.bodyMedium, + ) + } + } + item { + TextFieldListItem( + placeholder = placeholder.orEmpty(), + label = label, + text = textFieldContents, + onTextChange = { + error = if (!validation(it.text)) onValidationErrorMessage else null + textFieldContents = it + }, + error = error, + keyboardOptions = keyboardOptions, + keyboardActions = KeyboardActions(onAny = { + if (validation(textFieldContents.text)) { + onSubmit(textFieldContents.text) + } + }), + maxLines = maxLines, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + canRequestFocus = true + } + } + + if (autoSelectOnDisplay && canRequestFocus) { + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + } +} + +@PreviewsDayNight +@Composable +internal fun TextFieldDialogPreview() = ElementPreview { + TextFieldDialog( + title = "Title", + value = "", + placeholder = "Placeholder", + onSubmit = {}, + onDismissRequest = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun TextFieldDialogWithErrorPreview() = ElementPreview { + TextFieldDialog( + title = "Title", + content = "Some content", + onSubmit = {}, + validation = { false }, + onDismissRequest = {}, + value = "Value", + placeholder = "Placeholder", + label = "Label", + onValidationErrorMessage = "Error message", + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/keyboard/Keyboard.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/keyboard/Keyboard.kt deleted file mode 100644 index 020f567c1c2..00000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/keyboard/Keyboard.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.designsystem.components.keyboard - -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.isImeVisible -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.rememberUpdatedState -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LocalLifecycleOwner - -/** - * Inspired from https://stackoverflow.com/questions/68847559/how-can-i-detect-keyboard-opening-and-closing-in-jetpack-compose - */ -enum class Keyboard { - Opened, - Closed -} - -// Note: it does not work as expected... -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun keyboardAsState(): State { - val lifecycle = LocalLifecycleOwner.current.lifecycle - val isResumed = lifecycle.currentState == Lifecycle.State.RESUMED - return rememberUpdatedState(if (WindowInsets.isImeVisible && isResumed) Keyboard.Opened else Keyboard.Closed) -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/CheckboxListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/CheckboxListItem.kt index 490fdfd9d64..41cef050ffa 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/CheckboxListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/CheckboxListItem.kt @@ -29,7 +29,11 @@ fun CheckboxListItem( modifier = modifier, headlineContent = { Text(headline) }, supportingContent = supportingText?.let { @Composable { Text(it) } }, - leadingContent = ListItemContent.Checkbox(checked, null, enabled, compact = compactLayout), + leadingContent = ListItemContent.Checkbox( + checked = checked, + enabled = enabled, + compact = compactLayout, + ), trailingContent = trailingContent, style = style, enabled = enabled, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt index 651bcd5f5cc..5b9efc51c84 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/ListItemContent.kt @@ -10,10 +10,12 @@ package io.element.android.libraries.designsystem.components.list import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp @@ -35,26 +37,22 @@ sealed interface ListItemContent { /** * Default Switch content for [ListItem]. * @param checked The current state of the switch. - * @param onChange Callback when the switch is toggled: it should only be set to override the default click behaviour in the [ListItem]. * @param enabled Whether the switch is enabled or not. */ data class Switch( val checked: Boolean, - val onChange: ((Boolean) -> Unit)? = null, val enabled: Boolean = true ) : ListItemContent /** * Default Checkbox content for [ListItem]. * @param checked The current state of the checkbox. - * @param onChange Callback when the checkbox is toggled: it should only be set to override the default click behaviour in the [ListItem]. * @param enabled Whether the checkbox is enabled or not. * @param compact Reduces the size of the component to make the wrapping [ListItem] smaller. * This is especially useful when the [ListItem] is used inside a Dialog. `false` by default. */ data class Checkbox( val checked: Boolean, - val onChange: ((Boolean) -> Unit)? = null, val enabled: Boolean = true, val compact: Boolean = false ) : ListItemContent @@ -62,14 +60,12 @@ sealed interface ListItemContent { /** * Default RadioButton content for [ListItem]. * @param selected The current state of the radio button. - * @param onClick Callback when the radio button is toggled: it should only be set to override the default click behaviour in the [ListItem]. * @param enabled Whether the radio button is enabled or not. * @param compact Reduces the size of the component to make the wrapping [ListItem] smaller. * This is especially useful when the [ListItem] is used inside a Dialog. `false` by default. */ data class RadioButton( val selected: Boolean, - val onClick: (() -> Unit)? = null, val enabled: Boolean = true, val compact: Boolean = false ) : ListItemContent @@ -77,8 +73,9 @@ sealed interface ListItemContent { /** * Default Icon content for [ListItem]. Sets the Icon component to a predefined size. * @param iconSource The icon to display, using [IconSource.getPainter]. + * @param tintColor The tint color for the icon, if any. Defaults to `null`. */ - data class Icon(val iconSource: IconSource) : ListItemContent + data class Icon(val iconSource: IconSource, val tintColor: Color? = null) : ListItemContent /** * Default Text content for [ListItem]. Sets the Text component to a max size and clips overflow. @@ -96,30 +93,31 @@ sealed interface ListItemContent { data class Counter(val count: Int) : ListItemContent @Composable - fun View() { + fun View(isItemEnabled: Boolean) { when (this) { is Switch -> SwitchComponent( checked = checked, - onCheckedChange = onChange, - enabled = enabled + onCheckedChange = null, + enabled = enabled && isItemEnabled, ) is Checkbox -> CheckboxComponent( modifier = if (compact) Modifier.size(maxCompactSize) else Modifier, checked = checked, - onCheckedChange = onChange, - enabled = enabled + onCheckedChange = null, + enabled = enabled && isItemEnabled, ) is RadioButton -> RadioButtonComponent( modifier = if (compact) Modifier.size(maxCompactSize) else Modifier, selected = selected, - onClick = onClick, - enabled = enabled + onClick = null, + enabled = enabled && isItemEnabled, ) is Icon -> { IconComponent( modifier = Modifier.size(maxCompactSize), painter = iconSource.getPainter(), - contentDescription = iconSource.contentDescription + contentDescription = iconSource.contentDescription, + tint = tintColor ?: LocalContentColor.current, ) } is Text -> TextComponent(modifier = Modifier.widthIn(max = 128.dp), text = text, maxLines = 1, overflow = TextOverflow.Ellipsis) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/RadioButtonListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/RadioButtonListItem.kt index d711fd54d4f..9cbfe2edda3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/RadioButtonListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/RadioButtonListItem.kt @@ -29,7 +29,11 @@ fun RadioButtonListItem( modifier = modifier, headlineContent = { Text(headline) }, supportingContent = supportingText?.let { @Composable { Text(it) } }, - leadingContent = ListItemContent.RadioButton(selected, null, enabled, compact = compactLayout), + leadingContent = ListItemContent.RadioButton( + selected = selected, + enabled = enabled, + compact = compactLayout, + ), trailingContent = trailingContent, style = style, enabled = enabled, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SwitchListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SwitchListItem.kt index 3db9d8e7f75..070e112736d 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SwitchListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/SwitchListItem.kt @@ -29,7 +29,10 @@ fun SwitchListItem( headlineContent = { Text(headline) }, supportingContent = supportingText?.let { @Composable { Text(it) } }, leadingContent = leadingContent, - trailingContent = ListItemContent.Switch(value, null, enabled), + trailingContent = ListItemContent.Switch( + checked = value, + enabled = enabled, + ), style = style, enabled = enabled, onClick = { onChange(!value) }, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt index 299b41ad943..f9d61b6593e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt @@ -9,17 +9,14 @@ package io.element.android.libraries.designsystem.components.list import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview -import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup -import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TextFieldValidity @Composable fun TextFieldListItem( @@ -29,26 +26,19 @@ fun TextFieldListItem( modifier: Modifier = Modifier, error: String? = null, maxLines: Int = 1, + label: String? = null, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, ) { - val textFieldStyle = ElementTheme.materialTypography.bodyLarge - - OutlinedTextField( + TextField( value = text, onValueChange = { onTextChange(it) }, - placeholder = placeholder?.let { @Composable { Text(it) } }, - colors = OutlinedTextFieldDefaults.colors( - disabledBorderColor = Color.Transparent, - errorBorderColor = Color.Transparent, - focusedBorderColor = Color.Transparent, - unfocusedBorderColor = Color.Transparent, - ), - isError = error != null, - supportingText = error?.let { @Composable { Text(it) } }, + placeholder = placeholder, + label = label, + validity = if (error != null) TextFieldValidity.Invalid else TextFieldValidity.None, + supportingText = error, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, - textStyle = textFieldStyle, maxLines = maxLines, singleLine = maxLines == 1, modifier = modifier, @@ -63,26 +53,19 @@ fun TextFieldListItem( modifier: Modifier = Modifier, error: String? = null, maxLines: Int = 1, + label: String? = null, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, ) { - val textFieldStyle = ElementTheme.materialTypography.bodyLarge - - OutlinedTextField( + TextField( value = text, onValueChange = { onTextChange(it) }, - placeholder = placeholder?.let { @Composable { Text(it) } }, - colors = OutlinedTextFieldDefaults.colors( - disabledBorderColor = Color.Transparent, - errorBorderColor = Color.Transparent, - focusedBorderColor = Color.Transparent, - unfocusedBorderColor = Color.Transparent, - ), - isError = error != null, - supportingText = error?.let { @Composable { Text(it) } }, + placeholder = placeholder, + label = label, + validity = if (error != null) TextFieldValidity.Invalid else TextFieldValidity.None, + supportingText = error, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, - textStyle = textFieldStyle, maxLines = maxLines, singleLine = maxLines == 1, modifier = modifier, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt index 8986db82eed..767b56410f5 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset @@ -60,7 +59,6 @@ private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F * @param lineWidth The width of the waveform lines. * @param linePadding The padding between waveform lines. */ -@OptIn(ExperimentalComposeUiApi::class) @Composable fun WaveformPlaybackView( playbackProgress: Float, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt index ce9f612de21..1a993899e80 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt @@ -47,10 +47,6 @@ internal fun PreferenceCategoryPreview() = ElementThemedPreview { PreferenceCategory( title = "Category title", ) { - PreferenceText( - title = "Title", - icon = CompoundIcons.ChatProblem(), - ) PreferenceSwitch( title = "Switch", icon = CompoundIcons.Threads(), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCheckbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCheckbox.kt index 8ba3c9ca59a..f70b87e8127 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCheckbox.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCheckbox.kt @@ -65,6 +65,7 @@ fun PreferenceCheckbox( checked = isChecked, enabled = enabled, ), + enabled = enabled, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceDropdown.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceDropdown.kt index 44bd714f269..7be47338ca3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceDropdown.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceDropdown.kt @@ -99,9 +99,10 @@ fun PreferenceDropdown( */ interface DropdownOption { /** - * The text to display for this option. + * Returns the text to be displayed for this option. */ - val text: String + @Composable + fun getText(): String } @Composable @@ -123,7 +124,7 @@ private fun DropdownTrailingContent( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = selectedOption?.text.orEmpty(), + text = selectedOption?.getText().orEmpty(), maxLines = 1, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary, @@ -139,7 +140,7 @@ private fun DropdownTrailingContent( DropdownMenuItem( text = { Text( - text = option.text, + text = option.getText(), style = ElementTheme.typography.fontBodyMdRegular ) }, @@ -158,13 +159,16 @@ private fun DropdownTrailingContent( internal fun PreferenceDropdownPreview() = ElementThemedPreview { val options = listOf( object : DropdownOption { - override val text = "Option 1" + @Composable + override fun getText(): String = "Option 1" }, object : DropdownOption { - override val text = "Option 2" + @Composable + override fun getText(): String = "Option 2" }, object : DropdownOption { - override val text = "Option 3" + @Composable + override fun getText(): String = "Option 3" }, ).toImmutableList() diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferencePage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferencePage.kt index 27e873be9bb..b81c3e6b865 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferencePage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferencePage.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -78,6 +80,9 @@ private fun PreferenceTopAppBar( }, title = { Text( + modifier = Modifier.semantics { + heading() + }, text = title, style = ElementTheme.typography.aliasScreenTitle, maxLines = 1, @@ -97,11 +102,6 @@ internal fun PreferencePagePreview() = ElementPreview { PreferenceCategory( title = "Category title", ) { - PreferenceText( - title = "Title", - subtitle = "Some other text", - icon = CompoundIcons.ChatProblem(), - ) PreferenceDivider() PreferenceSwitch( title = "Switch", diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt deleted file mode 100644 index a5d793445ec..00000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt +++ /dev/null @@ -1,216 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.designsystem.components.preferences - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.progressSemantics -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.AnnotatedString -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.tokens.generated.CompoundIcons -import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage -import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom -import io.element.android.libraries.designsystem.components.list.ListItemContent -import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.preview.PreviewGroup -import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator -import io.element.android.libraries.designsystem.theme.components.ListItem -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.toEnabledColor -import io.element.android.libraries.designsystem.toSecondaryEnabledColor - -@Composable -fun PreferenceText( - title: String, - modifier: Modifier = Modifier, - enabled: Boolean = true, - subtitle: String? = null, - subtitleAnnotated: AnnotatedString? = null, - currentValue: String? = null, - loadingCurrentValue: Boolean = false, - icon: ImageVector? = null, - @DrawableRes iconResourceId: Int? = null, - showIconAreaIfNoIcon: Boolean = false, - showIconBadge: Boolean = false, - showEndBadge: Boolean = false, - tintColor: Color? = null, - onClick: () -> Unit = {}, -) { - ListItem( - modifier = modifier, - enabled = enabled, - onClick = onClick, - leadingContent = preferenceIcon( - icon = icon, - iconResourceId = iconResourceId, - showIconBadge = showIconBadge, - enabled = enabled, - showIconAreaIfNoIcon = showIconAreaIfNoIcon, - tintColor = tintColor, - ), - headlineContent = { - Text( - style = ElementTheme.typography.fontBodyLgRegular, - text = title, - color = tintColor ?: enabled.toEnabledColor(), - ) - }, - supportingContent = if (subtitle != null) { - { - Text( - style = ElementTheme.typography.fontBodyMdRegular, - text = subtitle, - color = tintColor ?: enabled.toSecondaryEnabledColor(), - ) - } - } else { - subtitleAnnotated?.let { - { - Text( - style = ElementTheme.typography.fontBodyMdRegular, - text = it, - color = tintColor ?: enabled.toSecondaryEnabledColor(), - ) - } - } - }, - trailingContent = if (currentValue != null || loadingCurrentValue || showEndBadge) { - ListItemContent.Custom { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - if (currentValue != null) { - Text( - text = currentValue, - style = ElementTheme.typography.fontBodyXsMedium, - color = enabled.toSecondaryEnabledColor(), - ) - } else if (loadingCurrentValue) { - CircularProgressIndicator( - modifier = Modifier - .progressSemantics() - .size(20.dp), - strokeWidth = 2.dp - ) - } - if (showEndBadge) { - val endBadgeStartPadding = if (currentValue != null || loadingCurrentValue) 16.dp else 0.dp - RedIndicatorAtom( - modifier = Modifier - .padding(start = endBadgeStartPadding) - ) - } - } - } - } else { - null - } - ) -} - -@Preview(group = PreviewGroup.Preferences) -@Composable -internal fun PreferenceTextLightPreview() = ElementPreviewLight { - ContentToPreview(showEndBadge = false) -} - -@Preview(group = PreviewGroup.Preferences) -@Composable -internal fun PreferenceTextDarkPreview() = ElementPreviewDark { - ContentToPreview(showEndBadge = false) -} - -@Preview(group = PreviewGroup.Preferences) -@Composable -internal fun PreferenceTextWithEndBadgeLightPreview() = ElementPreviewLight { - ContentToPreview(showEndBadge = true) -} - -@Preview(group = PreviewGroup.Preferences) -@Composable -internal fun PreferenceTextWithEndBadgeDarkPreview() = ElementPreviewDark { - ContentToPreview(showEndBadge = true) -} - -@ExcludeFromCoverage -@Composable -private fun ContentToPreview(showEndBadge: Boolean) { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - PreferenceText( - title = "Title", - icon = CompoundIcons.ChatProblem(), - showEndBadge = showEndBadge, - ) - PreferenceText( - title = "Title", - subtitle = "Some content", - icon = CompoundIcons.ChatProblem(), - showEndBadge = showEndBadge, - ) - PreferenceText( - title = "Title", - subtitle = "Some content", - icon = CompoundIcons.ChatProblem(), - currentValue = "123", - showEndBadge = showEndBadge, - ) - PreferenceText( - title = "Title", - subtitle = "Some content", - icon = CompoundIcons.ChatProblem(), - currentValue = "123", - enabled = false, - showEndBadge = showEndBadge, - ) - PreferenceText( - title = "Title", - subtitle = "Some content", - icon = CompoundIcons.ChatProblem(), - loadingCurrentValue = true, - showEndBadge = showEndBadge, - ) - PreferenceText( - title = "Title", - icon = CompoundIcons.ChatProblem(), - currentValue = "123", - showEndBadge = showEndBadge, - ) - PreferenceText( - title = "Title", - icon = CompoundIcons.ChatProblem(), - loadingCurrentValue = true, - showEndBadge = showEndBadge, - ) - PreferenceText( - title = "Title no icon with icon area", - showIconAreaIfNoIcon = true, - loadingCurrentValue = true, - showEndBadge = showEndBadge, - ) - PreferenceText( - title = "Title no icon", - loadingCurrentValue = true, - showEndBadge = showEndBadge, - ) - } -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt index 8810c83f547..98f324f88a9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt @@ -7,24 +7,15 @@ package io.element.android.libraries.designsystem.components.preferences -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import io.element.android.libraries.designsystem.components.dialogs.ListDialog +import io.element.android.libraries.designsystem.components.dialogs.TextFieldDialog import io.element.android.libraries.designsystem.components.list.ListItemContent -import io.element.android.libraries.designsystem.components.list.TextFieldListItem 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 @@ -74,58 +65,3 @@ fun PreferenceTextField( ) } } - -@Composable -private fun TextFieldDialog( - title: String, - onSubmit: (String) -> Unit, - onDismissRequest: () -> Unit, - value: String?, - placeholder: String?, - validation: (String?) -> Boolean = { true }, - onValidationErrorMessage: String? = null, - autoSelectOnDisplay: Boolean = true, - maxLines: Int = 1, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, -) { - val focusRequester = remember { FocusRequester() } - var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(value.orEmpty(), selection = TextRange(value.orEmpty().length))) - } - var error by rememberSaveable { mutableStateOf(null) } - var canRequestFocus by rememberSaveable { mutableStateOf(false) } - val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } } - ListDialog( - title = title, - onSubmit = { onSubmit(textFieldContents.text) }, - onDismissRequest = onDismissRequest, - enabled = canSubmit, - ) { - item { - TextFieldListItem( - placeholder = placeholder.orEmpty(), - text = textFieldContents, - onTextChange = { - error = if (!validation(it.text)) onValidationErrorMessage else null - textFieldContents = it - }, - error = error, - keyboardOptions = keyboardOptions, - keyboardActions = KeyboardActions(onAny = { - if (validation(textFieldContents.text)) { - onSubmit(textFieldContents.text) - } - }), - maxLines = maxLines, - modifier = Modifier.focusRequester(focusRequester), - ) - canRequestFocus = true - } - } - - if (autoSelectOnDisplay && canRequestFocus) { - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - } -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt index a174d0bea7e..2e4fff1e923 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt @@ -25,7 +25,7 @@ import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.toSecondaryEnabledColor +import io.element.android.libraries.designsystem.toIconSecondaryEnabledColor @Composable fun preferenceIcon( @@ -68,7 +68,7 @@ private fun PreferenceIcon( imageVector = icon, resourceId = iconResourceId, contentDescription = null, - tint = tintColor ?: enabled.toSecondaryEnabledColor(), + tint = tintColor ?: enabled.toIconSecondaryEnabledColor(), modifier = Modifier .size(24.dp), ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsList.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsList.kt index a1b921988a7..ff98ab5b1e8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsList.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsList.kt @@ -12,10 +12,7 @@ import io.element.android.libraries.designsystem.R // This list and all the drawable it contains should be removed at some point. // All the icons should be defined in Compound. internal val iconsOther = listOf( - R.drawable.ic_cancel, - R.drawable.ic_encryption_enabled, - R.drawable.ic_notification_small, - R.drawable.ic_plus_composer, + R.drawable.ic_notification, R.drawable.ic_stop, R.drawable.pin, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt deleted file mode 100644 index 5436a0330c7..00000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.designsystem.modifiers - -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillNode -import androidx.compose.ui.autofill.AutofillType -import androidx.compose.ui.composed -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalAutofill -import androidx.compose.ui.platform.LocalAutofillTree - -@Suppress("ModifierComposed") -@OptIn(ExperimentalComposeUiApi::class) -fun Modifier.autofill(autofillTypes: List, onFill: (String) -> Unit) = composed { - val autofillNode = AutofillNode(autofillTypes, onFill = onFill) - LocalAutofillTree.current += autofillNode - - val autofill = LocalAutofill.current - - this - .onGloballyPositioned { - // Inform autofill framework of where our composable is so it can show the popup in the right place - autofillNode.boundingBox = it.boundsInWindow() - } - .onFocusChanged { - autofill?.run { - if (it.isFocused) { - requestAutofillForNode(autofillNode) - } else { - cancelAutofillForNode(autofillNode) - } - } - } -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Clickable.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Clickable.kt index c8f71f16cc9..5789a4525dd 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Clickable.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Clickable.kt @@ -8,12 +8,24 @@ package io.element.android.libraries.designsystem.modifiers import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp -fun Modifier.clickableIfNotNull(onClick: (() -> Unit)? = null): Modifier = then( +fun Modifier.clickableIfNotNull(onClick: (() -> Unit)? = null): Modifier = this.then( if (onClick != null) { Modifier.clickable { onClick() } } else { Modifier } ) + +fun Modifier.niceClickable( + onClick: () -> Unit, +): Modifier { + return clip(RoundedCornerShape(4.dp)) + .clickable { onClick() } + .padding(horizontal = 4.dp) +} 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 6a25dd72fe3..788b8fee555 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 @@ -7,24 +7,43 @@ package io.element.android.libraries.designsystem.preview +import androidx.annotation.DrawableRes import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.res.ResourcesCompat +import coil3.annotation.ExperimentalCoilApi +import coil3.asImage +import coil3.compose.AsyncImagePreviewHandler +import coil3.compose.LocalAsyncImagePreviewHandler import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.utils.CommonDrawables +@OptIn(ExperimentalCoilApi::class) @Composable @Suppress("ModifierMissing") fun ElementPreview( darkTheme: Boolean = isSystemInDarkTheme(), showBackground: Boolean = true, + @DrawableRes + drawableFallbackForImages: Int = CommonDrawables.sample_background, content: @Composable () -> Unit ) { - ElementTheme(darkTheme = darkTheme) { - if (showBackground) { - // If we have a proper contentColor applied we need a Surface instead of a Box - Surface(content = content) - } else { - content() + val context = LocalContext.current + CompositionLocalProvider( + LocalAsyncImagePreviewHandler provides AsyncImagePreviewHandler { + ResourcesCompat.getDrawable(context.resources, drawableFallbackForImages, null)!!.asImage() + } + ) { + ElementTheme(darkTheme = darkTheme) { + if (showBackground) { + // If we have a proper contentColor applied we need a Surface instead of a Box + Surface(content = content) + } else { + 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 866735e69f6..7ab7d323e38 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 @@ -7,6 +7,7 @@ package io.element.android.libraries.designsystem.preview +import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,12 +20,15 @@ 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.libraries.designsystem.utils.CommonDrawables @Composable @Suppress("ModifierMissing") fun ElementThemedPreview( showBackground: Boolean = true, vertical: Boolean = true, + @DrawableRes + drawableFallbackForImages: Int = CommonDrawables.sample_background, content: @Composable () -> Unit, ) { Box( @@ -37,12 +41,14 @@ fun ElementThemedPreview( ElementPreview( darkTheme = false, showBackground = showBackground, + drawableFallbackForImages = drawableFallbackForImages, content = content, ) Spacer(modifier = Modifier.height(4.dp)) ElementPreview( darkTheme = true, showBackground = showBackground, + drawableFallbackForImages = drawableFallbackForImages, content = content ) } @@ -51,12 +57,14 @@ fun ElementThemedPreview( ElementPreview( darkTheme = false, showBackground = showBackground, + drawableFallbackForImages = drawableFallbackForImages, content = content, ) Spacer(modifier = Modifier.width(4.dp)) ElementPreview( darkTheme = true, showBackground = showBackground, + drawableFallbackForImages = drawableFallbackForImages, content = content ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/Images.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/Images.kt deleted file mode 100644 index 74751deb06e..00000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/Images.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.designsystem.preview - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.painterResource -import io.element.android.libraries.designsystem.R - -/** - * I wanted to set up a FakeImageLoader as per https://github.com/coil-kt/coil/issues/1327 - * but it does not render in preview. In the meantime, you can use this trick to have image. - */ -@Composable -fun debugPlaceholder( - @DrawableRes debugPreview: Int, - nonDebugPainter: Painter? = null, -) = if (LocalInspectionMode.current) { - painterResource(id = debugPreview) -} else { - nonDebugPainter -} - -@Composable -fun debugPlaceholderBackground(nonDebugPainter: Painter? = null): Painter? { - return debugPlaceholder(debugPreview = R.drawable.sample_background, nonDebugPainter) -} - -@Composable -fun debugPlaceholderAvatar(nonDebugPainter: Painter? = null): Painter? { - return debugPlaceholder(debugPreview = R.drawable.sample_avatar, nonDebugPainter) -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt index 36ac7d71bd3..79f70cc2f5b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.designsystem.text import android.graphics.Typeface -import android.text.SpannableString +import android.text.SpannedString import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import android.text.style.UnderlineSpan @@ -26,7 +26,7 @@ import io.element.android.compound.theme.LinkColor fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString { append(this@toAnnotatedString) - val spannable = SpannableString(this@toAnnotatedString) + val spannable = SpannedString.valueOf(this@toAnnotatedString) spannable.getSpans(0, spannable.length, Any::class.java).forEach { span -> val start = spannable.getSpanStart(span) val end = spannable.getSpanEnd(span) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt index 258d24cb99d..856160d7bfb 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview @@ -27,6 +28,7 @@ import io.element.android.libraries.designsystem.theme.components.Text * will be smaller. */ @Composable +@ReadOnlyComposable fun Dp.applyScaleDown(): Dp = with(LocalDensity.current) { return this@applyScaleDown * fontScale.coerceAtMost(1f) } @@ -37,6 +39,7 @@ fun Dp.applyScaleDown(): Dp = with(LocalDensity.current) { * will be bigger. */ @Composable +@ReadOnlyComposable fun Dp.applyScaleUp(): Dp = with(LocalDensity.current) { return this@applyScaleUp * fontScale.coerceAtLeast(1f) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/UnitConverters.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/UnitConverters.kt index c358517946f..eaf67128307 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/UnitConverters.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/UnitConverters.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.designsystem.text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit @@ -17,6 +18,7 @@ import androidx.compose.ui.unit.TextUnit * Can be used for instance to use Dp unit for text. */ @Composable +@ReadOnlyComposable fun Dp.toSp(): TextUnit = with(LocalDensity.current) { toSp() } /** @@ -24,22 +26,26 @@ fun Dp.toSp(): TextUnit = with(LocalDensity.current) { toSp() } * Can be used for instance to use Sp unit for size. */ @Composable +@ReadOnlyComposable fun TextUnit.toDp(): Dp = with(LocalDensity.current) { toDp() } /** * Convert Px value to Dp, regarding current density. */ @Composable +@ReadOnlyComposable fun Int.toDp(): Dp = with(LocalDensity.current) { toDp() } /** * Convert Dp value to pixels, regarding current density. */ @Composable +@ReadOnlyComposable fun Dp.toPx(): Float = with(LocalDensity.current) { toPx() } /** * Convert Dp value to pixels, regarding current density. */ @Composable +@ReadOnlyComposable fun Dp.roundToPx(): Int = with(LocalDensity.current) { roundToPx() } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt index 29d3633cc04..5576a565190 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.designsystem.theme -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import io.element.android.compound.annotations.CoreColorToken @@ -24,13 +23,13 @@ import kotlinx.collections.immutable.persistentMapOf * Room list. */ @Composable -fun MaterialTheme.roomListRoomName() = colorScheme.primary +fun ElementTheme.roomListRoomName() = colors.textPrimary @Composable -fun MaterialTheme.roomListRoomMessage() = colorScheme.secondary +fun ElementTheme.roomListRoomMessage() = colors.textSecondary @Composable -fun MaterialTheme.roomListRoomMessageDate() = colorScheme.secondary +fun ElementTheme.roomListRoomMessageDate() = colors.textSecondary val SemanticColors.unreadIndicator get() = iconAccentTertiary @@ -39,77 +38,42 @@ val SemanticColors.placeholderBackground get() = bgSubtleSecondary // This color is not present in Semantic color, so put hard-coded value for now +@OptIn(CoreColorToken::class) val SemanticColors.messageFromMeBackground - get() = if (isLight) { - // We want LightDesignTokens.colorGray400 - Color(0xFFE1E6EC) - } else { - // We want DarkDesignTokens.colorGray500 - Color(0xFF323539) - } + get() = if (isLight) LightColorTokens.colorGray400 else DarkColorTokens.colorGray500 // This color is not present in Semantic color, so put hard-coded value for now +@OptIn(CoreColorToken::class) val SemanticColors.messageFromOtherBackground - get() = if (isLight) { - // We want LightDesignTokens.colorGray300 - Color(0xFFF0F2F5) - } else { - // We want DarkDesignTokens.colorGray400 - Color(0xFF26282D) - } + get() = if (isLight) LightColorTokens.colorGray300 else DarkColorTokens.colorGray400 // This color is not present in Semantic color, so put hard-coded value for now +@OptIn(CoreColorToken::class) val SemanticColors.progressIndicatorTrackColor - get() = if (isLight) { - // We want LightDesignTokens.colorAlphaGray500 - Color(0x33052448) - } else { - // We want DarkDesignTokens.colorAlphaGray500 - Color(0x25F4F7FA) - } + get() = if (isLight) LightColorTokens.colorAlphaGray500 else DarkColorTokens.colorAlphaGray500 // This color is not present in Semantic color, so put hard-coded value for now +@OptIn(CoreColorToken::class) val SemanticColors.iconSuccessPrimaryBackground - get() = if (isLight) { - // We want LightDesignTokens.colorGreen300 - Color(0xffe3f7ed) - } else { - // We want DarkDesignTokens.colorGreen300 - Color(0xff002513) - } + get() = if (isLight) LightColorTokens.colorGreen300 else DarkColorTokens.colorGreen300 // This color is not present in Semantic color, so put hard-coded value for now +@OptIn(CoreColorToken::class) val SemanticColors.bgSubtleTertiary - get() = if (isLight) { - // We want LightDesignTokens.colorGray100 - Color(0xfffbfcfd) - } else { - // We want DarkDesignTokens.colorGray100 - Color(0xff14171b) - } + get() = if (isLight) LightColorTokens.colorGray100 else DarkColorTokens.colorGray100 // Temporary color, which is not in the token right now val SemanticColors.temporaryColorBgSpecial get() = if (isLight) Color(0xFFE4E8F0) else Color(0xFF3A4048) // This color is not present in Semantic color, so put hard-coded value for now +@OptIn(CoreColorToken::class) val SemanticColors.pinDigitBg - get() = if (isLight) { - // We want LightDesignTokens.colorGray300 - Color(0xFFF0F2F5) - } else { - // We want DarkDesignTokens.colorGray400 - Color(0xFF26282D) - } + get() = if (isLight) LightColorTokens.colorGray300 else DarkColorTokens.colorGray400 +@OptIn(CoreColorToken::class) val SemanticColors.currentUserMentionPillText - get() = if (isLight) { - // We want LightDesignTokens.colorGreen1100 - Color(0xff005c45) - } else { - // We want DarkDesignTokens.colorGreen1100 - Color(0xff1fc090) - } + get() = if (isLight) LightColorTokens.colorGreen1100 else DarkColorTokens.colorGreen1100 val SemanticColors.currentUserMentionPillBackground get() = if (isLight) { @@ -142,14 +106,6 @@ val SemanticColors.highlightedMessageBackgroundColor // Badge colors -@OptIn(CoreColorToken::class) -val SemanticColors.badgePositiveBackgroundColor - get() = if (isLight) LightColorTokens.colorAlphaGreen300 else DarkColorTokens.colorAlphaGreen300 - -@OptIn(CoreColorToken::class) -val SemanticColors.badgePositiveContentColor - get() = if (isLight) LightColorTokens.colorGreen1100 else DarkColorTokens.colorGreen1100 - @OptIn(CoreColorToken::class) val SemanticColors.badgeNeutralBackgroundColor get() = if (isLight) LightColorTokens.colorAlphaGray300 else DarkColorTokens.colorAlphaGray300 @@ -181,9 +137,9 @@ internal fun ColorAliasesPreview() = ElementPreview { backgroundColor = Color.Black, foregroundColor = Color.White, colors = persistentMapOf( - "roomListRoomName" to MaterialTheme.roomListRoomName(), - "roomListRoomMessage" to MaterialTheme.roomListRoomMessage(), - "roomListRoomMessageDate" to MaterialTheme.roomListRoomMessageDate(), + "roomListRoomName" to ElementTheme.roomListRoomName(), + "roomListRoomMessage" to ElementTheme.roomListRoomMessage(), + "roomListRoomMessageDate" to ElementTheme.roomListRoomMessageDate(), "unreadIndicator" to ElementTheme.colors.unreadIndicator, "placeholderBackground" to ElementTheme.colors.placeholderBackground, "messageFromMeBackground" to ElementTheme.colors.messageFromMeBackground, 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 2c69b8575ae..bb0fcd971be 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 @@ -9,16 +9,40 @@ package io.element.android.libraries.designsystem.theme import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue 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.features.enterprise.api.EnterpriseService +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.preferences.api.store.AppPreferencesStore +val LocalBuildMeta = staticCompositionLocalOf { + BuildMeta( + isDebuggable = true, + buildType = BuildType.DEBUG, + applicationName = "MyApp", + productionApplicationName = "MyAppProd", + desktopApplicationName = "MyAppDesktop", + applicationId = "AppId", + isEnterpriseBuild = false, + lowPrivacyLoggingEnabled = false, + versionName = "aVersion", + versionCode = 123, + gitRevision = "aRevision", + gitBranchName = "aBranch", + flavorDescription = "aFlavor", + flavorShortDescription = "aFlavorShort", + ) +} + /** * Theme to use for all the regular screens of the application. * Will manage the light / dark theme based on the user preference. @@ -29,6 +53,8 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore @Composable fun ElementThemeApp( appPreferencesStore: AppPreferencesStore, + enterpriseService: EnterpriseService, + buildMeta: BuildMeta, content: @Composable () -> Unit, ) { val theme by remember { @@ -44,8 +70,16 @@ fun ElementThemeApp( } ) } - ElementTheme( - darkTheme = theme.isDark(), - content = content, - ) + val compoundLight = remember { enterpriseService.semanticColorsLight() } + val compoundDark = remember { enterpriseService.semanticColorsDark() } + CompositionLocalProvider( + LocalBuildMeta provides buildMeta, + ) { + ElementTheme( + darkTheme = theme.isDark(), + content = content, + compoundLight = compoundLight, + compoundDark = compoundDark, + ) + } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt index 7936c25fcb9..51fe275ee14 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt @@ -159,7 +159,7 @@ internal fun SimpleAlertDialogContent( // most cases, TextButtons should be used for dismiss and confirm buttons. // TextButtons will not consume this provided content color value, and will used their // own defined or default colors. - buttonContentColor = MaterialTheme.colorScheme.primary, + buttonContentColor = ElementTheme.colors.textPrimary, applyPaddingToContents = applyPaddingToContents, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt index 5ac1dedb4a4..d4db8923a00 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt @@ -10,19 +10,18 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.contentColorFor +import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp -import io.element.android.libraries.designsystem.theme.components.bottomsheet.BottomSheetScaffoldState -import io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomBottomSheetScaffold -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState @Composable @ExperimentalMaterial3Api @@ -44,7 +43,7 @@ fun BottomSheetScaffold( contentColor: Color = contentColorFor(containerColor), content: @Composable (PaddingValues) -> Unit ) { - CustomBottomSheetScaffold( + androidx.compose.material3.BottomSheetScaffold( sheetContent = sheetContent, modifier = modifier, scaffoldState = scaffoldState, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt index 4a198379cac..8b1ca525153 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt @@ -314,7 +314,7 @@ internal enum class ButtonStyle { contentColor = if (destructive) { ElementTheme.colors.textCriticalPrimary } else { - if (LocalContentColor.current.isSpecified) LocalContentColor.current else ElementTheme.materialColors.primary + if (LocalContentColor.current.isSpecified) LocalContentColor.current else ElementTheme.colors.textPrimary }, disabledContainerColor = Color.Transparent, disabledContentColor = getDisabledContentColor(destructive), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconColorButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconColorButton.kt new file mode 100644 index 00000000000..cc863bcf8d5 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconColorButton.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +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 + +/** + * Button with colored background. + * Figma: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=1956-37586 + */ +@Composable +fun IconColorButton( + onClick: () -> Unit, + imageVector: ImageVector, + contentDescription: String?, + modifier: Modifier = Modifier, + buttonSize: ButtonSize = ButtonSize.Large, + iconColorButtonStyle: IconColorButtonStyle = IconColorButtonStyle.Primary, +) { + val bgColor = when (iconColorButtonStyle) { + IconColorButtonStyle.Primary -> ElementTheme.colors.iconPrimary + IconColorButtonStyle.Secondary -> ElementTheme.colors.iconSecondary + IconColorButtonStyle.Disabled -> ElementTheme.colors.iconDisabled + } + IconButton( + modifier = modifier.size(48.dp), + onClick = onClick, + ) { + Icon( + modifier = Modifier + .clip(CircleShape) + .size(buttonSize.toContainerSize()) + .background(bgColor) + .padding(buttonSize.toContainerPadding()), + imageVector = imageVector, + contentDescription = contentDescription, + tint = ElementTheme.colors.iconOnSolidPrimary + ) + } +} + +enum class IconColorButtonStyle { + Primary, + Secondary, + Disabled, +} + +private fun ButtonSize.toContainerSize() = when (this) { + ButtonSize.Small -> 20.dp + ButtonSize.Medium -> 24.dp + ButtonSize.Large, + ButtonSize.MediumLowPadding, + ButtonSize.LargeLowPadding -> 30.dp +} + +private fun ButtonSize.toContainerPadding() = when (this) { + ButtonSize.Small -> 2.dp + ButtonSize.Medium -> 2.dp + ButtonSize.Large, + ButtonSize.MediumLowPadding, + ButtonSize.LargeLowPadding -> 3.dp +} + +@PreviewsDayNight +@Composable +internal fun IconColorButtonPreview() = ElementPreview { + Column { + listOf( + IconColorButtonStyle.Primary, + IconColorButtonStyle.Secondary, + IconColorButtonStyle.Disabled, + ).forEach { style -> + Row( + modifier = Modifier.padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + listOf(ButtonSize.Large, ButtonSize.Medium, ButtonSize.Small).forEach { size -> + IconColorButton( + onClick = {}, + imageVector = CompoundIcons.Close(), + contentDescription = null, + buttonSize = size, + iconColorButtonStyle = style, + ) + } + } + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt index 18e503d26a4..2c9a030439e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt @@ -11,9 +11,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.RadioButtonUnchecked import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconToggleButtonColors import androidx.compose.runtime.Composable @@ -24,6 +21,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup @@ -56,7 +54,7 @@ internal fun IconToggleButtonPreview() = ElementThemedPreview(vertical = false) Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { val icon: @Composable () -> Unit = { Icon( - imageVector = if (checked) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked, + imageVector = if (checked) CompoundIcons.CheckCircleSolid() else CompoundIcons.Circle(), contentDescription = null ) } @@ -66,7 +64,7 @@ internal fun IconToggleButtonPreview() = ElementThemedPreview(vertical = false) Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { val icon: @Composable () -> Unit = { Icon( - imageVector = if (!checked) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked, + imageVector = if (!checked) CompoundIcons.CheckCircleSolid() else CompoundIcons.Circle(), contentDescription = null ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt index 8cea5a0ef8a..78195d4f981 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt @@ -9,6 +9,8 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalContentColor @@ -16,12 +18,9 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -135,7 +134,7 @@ fun ListItem( CompositionLocalProvider( LocalContentColor provides leadingContentColor, ) { - content.View() + content.View(isItemEnabled = enabled) } } } @@ -145,7 +144,7 @@ fun ListItem( LocalTextStyle provides ElementTheme.typography.fontBodyMdRegular, LocalContentColor provides trailingContentColor, ) { - content.View() + content.View(isItemEnabled = enabled) } } } @@ -158,7 +157,12 @@ fun ListItem( .then(modifier) } else { modifier - }, + } + .withAccessibilityModifier( + content = trailingContent ?: leadingContent, + enabled = enabled || alwaysClickable, + onClick = onClick, + ), overlineContent = null, supportingContent = decoratedSupportingContent, leadingContent = decoratedLeadingContent, @@ -169,6 +173,45 @@ fun ListItem( ) } +private fun Modifier.withAccessibilityModifier( + content: ListItemContent?, + enabled: Boolean, + onClick: (() -> Unit)?, +): Modifier = this.then( + when (content) { + is ListItemContent.Checkbox -> { + Modifier.toggleable( + value = content.checked, + role = Role.Checkbox, + enabled = content.enabled && enabled, + onValueChange = { onClick?.invoke() } + ) + } + is ListItemContent.Switch -> { + Modifier.toggleable( + value = content.checked, + role = Role.Switch, + enabled = content.enabled && enabled, + onValueChange = { onClick?.invoke() } + ) + } + is ListItemContent.RadioButton -> { + Modifier.selectable( + selected = content.selected, + role = Role.RadioButton, + enabled = content.enabled && enabled, + onClick = { onClick?.invoke() } + ) + } + ListItemContent.Badge, + is ListItemContent.Custom, + is ListItemContent.Icon, + is ListItemContent.Text, + is ListItemContent.Counter, + null -> Modifier + } +) + /** * The style to use for a [ListItem]. */ @@ -193,14 +236,14 @@ sealed interface ListItemStyle { @Composable fun leadingIconColor() = when (this) { - Default -> ListItemDefaultColors.icon + Default -> ListItemDefaultColors.leadingIcon Primary -> ElementTheme.colors.iconPrimary Destructive -> ElementTheme.colors.iconCriticalPrimary } @Composable fun trailingIconColor() = when (this) { - Default -> ListItemDefaultColors.icon + Default -> ListItemDefaultColors.trailingIcon Primary -> ElementTheme.colors.iconPrimary Destructive -> ElementTheme.colors.iconCriticalPrimary } @@ -210,15 +253,16 @@ object ListItemDefaultColors { val headline: Color @Composable get() = ElementTheme.colors.textPrimary val headlineDisabled: Color @Composable get() = ElementTheme.colors.textDisabled val supportingText: Color @Composable get() = ElementTheme.materialColors.onSurfaceVariant - val icon: Color @Composable get() = ElementTheme.colors.iconTertiary + val leadingIcon: Color @Composable get() = ElementTheme.colors.iconSecondary + val trailingIcon: Color @Composable get() = ElementTheme.colors.iconPrimary val iconDisabled: Color @Composable get() = ElementTheme.colors.iconDisabled val colors: ListItemColors @Composable get() = ListItemDefaults.colors( headlineColor = headline, supportingColor = supportingText, - leadingIconColor = icon, - trailingIconColor = icon, + leadingIconColor = leadingIcon, + trailingIconColor = trailingIcon, disabledHeadlineColor = headlineDisabled, disabledLeadingIconColor = iconDisabled, disabledTrailingIconColor = iconDisabled, @@ -545,20 +589,17 @@ private object PreviewItems { @Composable fun checkbox(): ListItemContent { - var checked by remember { mutableStateOf(false) } - return ListItemContent.Checkbox(checked = checked, onChange = { checked = !checked }) + return ListItemContent.Checkbox(checked = false) } @Composable fun radioButton(): ListItemContent { - var checked by remember { mutableStateOf(false) } - return ListItemContent.RadioButton(selected = checked, onClick = { checked = !checked }) + return ListItemContent.RadioButton(selected = false) } @Composable fun switch(): ListItemContent { - var checked by remember { mutableStateOf(false) } - return ListItemContent.Switch(checked = checked, onChange = { checked = !checked }) + return ListItemContent.Switch(checked = false) } @Composable diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt index 6a7d9b0d737..558cbea6533 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt @@ -164,7 +164,7 @@ internal fun ListSupportingTextSmallPaddingPreview() { internal fun ListSupportingTextLargePaddingPreview() { ElementThemedPreview { Column { - ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Switch(checked = true, onChange = {})) + ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Switch(checked = true)) ListSupportingText( text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more", contentPadding = ListSupportingTextDefaults.Padding.LargeLeadingContent, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBar.kt new file mode 100644 index 00000000000..9d67f4ec054 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBar.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.preview.ElementThemedPreview +import io.element.android.libraries.designsystem.preview.PreviewGroup + +@Composable +fun NavigationBar( + modifier: Modifier = Modifier, + containerColor: Color = ElementNavigationBarDefaults.containerColor, + contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), + tonalElevation: Dp = ElementNavigationBarDefaults.tonalElevation, + windowInsets: WindowInsets = ElementNavigationBarDefaults.windowInsets, + content: @Composable RowScope.() -> Unit +) { + androidx.compose.material3.NavigationBar( + modifier = modifier, + containerColor = containerColor, + contentColor = contentColor, + tonalElevation = tonalElevation, + windowInsets = windowInsets, + content = content + ) +} + +object ElementNavigationBarDefaults { + val containerColor: Color + @Composable get() = if (ElementTheme.isLightTheme) { + ElementTheme.colors.bgSubtlePrimary + } else { + ElementTheme.colors.textOnSolidPrimary + } + + val tonalElevation: Dp = NavigationBarDefaults.Elevation + + val windowInsets: WindowInsets + @Composable get() = NavigationBarDefaults.windowInsets +} + +@Preview(group = PreviewGroup.AppBars) +@Composable +internal fun NavigationBarPreview() = ElementThemedPreview { + NavigationBar { + NavigationBarItem( + icon = { + NavigationBarIcon( + imageVector = CompoundIcons.ChatSolid(), + count = 5, + isCritical = false, + ) + }, + label = { + NavigationBarText( + text = "Chats" + ) + }, + selected = true, + onClick = {}, + ) + NavigationBarItem( + icon = { + NavigationBarIcon( + imageVector = CompoundIcons.ChatSolid(), + count = 5, + isCritical = true, + ) + }, + label = { + NavigationBarText( + text = "Teams" + ) + }, + selected = false, + onClick = {}, + ) + NavigationBarItem( + icon = { + NavigationBarIcon( + imageVector = CompoundIcons.ChatSolid(), + count = 0, + isCritical = false, + ) + }, + label = { + NavigationBarText( + text = "Other" + ) + }, + selected = false, + onClick = {}, + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarIcon.kt new file mode 100644 index 00000000000..2c2890f8546 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarIcon.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.atomic.atoms.CounterAtom + +@Composable +fun NavigationBarIcon( + imageVector: ImageVector, + modifier: Modifier = Modifier, + count: Int = 0, + isCritical: Boolean = false, +) { + Box(modifier) { + Icon( + imageVector = imageVector, + contentDescription = null, + ) + CounterAtom( + modifier = Modifier.offset(11.dp, (-11).dp), + textStyle = ElementTheme.typography.fontBodyXsMedium, + count = count, + isCritical = isCritical, + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarItem.kt new file mode 100644 index 00000000000..8c42b932dd2 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarItem.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemColors +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import io.element.android.compound.theme.ElementTheme + +@Composable +fun RowScope.NavigationBarItem( + selected: Boolean, + onClick: () -> Unit, + icon: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + label: @Composable (() -> Unit)? = null, + alwaysShowLabel: Boolean = true, + colors: NavigationBarItemColors = ElementNavigationBarItemDefaults.colors(), + interactionSource: MutableInteractionSource? = null +) { + NavigationBarItem( + selected = selected, + onClick = onClick, + icon = icon, + modifier = modifier, + enabled = enabled, + label = label, + alwaysShowLabel = alwaysShowLabel, + colors = colors, + interactionSource = interactionSource, + ) +} + +object ElementNavigationBarItemDefaults { + @Composable + fun colors() = NavigationBarItemDefaults.colors().copy( + selectedIconColor = ElementTheme.colors.iconPrimary, + selectedTextColor = ElementTheme.colors.textPrimary, + unselectedIconColor = ElementTheme.colors.iconTertiary, + unselectedTextColor = ElementTheme.colors.textDisabled, + selectedIndicatorColor = Color.Companion.Transparent, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarText.kt new file mode 100644 index 00000000000..826c99f743f --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarText.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.compound.theme.ElementTheme + +@Composable +fun NavigationBarText( + text: String, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier, + text = text, + style = ElementTheme.typography.fontBodySmMedium, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt index 45de16d984d..64d5672b380 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt @@ -10,12 +10,12 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material3.FabPosition -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import io.element.android.compound.theme.ElementTheme @Composable fun Scaffold( @@ -25,7 +25,7 @@ fun Scaffold( snackbarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, - containerColor: Color = MaterialTheme.colorScheme.background, + containerColor: Color = ElementTheme.colors.bgCanvasDefault, contentColor: Color = contentColorFor(containerColor), contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, content: @Composable (PaddingValues) -> Unit diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt index 99f9f1a5563..e75b7727ebf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt @@ -106,7 +106,7 @@ fun SearchBar( Icon( imageVector = CompoundIcons.Search(), contentDescription = stringResource(CommonStrings.action_search), - tint = ElementTheme.materialColors.tertiary, + tint = ElementTheme.colors.iconTertiary, ) } } @@ -138,7 +138,7 @@ fun SearchBar( Text( text = stringResource(CommonStrings.common_no_results), textAlign = TextAlign.Center, - color = ElementTheme.materialColors.tertiary, + color = ElementTheme.colors.textSecondary, modifier = Modifier.fillMaxWidth() ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt index 333716e6c29..d51e9745c20 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt @@ -58,7 +58,7 @@ fun TextField( placeholder: String? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, - isError: Boolean = false, + validity: TextFieldValidity = TextFieldValidity.None, enabled: Boolean = true, readOnly: Boolean = false, singleLine: Boolean = false, @@ -93,7 +93,7 @@ fun TextField( readOnly = readOnly, enabled = enabled, isFocused = isFocused, - isError = isError, + validity = validity, leadingIcon = leadingIcon, placeholder = placeholder, isTextEmpty = value.isEmpty(), @@ -114,7 +114,7 @@ fun TextField( placeholder: String? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, - isError: Boolean = false, + validity: TextFieldValidity? = null, enabled: Boolean = true, readOnly: Boolean = false, singleLine: Boolean = false, @@ -149,7 +149,7 @@ fun TextField( readOnly = readOnly, enabled = enabled, isFocused = isFocused, - isError = isError, + validity = validity, leadingIcon = leadingIcon, placeholder = placeholder, isTextEmpty = value.text.isEmpty(), @@ -166,7 +166,7 @@ private fun DecorationBox( enabled: Boolean, readOnly: Boolean, isFocused: Boolean, - isError: Boolean, + validity: TextFieldValidity?, placeholder: String?, isTextEmpty: Boolean, supportingText: String?, @@ -187,7 +187,7 @@ private fun DecorationBox( enabled = enabled, readOnly = readOnly, isFocused = isFocused, - isError = isError + isError = validity == TextFieldValidity.Invalid ) { Row(modifier = Modifier.padding(16.dp)) { if (leadingIcon != null) { @@ -216,7 +216,7 @@ private fun DecorationBox( } if (supportingText != null) { Spacer(modifier = Modifier.height(4.dp)) - SupportingTextLayout(isError, supportingText) + SupportingTextLayout(validity, supportingText) } } } @@ -254,24 +254,45 @@ private fun TextFieldContainer( } @Composable -private fun SupportingTextLayout(isError: Boolean, supportingText: String) { +private fun SupportingTextLayout(validity: TextFieldValidity?, supportingText: String) { Row(horizontalArrangement = spacedBy(4.dp)) { - if (isError) { - Icon( - imageVector = CompoundIcons.Error(), - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = ElementTheme.colors.iconCriticalPrimary - ) + when (validity) { + TextFieldValidity.Invalid -> { + Icon( + imageVector = CompoundIcons.ErrorSolid(), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = ElementTheme.colors.iconCriticalPrimary + ) + } + TextFieldValidity.Valid -> { + Icon( + imageVector = CompoundIcons.CheckCircleSolid(), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = ElementTheme.colors.iconSuccessPrimary + ) + } + else -> Unit } Text( text = supportingText, - color = if (isError) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary, + color = when (validity) { + TextFieldValidity.Invalid -> ElementTheme.colors.textCriticalPrimary + TextFieldValidity.Valid -> ElementTheme.colors.textSuccessPrimary + else -> ElementTheme.colors.textSecondary + }, style = ElementTheme.typography.fontBodySmRegular, ) } } +enum class TextFieldValidity { + None, + Invalid, + Valid +} + @Composable private fun textFieldStyle(enabled: Boolean): TextStyle { return ElementTheme.typography.fontBodyLgRegular.copy( @@ -283,11 +304,11 @@ private fun textFieldStyle(enabled: Boolean): TextStyle { ) } -@Preview(group = PreviewGroup.TextFields) +@Preview(group = PreviewGroup.TextFields, heightDp = 1000) @Composable internal fun TextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() } -@Preview(group = PreviewGroup.TextFields) +@Preview(group = PreviewGroup.TextFields, heightDp = 1000) @Composable internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() } @@ -295,15 +316,15 @@ internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() } @ExcludeFromCoverage private fun ContentToPreview() { Column(modifier = Modifier.padding(4.dp)) { - allBooleans.forEach { isError -> + TextFieldValidity.entries.forEach { validity -> allBooleans.forEach { enabled -> allBooleans.forEach { readonly -> TextField( onValueChange = {}, label = "Label", - value = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}", + value = "Hello val=$validity, en=${enabled.asInt()}, ro=${readonly.asInt()}", supportingText = "Supporting text", - isError = isError, + validity = validity, enabled = enabled, readOnly = readonly, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt index e5daeb123b9..3087f8f5f09 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt @@ -17,13 +17,66 @@ import androidx.compose.material3.TopAppBarScrollBehavior 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 io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +/** + * A top app bar that displays a title string, navigation icon, and actions. + * @param titleStr The title string to display in the top app bar. + * @param modifier The [Modifier] to be applied to this top app bar. + * @param navigationIcon The content to display as the navigation icon. + * @param actions The content to display in the action area of the top app bar. + * @param windowInsets The window insets to apply to this top app bar. + * @param colors The colors used for this top app bar. + * @param scrollBehavior Optional scroll behavior for this top app bar. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopAppBar( + titleStr: String, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) { + TopAppBar( + title = { + Text( + text = titleStr, + modifier = Modifier.semantics { heading() }, + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + modifier = modifier, + navigationIcon = navigationIcon, + actions = actions, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior, + ) +} + +/** + * A top app bar that displays a title, navigation icon, and actions. + * + * @param title The content to display as the title of the top app bar. Do not forget to apply `heading()` to + * the semantics of the title to ensure it is announced correctly by accessibility services. + * @param modifier The [Modifier] to be applied to this top app bar. + * @param navigationIcon The content to display as the navigation icon. + * @param actions The content to display in the action area of the top app bar. + * @param windowInsets The window insets to apply to this top app bar. + * @param colors The colors used for this top app bar. + * @param scrollBehavior Optional scroll behavior for this top app bar. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun TopAppBar( @@ -68,3 +121,22 @@ internal fun TopAppBarPreview() = ElementThemedPreview { } ) } + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(group = PreviewGroup.AppBars) +@Composable +internal fun TopAppBarStrPreview() = ElementThemedPreview { + TopAppBar( + titleStr = "Title string", + navigationIcon = { BackButton(onClick = {}) }, + actions = { + TextButton(text = "Action", onClick = {}) + IconButton(onClick = {}) { + Icon( + imageVector = CompoundIcons.ShareAndroid(), + contentDescription = null, + ) + } + } + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt deleted file mode 100644 index a819d9770b6..00000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt +++ /dev/null @@ -1,516 +0,0 @@ -/* - * 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. - */ -@file:OptIn(ExperimentalFoundationApi::class) - -package io.element.android.libraries.designsystem.theme.components.bottomsheet - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.requiredHeightIn -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.BottomSheetScaffold -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue -import androidx.compose.material3.SheetValue.Expanded -import androidx.compose.material3.SheetValue.PartiallyExpanded -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.SubcomposeLayout -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.semantics.collapse -import androidx.compose.ui.semantics.dismiss -import androidx.compose.ui.semantics.expand -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.Velocity -import kotlinx.coroutines.launch -import kotlin.math.abs -import kotlin.math.roundToInt - -// These are needed until https://issuetracker.google.com/issues/306464779 is fixed - -@Composable -@ExperimentalMaterial3Api -fun CustomBottomSheetScaffold( - sheetContent: @Composable ColumnScope.() -> Unit, - modifier: Modifier = Modifier, - scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), - sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight, - sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth, - sheetShape: Shape = BottomSheetDefaults.ExpandedShape, - sheetContainerColor: Color = Color.White, - sheetContentColor: Color = contentColorFor(sheetContainerColor), - sheetTonalElevation: Dp = BottomSheetDefaults.Elevation, - sheetShadowElevation: Dp = BottomSheetDefaults.Elevation, - sheetDragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, - sheetSwipeEnabled: Boolean = true, - topBar: @Composable (() -> Unit)? = null, - snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, - containerColor: Color = MaterialTheme.colorScheme.surface, - contentColor: Color = contentColorFor(containerColor), - content: @Composable (PaddingValues) -> Unit -) { - val peekHeightPx = with(LocalDensity.current) { - sheetPeekHeight.roundToPx() - } - CustomBottomSheetScaffoldLayout( - modifier = modifier, - topBar = topBar, - body = content, - snackbarHost = { - snackbarHost(scaffoldState.snackbarHostState) - }, - sheetPeekHeight = sheetPeekHeight, - sheetOffset = { scaffoldState.bottomSheetState.requireOffset() }, - sheetState = scaffoldState.bottomSheetState, - containerColor = containerColor, - contentColor = contentColor, - bottomSheet = { layoutHeight -> - CustomStandardBottomSheet( - state = scaffoldState.bottomSheetState, - peekHeight = sheetPeekHeight, - sheetMaxWidth = sheetMaxWidth, - sheetSwipeEnabled = sheetSwipeEnabled, - calculateAnchors = { sheetSize -> - val sheetHeight = sheetSize.height - io.element.android.libraries.designsystem.theme.components.bottomsheet.DraggableAnchors { - if (!scaffoldState.bottomSheetState.skipPartiallyExpanded) { - PartiallyExpanded at (layoutHeight - peekHeightPx).toFloat() - } - if (sheetHeight != peekHeightPx) { - Expanded at maxOf(layoutHeight - sheetHeight, 0).toFloat() - } - if (!scaffoldState.bottomSheetState.skipHiddenState) { - SheetValue.Hidden at layoutHeight.toFloat() - } - } - }, - shape = sheetShape, - containerColor = sheetContainerColor, - contentColor = sheetContentColor, - tonalElevation = sheetTonalElevation, - shadowElevation = sheetShadowElevation, - dragHandle = sheetDragHandle, - content = sheetContent - ) - } - ) -} - -@SuppressWarnings("ModifierWithoutDefault") -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun CustomBottomSheetScaffoldLayout( - modifier: Modifier, - topBar: @Composable (() -> Unit)?, - body: @Composable (innerPadding: PaddingValues) -> Unit, - bottomSheet: @Composable (layoutHeight: Int) -> Unit, - snackbarHost: @Composable () -> Unit, - sheetPeekHeight: Dp, - sheetOffset: () -> Float, - sheetState: CustomSheetState, - containerColor: Color, - contentColor: Color, -) { - // b/291735717 Remove this once deprecated methods without density are removed - val density = LocalDensity.current - SideEffect { - sheetState.density = density - } - SubcomposeLayout { constraints -> - val layoutWidth = constraints.maxWidth - val layoutHeight = constraints.maxHeight - val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) - - val sheetPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Sheet) { - bottomSheet(layoutHeight) - }[0].measure(looseConstraints) - - val topBarPlaceable = topBar?.let { - subcompose(BottomSheetScaffoldLayoutSlot.TopBar) { topBar() }[0] - .measure(looseConstraints) - } - val topBarHeight = topBarPlaceable?.height ?: 0 - - val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight) - val bodyPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Body) { - Surface( - modifier = modifier, - color = containerColor, - contentColor = contentColor, - ) { body(PaddingValues(bottom = sheetPeekHeight)) } - }[0].measure(bodyConstraints) - - val snackbarPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Snackbar, snackbarHost)[0] - .measure(looseConstraints) - - layout(layoutWidth, layoutHeight) { - val sheetOffsetY = sheetOffset().roundToInt() - val sheetOffsetX = Integer.max(0, (layoutWidth - sheetPlaceable.width) / 2) - - val snackbarOffsetX = (layoutWidth - snackbarPlaceable.width) / 2 - val snackbarOffsetY = when (sheetState.currentValue) { - SheetValue.PartiallyExpanded -> sheetOffsetY - snackbarPlaceable.height - SheetValue.Expanded, SheetValue.Hidden -> layoutHeight - snackbarPlaceable.height - } - - // Placement order is important for elevation - bodyPlaceable.placeRelative(0, topBarHeight) - topBarPlaceable?.placeRelative(0, 0) - sheetPlaceable.placeRelative(sheetOffsetX, sheetOffsetY) - snackbarPlaceable.placeRelative(snackbarOffsetX, snackbarOffsetY) - } - } -} - -private enum class BottomSheetScaffoldLayoutSlot { TopBar, Body, Sheet, Snackbar } - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) -@Composable -private fun CustomStandardBottomSheet( - state: CustomSheetState, - @Suppress("PrimitiveInLambda") - calculateAnchors: (sheetSize: IntSize) -> DraggableAnchors, - peekHeight: Dp, - sheetMaxWidth: Dp, - sheetSwipeEnabled: Boolean, - shape: Shape, - containerColor: Color, - contentColor: Color, - tonalElevation: Dp, - shadowElevation: Dp, - dragHandle: @Composable (() -> Unit)?, - content: @Composable ColumnScope.() -> Unit -) { - val scope = rememberCoroutineScope() - - val orientation = Orientation.Vertical - - Surface( - modifier = Modifier - .widthIn(max = sheetMaxWidth) - .fillMaxWidth() - .requiredHeightIn(min = peekHeight) - .apply { - if (sheetSwipeEnabled) { - nestedScroll( - remember(state.anchoredDraggableState) { - ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( - sheetState = state, - orientation = orientation, - onFling = { scope.launch { state.settle(it) } } - ) - } - ) - } - } - .anchoredDraggable( - state = state.anchoredDraggableState, - orientation = orientation, - enabled = sheetSwipeEnabled - ) - .onSizeChanged { layoutSize -> - val newAnchors = calculateAnchors(layoutSize) - val newTarget = when (state.anchoredDraggableState.targetValue) { - SheetValue.Hidden, SheetValue.PartiallyExpanded -> SheetValue.PartiallyExpanded - SheetValue.Expanded -> { - if (newAnchors.hasAnchorFor(SheetValue.Expanded)) SheetValue.Expanded else SheetValue.PartiallyExpanded - } - } - state.anchoredDraggableState.updateAnchors(newAnchors, newTarget) - }, - shape = shape, - color = containerColor, - contentColor = contentColor, - tonalElevation = tonalElevation, - shadowElevation = shadowElevation, - ) { - Column(Modifier.fillMaxWidth()) { - if (dragHandle != null) { - val partialExpandActionLabel = - "Partial Expand" - val dismissActionLabel = "Dismiss" - val expandActionLabel = "Expand" - Box( - Modifier - .align(Alignment.CenterHorizontally) - .semantics(mergeDescendants = true) { - with(state) { - // Provides semantics to interact with the bottomsheet if there is more - // than one anchor to swipe to and swiping is enabled. - if (anchoredDraggableState.anchors.size > 1 && sheetSwipeEnabled) { - if (currentValue == SheetValue.PartiallyExpanded) { - expand(expandActionLabel) { - scope.launch { expand() } - true - } - } else { - collapse(partialExpandActionLabel) { - scope.launch { partialExpand() } - true - } - } - if (!state.skipHiddenState) { - dismiss(dismissActionLabel) { - scope.launch { hide() } - true - } - } - } - } - }, - ) { - dragHandle() - } - } - content() - } - } -} - -/** - * [DraggableAnchorsConfig] stores a mutable configuration anchors, comprised of values of [T] and - * corresponding [Float] positions. This [DraggableAnchorsConfig] is used to construct an immutable - * [DraggableAnchors] instance later on. - */ -@ExperimentalFoundationApi -class DraggableAnchorsConfig { - internal val anchors = mutableMapOf() - - /** - * Set the anchor position for [this] anchor. - * - * @param position The anchor position. - */ - @Suppress("BuilderSetStyle") - infix fun T.at(position: Float) { - anchors[this] = position - } -} - -/** - * Create a new [DraggableAnchors] instance using a builder function. - * - * @param T The type of the anchor values. - * @param builder A function with a [DraggableAnchorsConfig] that offers APIs to configure anchors - * @return A new [DraggableAnchors] instance with the anchor positions set by the `builder` - * function. - */ -@OptIn(ExperimentalFoundationApi::class) -@ExperimentalMaterial3Api -@SuppressWarnings("FunctionName") -internal fun DraggableAnchors( - builder: DraggableAnchorsConfig.() -> Unit -): DraggableAnchors = MapDraggableAnchors(DraggableAnchorsConfig().apply(builder).anchors) - -private class MapDraggableAnchors(private val anchors: Map) : DraggableAnchors { - override fun positionOf(value: T): Float = anchors[value] ?: Float.NaN - override fun hasAnchorFor(value: T) = anchors.containsKey(value) - - override fun closestAnchor(position: Float): T? = anchors.minByOrNull { - abs(position - it.value) - }?.key - - override fun closestAnchor( - position: Float, - searchUpwards: Boolean - ): T? { - return anchors.minByOrNull { (_, anchor) -> - val delta = if (searchUpwards) anchor - position else position - anchor - if (delta < 0) Float.POSITIVE_INFINITY else delta - }?.key - } - - override fun minAnchor() = anchors.values.minOrNull() ?: Float.NaN - - override fun maxAnchor() = anchors.values.maxOrNull() ?: Float.NaN - - override val size: Int - get() = anchors.size - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is MapDraggableAnchors<*>) return false - - return anchors == other.anchors - } - - override fun forEach(block: (anchor: T, position: Float) -> Unit) { - for (anchor in anchors) { - block(anchor.key, anchor.value) - } - } - - override fun hashCode() = 31 * anchors.hashCode() - - override fun toString() = "MapDraggableAnchors($anchors)" -} - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) -@SuppressWarnings("FunctionName") -internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( - sheetState: CustomSheetState, - orientation: Orientation, - onFling: (velocity: Float) -> Unit -): NestedScrollConnection = object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.UserInput) { - sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset() - } else { - Offset.Zero - } - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - return if (source == NestedScrollSource.UserInput) { - sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset() - } else { - Offset.Zero - } - } - - override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = available.toFloat() - val currentOffset = sheetState.requireOffset() - val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor() - return if (toFling < 0 && currentOffset > minAnchor) { - onFling(toFling) - // since we go to the anchor with tween settling, consume all for the best UX - available - } else { - Velocity.Zero - } - } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - onFling(available.toFloat()) - return available - } - - private fun Float.toOffset(): Offset = Offset( - x = if (orientation == Orientation.Horizontal) this else 0f, - y = if (orientation == Orientation.Vertical) this else 0f - ) - - @JvmName("velocityToFloat") - private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y - - @JvmName("offsetToFloat") - private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y -} - -/** - * State of the [BottomSheetScaffold] composable. - * - * @param bottomSheetState the state of the persistent bottom sheet - * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold - */ -@ExperimentalMaterial3Api -@Stable -@SuppressWarnings("UseDataClass") -class BottomSheetScaffoldState( - val bottomSheetState: CustomSheetState, - val snackbarHostState: SnackbarHostState -) - -/** - * Create and [remember] a [BottomSheetScaffoldState]. - * - * @param bottomSheetState the state of the standard bottom sheet. See - * [rememberStandardBottomSheetState] - * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold - */ -@Composable -@ExperimentalMaterial3Api -fun rememberBottomSheetScaffoldState( - bottomSheetState: CustomSheetState = rememberStandardBottomSheetState(), - snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } -): BottomSheetScaffoldState { - return remember(bottomSheetState, snackbarHostState) { - BottomSheetScaffoldState( - bottomSheetState = bottomSheetState, - snackbarHostState = snackbarHostState - ) - } -} - -/** - * Create and [remember] a [SheetState] for [BottomSheetScaffold]. - * - * @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or - * [Expanded] if [skipHiddenState] is true - * @param confirmValueChange optional callback invoked to confirm or veto a pending state change - * @param [skipHiddenState] whether Hidden state is skipped for [BottomSheetScaffold] - */ -@Composable -@ExperimentalMaterial3Api -fun rememberStandardBottomSheetState( - initialValue: SheetValue = PartiallyExpanded, - confirmValueChange: (SheetValue) -> Boolean = { true }, - skipHiddenState: Boolean = true, -) = rememberSheetState(false, confirmValueChange, initialValue, skipHiddenState) - -@Composable -@ExperimentalMaterial3Api -internal fun rememberSheetState( - skipPartiallyExpanded: Boolean = false, - confirmValueChange: (SheetValue) -> Boolean = { true }, - initialValue: SheetValue = SheetValue.Hidden, - skipHiddenState: Boolean = false, -): CustomSheetState { - val density = LocalDensity.current - return rememberSaveable( - skipPartiallyExpanded, - confirmValueChange, - saver = CustomSheetState.Saver( - skipPartiallyExpanded = skipPartiallyExpanded, - confirmValueChange = confirmValueChange, - density = density - ) - ) { - CustomSheetState( - skipPartiallyExpanded, - density, - initialValue, - confirmValueChange, - skipHiddenState - ) - } -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt deleted file mode 100644 index 85507485e3f..00000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt +++ /dev/null @@ -1,303 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.designsystem.theme.components.bottomsheet - -import androidx.compose.animation.core.SpringSpec -import androidx.compose.animation.core.exponentialDecay -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.animateTo -import androidx.compose.foundation.gestures.snapTo -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetValue -import androidx.compose.material3.SheetValue.Expanded -import androidx.compose.material3.SheetValue.Hidden -import androidx.compose.material3.SheetValue.PartiallyExpanded -import androidx.compose.runtime.Stable -import androidx.compose.runtime.saveable.Saver -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CancellationException - -@OptIn(ExperimentalFoundationApi::class) -@Stable -@ExperimentalMaterial3Api -class CustomSheetState -@Deprecated( - message = "This constructor is deprecated. " + - "Please use the constructor that provides a [Density]", - replaceWith = ReplaceWith( - "SheetState(" + - "skipPartiallyExpanded, LocalDensity.current, initialValue, " + - "confirmValueChange, skipHiddenState)" - ) -) -constructor( - internal val skipPartiallyExpanded: Boolean, - initialValue: SheetValue = Hidden, - confirmValueChange: (SheetValue) -> Boolean = { true }, - internal val skipHiddenState: Boolean = false, -) { - /** - * State of a sheet composable, such as [ModalBottomSheet] - * - * Contains states relating to its swipe position as well as animations between state values. - * - * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large - * enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move - * to the [Hidden] state if available when hiding the sheet, either programmatically or by user - * interaction. - * @param density The density that this state can use to convert values to and from dp. - * @param initialValue The initial value of the state. - * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change. - * @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always - * expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either - * programmatically or by user interaction. - */ - @ExperimentalMaterial3Api - @Suppress("Deprecation") - constructor( - skipPartiallyExpanded: Boolean, - density: Density, - initialValue: SheetValue = Hidden, - confirmValueChange: (SheetValue) -> Boolean = { true }, - skipHiddenState: Boolean = false, - ) : this(skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState) { - this.density = density - } - - init { - if (skipPartiallyExpanded) { - require(initialValue != PartiallyExpanded) { - "The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " + - "is set to true." - } - } - if (skipHiddenState) { - require(initialValue != Hidden) { - "The initial value must not be set to Hidden if skipHiddenState is set to true." - } - } - } - - /** - * The current value of the state. - * - * If no swipe or animation is in progress, this corresponds to the state the bottom sheet is - * currently in. If a swipe or an animation is in progress, this corresponds the state the sheet - * was in before the swipe or animation started. - */ - - val currentValue: SheetValue get() = anchoredDraggableState.currentValue - - /** - * The target value of the bottom sheet state. - * - * If a swipe is in progress, this is the value that the sheet would animate to if the - * swipe finishes. If an animation is running, this is the target value of that animation. - * Finally, if no swipe or animation is in progress, this is the same as the [currentValue]. - */ - val targetValue: SheetValue get() = anchoredDraggableState.targetValue - - /** - * Whether the modal bottom sheet is visible. - */ - val isVisible: Boolean - get() = anchoredDraggableState.currentValue != Hidden - - /** - * Require the current offset (in pixels) of the bottom sheet. - * - * The offset will be initialized during the first measurement phase of the provided sheet - * content. - * - * These are the phases: - * Composition { -> Effects } -> Layout { Measurement -> Placement } -> Drawing - * - * During the first composition, an [IllegalStateException] is thrown. In subsequent - * compositions, the offset will be derived from the anchors of the previous pass. Always prefer - * accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next - * frame, after layout. - * - * @throws IllegalStateException If the offset has not been initialized yet - */ - fun requireOffset(): Float = anchoredDraggableState.requireOffset() - - fun getOffset(): Float? = anchoredDraggableState.offset.takeIf { !it.isNaN() } - - /** - * Whether the sheet has an expanded state defined. - */ - - val hasExpandedState: Boolean - get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded) - - /** - * Whether the modal bottom sheet has a partially expanded state defined. - */ - val hasPartiallyExpandedState: Boolean - get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded) - - /** - * Fully expand the bottom sheet with animation and suspend until it is fully expanded or - * animation has been cancelled. - * * - * @throws [CancellationException] if the animation is interrupted - */ - suspend fun expand() { - anchoredDraggableState.animateTo(Expanded) - } - - /** - * Animate the bottom sheet and suspend until it is partially expanded or animation has been - * cancelled. - * @throws [CancellationException] if the animation is interrupted - * @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true - */ - suspend fun partialExpand() { - check(!skipPartiallyExpanded) { - "Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" + - " skipPartiallyExpanded to false to use this function." - } - animateTo(PartiallyExpanded) - } - - /** - * Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined - * else [Expanded]. - * @throws [CancellationException] if the animation is interrupted - */ - suspend fun show() { - val targetValue = when { - hasPartiallyExpandedState -> PartiallyExpanded - else -> Expanded - } - animateTo(targetValue) - } - - /** - * Hide the bottom sheet with animation and suspend until it is fully hidden or animation has - * been cancelled. - * @throws [CancellationException] if the animation is interrupted - */ - suspend fun hide() { - check(!skipHiddenState) { - "Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" + - " to false to use this function." - } - animateTo(Hidden) - } - - /** - * Animate to a [targetValue]. - * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the - * [targetValue] without updating the offset. - * - * @throws CancellationException if the interaction interrupted by another interaction like a - * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. - * - * @param targetValue The target value of the animation - */ - @OptIn(ExperimentalFoundationApi::class) - internal suspend fun animateTo( - targetValue: SheetValue, - ) { - anchoredDraggableState.animateTo(targetValue) - } - - /** - * Snap to a [targetValue] without any animation. - * - * @throws CancellationException if the interaction interrupted by another interaction like a - * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. - * - * @param targetValue The target value of the animation - */ - @OptIn(ExperimentalFoundationApi::class) - internal suspend fun snapTo(targetValue: SheetValue) { - anchoredDraggableState.snapTo(targetValue) - } - - /** - * Find the closest anchor taking into account the velocity and settle at it with an animation. - */ - @OptIn(ExperimentalFoundationApi::class) - internal suspend fun settle(velocity: Float) { - anchoredDraggableState.settle(velocity) - } - - @OptIn(ExperimentalFoundationApi::class) - internal var anchoredDraggableState = androidx.compose.foundation.gestures.AnchoredDraggableState( - initialValue = initialValue, - snapAnimationSpec = AnchoredDraggableDefaults.SnapAnimationSpec, - decayAnimationSpec = AnchoredDraggableDefaults.DecayAnimationSpec, - confirmValueChange = confirmValueChange, - positionalThreshold = { with(requireDensity()) { 56.dp.toPx() } }, - velocityThreshold = { with(requireDensity()) { 125.dp.toPx() } } - ) - - @OptIn(ExperimentalFoundationApi::class) - internal val offset: Float? get() = anchoredDraggableState.offset - - internal var density: Density? = null - private fun requireDensity() = requireNotNull(density) { - "SheetState did not have a density attached. Are you using SheetState with " + - "BottomSheetScaffold or ModalBottomSheet component?" - } - - companion object { - /** - * The default [Saver] implementation for [SheetState]. - */ - @SuppressWarnings("FunctionName") - fun Saver( - skipPartiallyExpanded: Boolean, - confirmValueChange: (SheetValue) -> Boolean, - density: Density - ) = Saver( - save = { it.currentValue }, - restore = { savedValue -> - CustomSheetState(skipPartiallyExpanded, density, savedValue, confirmValueChange) - } - ) - - /** - * The default [Saver] implementation for [SheetState]. - */ - @Deprecated( - message = "This function is deprecated. Please use the overload where Density is" + - " provided.", - replaceWith = ReplaceWith( - "Saver(skipPartiallyExpanded, confirmValueChange, LocalDensity.current)" - ) - ) - @Suppress("Deprecation", "FunctionName") - fun Saver( - skipPartiallyExpanded: Boolean, - confirmValueChange: (SheetValue) -> Boolean - ) = Saver( - save = { it.currentValue }, - restore = { savedValue -> - CustomSheetState(skipPartiallyExpanded, savedValue, confirmValueChange) - } - ) - } -} - -@Stable -@ExperimentalMaterial3Api -internal object AnchoredDraggableDefaults { - /** - * The default animation used by [AnchoredDraggableState]. - */ - @ExperimentalMaterial3Api - val SnapAnimationSpec = SpringSpec() - - @ExperimentalMaterial3Api - val DecayAnimationSpec = exponentialDecay() -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt index e207657d2c5..dbfb0b29a66 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt @@ -10,10 +10,10 @@ package io.element.android.libraries.designsystem.theme.components.previews import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.DatePicker import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -48,7 +48,7 @@ private fun ContentToPreview() { shape = AlertDialogDefaults.shape, containerColor = AlertDialogDefaults.containerColor, tonalElevation = AlertDialogDefaults.TonalElevation, - buttonContentColor = MaterialTheme.colorScheme.primary, + buttonContentColor = ElementTheme.colors.textPrimary, iconContentColor = AlertDialogDefaults.iconContentColor, titleContentColor = AlertDialogDefaults.titleContentColor, textContentColor = AlertDialogDefaults.textContentColor, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/MenuPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/MenuPreview.kt index 8c69eeac5db..6ae35532eaf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/MenuPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/MenuPreview.kt @@ -7,8 +7,6 @@ package io.element.android.libraries.designsystem.theme.components.previews -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -35,7 +33,7 @@ internal fun MenuPreview() { val leadingIcon: @Composable (() -> Unit)? = if (i in 2..3) { @Composable { Icon( - imageVector = Icons.Filled.Favorite, + imageVector = CompoundIcons.Favourite(), contentDescription = null ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt index 9513a2468f1..57ecb9c9674 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt @@ -9,12 +9,12 @@ package io.element.android.libraries.designsystem.theme.components.previews import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TimePicker import androidx.compose.material3.TimePickerLayoutType import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementThemedPreview @@ -35,7 +35,7 @@ internal fun TimePickerHorizontalPreview() { shape = AlertDialogDefaults.shape, containerColor = AlertDialogDefaults.containerColor, tonalElevation = AlertDialogDefaults.TonalElevation, - buttonContentColor = MaterialTheme.colorScheme.primary, + buttonContentColor = ElementTheme.colors.textPrimary, iconContentColor = AlertDialogDefaults.iconContentColor, titleContentColor = AlertDialogDefaults.titleContentColor, textContentColor = AlertDialogDefaults.textContentColor, @@ -57,7 +57,7 @@ internal fun TimePickerVerticalLightPreview() { shape = AlertDialogDefaults.shape, containerColor = AlertDialogDefaults.containerColor, tonalElevation = AlertDialogDefaults.TonalElevation, - buttonContentColor = MaterialTheme.colorScheme.primary, + buttonContentColor = ElementTheme.colors.textPrimary, iconContentColor = AlertDialogDefaults.iconContentColor, titleContentColor = AlertDialogDefaults.titleContentColor, textContentColor = AlertDialogDefaults.textContentColor, @@ -83,7 +83,7 @@ internal fun TimePickerVerticalDarkPreview() { shape = AlertDialogDefaults.shape, containerColor = AlertDialogDefaults.containerColor, tonalElevation = AlertDialogDefaults.TonalElevation, - buttonContentColor = MaterialTheme.colorScheme.primary, + buttonContentColor = ElementTheme.colors.textPrimary, iconContentColor = AlertDialogDefaults.iconContentColor, titleContentColor = AlertDialogDefaults.titleContentColor, textContentColor = AlertDialogDefaults.textContentColor, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientation.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientation.kt index bb2833a0224..37e6e1c93e0 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientation.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientation.kt @@ -8,14 +8,13 @@ package io.element.android.libraries.designsystem.utils import android.content.pm.ActivityInfo -import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.platform.LocalContext @Composable fun ForceOrientation(orientation: ScreenOrientation) { - val activity = LocalContext.current as? ComponentActivity ?: return + val activity = LocalActivity.current ?: return val orientationFlags = when (orientation) { ScreenOrientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ScreenOrientation.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt new file mode 100644 index 00000000000..5c1288f0e72 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.utils + +import androidx.compose.runtime.staticCompositionLocalOf + +/** + * A composition local that indicates whether the app is running in UI test mode. + */ +val LocalUiTestMode = staticCompositionLocalOf { false } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OpenUrlInTabView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OpenUrlInTabView.kt new file mode 100644 index 00000000000..9770c6849d3 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/OpenUrlInTabView.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.utils + +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab + +@Suppress("MutableStateParam") +@Composable +fun OpenUrlInTabView(url: MutableState) { + val activity = requireNotNull(LocalActivity.current) + val darkTheme = ElementTheme.isLightTheme.not() + + LaunchedEffect(url.value) { + url.value?.let { + activity.openUrlInChromeCustomTab(null, darkTheme, it) + url.value = null + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt index f70cb5424e0..a3a471875be 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt @@ -9,10 +9,12 @@ package io.element.android.libraries.designsystem.utils import androidx.compose.foundation.layout.WindowInsets import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection @Composable +@ReadOnlyComposable fun WindowInsets.copy( top: Int? = null, right: Int? = null, diff --git a/libraries/designsystem/src/main/res/drawable/ic_cancel.xml b/libraries/designsystem/src/main/res/drawable/ic_cancel.xml deleted file mode 100644 index c36efcec943..00000000000 --- a/libraries/designsystem/src/main/res/drawable/ic_cancel.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/libraries/designsystem/src/main/res/drawable/ic_encryption_enabled.xml b/libraries/designsystem/src/main/res/drawable/ic_encryption_enabled.xml deleted file mode 100644 index c29636b311d..00000000000 --- a/libraries/designsystem/src/main/res/drawable/ic_encryption_enabled.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/libraries/designsystem/src/main/res/drawable/ic_notification_small.xml b/libraries/designsystem/src/main/res/drawable/ic_notification.xml similarity index 100% rename from libraries/designsystem/src/main/res/drawable/ic_notification_small.xml rename to libraries/designsystem/src/main/res/drawable/ic_notification.xml diff --git a/libraries/designsystem/src/main/res/drawable/ic_plus_composer.xml b/libraries/designsystem/src/main/res/drawable/ic_plus_composer.xml deleted file mode 100644 index e22f124645d..00000000000 --- a/libraries/designsystem/src/main/res/drawable/ic_plus_composer.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataTest.kt b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataTest.kt new file mode 100644 index 00000000000..01395eb9f1a --- /dev/null +++ b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class AvatarDataTest { + @Test + fun `initial with text should get the first char, uppercased`() { + val data = AvatarData("id", "test", null, AvatarSize.InviteSender) + assertThat(data.initialLetter).isEqualTo("T") + } + + @Test + fun `initial with leading whitespace should get the first non-whitespace char, uppercased`() { + val data = AvatarData("id", " test", null, AvatarSize.InviteSender) + assertThat(data.initialLetter).isEqualTo("T") + } + + @Test + fun `initial with long emoji should get the full emoji`() { + val data = AvatarData("id", "\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08 Test", null, AvatarSize.InviteSender) + assertThat(data.initialLetter).isEqualTo("\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08") + } + + @Test + fun `initial with short emoji should get the emoji`() { + val data = AvatarData("id", "✂ Test", null, AvatarSize.InviteSender) + assertThat(data.initialLetter).isEqualTo("✂") + } + + @Test + fun `initial with a single letter should take that letter`() { + val data = AvatarData("id", "T", null, AvatarSize.InviteSender) + assertThat(data.initialLetter).isEqualTo("T") + } +} diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/AppCoroutineScope.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/AppCoroutineScope.kt new file mode 100644 index 00000000000..ea597e56e17 --- /dev/null +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/AppCoroutineScope.kt @@ -0,0 +1,18 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.di.annotations + +import javax.inject.Qualifier + +/** + * Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for the application. + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Qualifier +annotation class AppCoroutineScope diff --git a/libraries/encrypted-db/build.gradle.kts b/libraries/encrypted-db/build.gradle.kts index 7e7b5bab848..ea53646cfc1 100644 --- a/libraries/encrypted-db/build.gradle.kts +++ b/libraries/encrypted-db/build.gradle.kts @@ -23,7 +23,7 @@ dependencies { implementation(libs.sqldelight.driver.android) implementation(libs.sqlcipher) implementation(libs.sqlite) - implementation(libs.androidx.security.crypto) + implementation(libs.google.tink) implementation(projects.libraries.androidutils) } diff --git a/libraries/encrypted-db/consumer-proguard-rules.pro b/libraries/encrypted-db/consumer-proguard-rules.pro index 5d01f21e9b4..f1b424510da 100644 --- a/libraries/encrypted-db/consumer-proguard-rules.pro +++ b/libraries/encrypted-db/consumer-proguard-rules.pro @@ -19,7 +19,3 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile - -# Prevent ProGuard from renaming internal SQLCipher classes, which breaks the library. -# From https://github.com/sqlcipher/android-database-sqlcipher#proguard --keep class net.sqlcipher.** { *; } diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt index c1c2c6eb2ac..172e4b52853 100644 --- a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt @@ -13,10 +13,10 @@ import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema import app.cash.sqldelight.driver.android.AndroidSqliteDriver import io.element.encrypteddb.passphrase.PassphraseProvider -import net.sqlcipher.database.SupportFactory +import net.zetetic.database.sqlcipher.SupportOpenHelperFactory /** - * Creates an encrypted version of the [SqlDriver] using SQLCipher's [SupportFactory]. + * Creates an encrypted version of the [SqlDriver] using SQLCipher's [SupportOpenHelperFactory]. * @param passphraseProvider Provides the passphrase needed to use the SQLite database with SQLCipher. */ class SqlCipherDriverFactory( @@ -29,8 +29,9 @@ class SqlCipherDriverFactory( * @param context Android [Context], used to instantiate the driver. */ fun create(schema: SqlSchema>, name: String, context: Context): SqlDriver { + System.loadLibrary("sqlcipher") val passphrase = passphraseProvider.getPassphrase() - val factory = SupportFactory(passphrase) + val factory = SupportOpenHelperFactory(passphrase) return AndroidSqliteDriver(schema = schema, context = context, name = name, factory = factory) } } diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFile.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFile.kt new file mode 100644 index 00000000000..9e9f6e1b2f5 --- /dev/null +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFile.kt @@ -0,0 +1,82 @@ +/* + * 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.encrypteddb.crypto + +import android.content.Context +import com.google.crypto.tink.KeyTemplates +import com.google.crypto.tink.RegistryConfiguration +import com.google.crypto.tink.StreamingAead +import com.google.crypto.tink.integration.android.AndroidKeysetManager +import com.google.crypto.tink.streamingaead.StreamingAeadConfig +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream + +/** + * This class is used to write and read encrypted data to/from a file. + * + * It's a simplified version of the same class in [androidx.security.crypto](https://developer.android.com/reference/androidx/security/crypto/package-summary). + * + * It uses hardcoded constants that are used in that library, for backwards compatibility reasons. + */ +internal class EncryptedFile( + private val context: Context, + private val file: File +) { + companion object { + /** + * The file content is encrypted using StreamingAead with AES-GCM, with the file name as associated data. + */ + private const val KEYSET_ENCRYPTION_SCHEME = "AES256_GCM_HKDF_4KB" + + /** + * The keyset is stored in a shared preference file with this name. + */ + private const val KEYSET_PREF_FILE_NAME = "__androidx_security_crypto_encrypted_file_pref__" + + /** + * The keyset is stored in a shared preference with this key, inside the specified file. + */ + private const val KEYSET_ALIAS = "__androidx_security_crypto_encrypted_file_keyset__" + + /** + * The URI referencing the master key in the Android Keystore used to encrypt/decrypt the keyset. + */ + private const val MASTER_KEY_URI = "android-keystore://_androidx_security_master_key_" + } + + private val androidKeysetManager by lazy { + val keysetManagerBuilder = AndroidKeysetManager.Builder() + .withKeyTemplate(KeyTemplates.get(KEYSET_ENCRYPTION_SCHEME)) + .withSharedPref(context, KEYSET_ALIAS, KEYSET_PREF_FILE_NAME) + .withMasterKeyUri(MASTER_KEY_URI) + + keysetManagerBuilder.build() + } + + private val streamingAead: StreamingAead by lazy { + val streamingAeadKeysetHandle = androidKeysetManager.keysetHandle + streamingAeadKeysetHandle.getPrimitive(RegistryConfiguration.get(), StreamingAead::class.java) + } + + init { + StreamingAeadConfig.register() + } + + fun openFileOutput(): FileOutputStream { + val fos = FileOutputStream(file) + val stream = streamingAead.newEncryptingStream(fos, file.name.toByteArray()) + return EncryptedFileOutputStream(fos.fd, stream) + } + + fun openFileInput(): FileInputStream { + val fis = FileInputStream(file) + val stream = streamingAead.newDecryptingStream(fis, file.name.toByteArray()) + return EncryptedFileInputStream(fis.fd, stream) + } +} diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFileInputStream.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFileInputStream.kt new file mode 100644 index 00000000000..8ef15f3be58 --- /dev/null +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFileInputStream.kt @@ -0,0 +1,53 @@ +/* + * 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.encrypteddb.crypto + +import android.os.Build +import androidx.annotation.RequiresApi +import java.io.FileDescriptor +import java.io.FileInputStream +import java.io.InputStream + +/** + * This class is used to read encrypted data from a file. + * + * It comes directly from [androidx.security.crypto](https://developer.android.com/reference/androidx/security/crypto/package-summary). + */ +internal class EncryptedFileInputStream( + fileDescriptor: FileDescriptor, + private val inputStream: InputStream, +) : FileInputStream(fileDescriptor) { + private val lock = Any() + + override fun read(): Int = inputStream.read() + + override fun read(b: ByteArray?): Int = inputStream.read(b) + + override fun read(b: ByteArray?, off: Int, len: Int): Int = inputStream.read(b, off, len) + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun readAllBytes(): ByteArray? = inputStream.readAllBytes() + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun readNBytes(b: ByteArray?, off: Int, len: Int): Int = inputStream.readNBytes(b, off, len) + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun readNBytes(len: Int): ByteArray? = inputStream.readNBytes(len) + + override fun skip(n: Long): Long = inputStream.skip(n) + + override fun available(): Int = inputStream.available() + + override fun mark(readlimit: Int) = synchronized(lock) { inputStream.mark(readlimit) } + + override fun markSupported(): Boolean = inputStream.markSupported() + + override fun reset() = synchronized(lock) { inputStream.reset() } + + override fun close() = inputStream.close() +} diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFileOutputStream.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFileOutputStream.kt new file mode 100644 index 00000000000..539ca0f4907 --- /dev/null +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/crypto/EncryptedFileOutputStream.kt @@ -0,0 +1,32 @@ +/* + * 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.encrypteddb.crypto + +import java.io.FileDescriptor +import java.io.FileOutputStream +import java.io.OutputStream + +/** + * This class is used to write encrypted data to a file. + * + * It comes directly from [androidx.security.crypto](https://developer.android.com/reference/androidx/security/crypto/package-summary). + */ +internal class EncryptedFileOutputStream( + fileDescriptor: FileDescriptor, + private val outputStream: OutputStream +) : FileOutputStream(fileDescriptor) { + override fun write(b: ByteArray?) = outputStream.write(b) + + override fun write(b: ByteArray?, off: Int, len: Int) = outputStream.write(b, off, len) + + override fun write(b: Int) = outputStream.write(b) + + override fun flush() = outputStream.flush() + + override fun close() = outputStream.close() +} diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomSecretPassphraseProvider.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomSecretPassphraseProvider.kt index 39bd683f278..601dce508a0 100644 --- a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomSecretPassphraseProvider.kt +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomSecretPassphraseProvider.kt @@ -8,8 +8,7 @@ package io.element.encrypteddb.passphrase import android.content.Context -import androidx.security.crypto.EncryptedFile -import io.element.android.libraries.androidutils.file.EncryptedFileFactory +import io.element.encrypteddb.crypto.EncryptedFile import java.io.File import java.security.SecureRandom @@ -25,7 +24,7 @@ class RandomSecretPassphraseProvider( private val secretSize: Int = 256, ) : PassphraseProvider { override fun getPassphrase(): ByteArray { - val encryptedFile = EncryptedFileFactory(context).create(file) + val encryptedFile = EncryptedFile(context, file) return if (!file.exists()) { val secret = generateSecret() encryptedFile.openFileOutput().use { it.write(secret) } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RoomMembershipContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RoomMembershipContentFormatter.kt index 4e920ea2655..5d6e695b7c5 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RoomMembershipContentFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RoomMembershipContentFormatter.kt @@ -26,6 +26,7 @@ class RoomMembershipContentFormatter @Inject constructor( val userId = membershipContent.userId val memberIsYou = matrixClient.isMe(userId) val userDisplayNameOrId = membershipContent.userDisplayName ?: userId.value + val reason = membershipContent.reason?.takeIf { it.isNotBlank() } return when (membershipContent.change) { MembershipChange.JOINED -> if (memberIsYou) { sp.getString(R.string.state_event_room_join_by_you) @@ -38,9 +39,17 @@ class RoomMembershipContentFormatter @Inject constructor( sp.getString(R.string.state_event_room_leave, senderDisambiguatedDisplayName) } MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) { - sp.getString(R.string.state_event_room_ban_by_you, userDisplayNameOrId) + if (reason != null) { + sp.getString(R.string.state_event_room_ban_by_you_with_reason, userDisplayNameOrId, reason) + } else { + sp.getString(R.string.state_event_room_ban_by_you, userDisplayNameOrId) + } } else { - sp.getString(R.string.state_event_room_ban, senderDisambiguatedDisplayName, userDisplayNameOrId) + if (reason != null) { + sp.getString(R.string.state_event_room_ban_with_reason, senderDisambiguatedDisplayName, userDisplayNameOrId, reason) + } else { + sp.getString(R.string.state_event_room_ban, senderDisambiguatedDisplayName, userDisplayNameOrId) + } } MembershipChange.UNBANNED -> if (senderIsYou) { sp.getString(R.string.state_event_room_unban_by_you, userDisplayNameOrId) @@ -48,9 +57,17 @@ class RoomMembershipContentFormatter @Inject constructor( sp.getString(R.string.state_event_room_unban, senderDisambiguatedDisplayName, userDisplayNameOrId) } MembershipChange.KICKED -> if (senderIsYou) { - sp.getString(R.string.state_event_room_remove_by_you, userDisplayNameOrId) + if (reason != null) { + sp.getString(R.string.state_event_room_remove_by_you_with_reason, userDisplayNameOrId, reason) + } else { + sp.getString(R.string.state_event_room_remove_by_you, userDisplayNameOrId) + } } else { - sp.getString(R.string.state_event_room_remove, senderDisambiguatedDisplayName, userDisplayNameOrId) + if (reason != null) { + sp.getString(R.string.state_event_room_remove_with_reason, senderDisambiguatedDisplayName, userDisplayNameOrId, reason) + } else { + sp.getString(R.string.state_event_room_remove, senderDisambiguatedDisplayName, userDisplayNameOrId) + } } MembershipChange.INVITED -> if (senderIsYou) { sp.getString(R.string.state_event_room_invite_by_you, userDisplayNameOrId) diff --git a/libraries/eventformatter/impl/src/main/res/values-be/translations.xml b/libraries/eventformatter/impl/src/main/res/values-be/translations.xml index 5f1722ad1aa..78ca6d5b15f 100644 --- a/libraries/eventformatter/impl/src/main/res/values-be/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-be/translations.xml @@ -28,7 +28,7 @@ "%1$s запрасіў(-ла) вас" "%1$s далучыўся(-лась) да пакоя" "Вы далучыліся да пакоя" - "%1$s прасіў(-ла) далучыцца" + "%1$sпросіць далучыцца" "%1$s дазволіў(-ла) %2$s далучыцца" "Вы дазволілі %1$s далучыцца" "Вы прасілі далучыцца" diff --git a/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml b/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml index 3a5a3d18305..b1f3fe1f115 100644 --- a/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-bg/translations.xml @@ -22,9 +22,9 @@ "%1$s ви покани" "%1$s се присъедини към стаята" "Вие се присъединихте към стаята" - "%1$s поиска да се присъедини" - "%1$s позволи на %2$s да се присъедини" - "%1$s ви позволи да се присъедините" + "%1$s иска да се присъедини" + "%1$s получи достъп до %2$s" + "Вие позволихте на %1$s да се присъедини" "Вие поискахте да се присъедините" "%1$s вече не се интересува от присъединяване" "%1$s напусна стаята" @@ -35,6 +35,10 @@ "Вие премахнахте името на стаята" "%1$s не направи промени" "Не направихте промени" + "%1$s промени закачените съобщения" + "Вие променихте закачените съобщения" + "%1$s закачи съобщение" + "Вие закачихте съобщение" "%1$s отхвърли поканата" "Вие отхвърлихте поканата" "%1$s премахна %2$s" diff --git a/libraries/eventformatter/impl/src/main/res/values-cs/translations.xml b/libraries/eventformatter/impl/src/main/res/values-cs/translations.xml index d526b96815c..98a5bd2b208 100644 --- a/libraries/eventformatter/impl/src/main/res/values-cs/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-cs/translations.xml @@ -19,6 +19,8 @@ "Odstranili jste obrázek místnosti" "%1$s vykázal(a) %2$s" "Vykázali jste %1$s" + "Vykázali jste %1$s: %2$s" + "%1$s vykázal(a) %2$s: %3$s" "%1$s založil(a) místnost" "Založili jste místnost" "%1$s pozval(a) %2$s" @@ -55,6 +57,8 @@ "Odmítli jste pozvání" "%1$s odebral(a) %2$s" "Odebrali jste %1$s" + "Odstranili jste %1$s: %2$s" + "%1$s odstranil(a) %2$s: %3$s" "%1$s do této místnosti pozval(a) %2$s" "Poslali jste %1$s pozvání do místnosti" "%1$s zrušil(a) pozvánku do místnosti pro %2$s" diff --git a/libraries/eventformatter/impl/src/main/res/values-cy/translations.xml b/libraries/eventformatter/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..e4aec60ffac --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,73 @@ + + + "(newidiwyd yr afatar hefyd)" + "Mae %1$s wedi newid eu afatar" + "Rydych chi wedi newid eich afatar" + "Cafodd %1$s ei israddio i aelod" + "Cafodd %1$s ei israddio i gymedrolwr" + "Newidiodd %1$s ei enw dangos o %2$s i %3$s" + "Rydych chi wedi newid eich enw dangos o %1$s i %2$s" + "Mae %1$s wedi tynnu ei enw dangos (%2$s ydoedd)" + "Rydych wedi dileu eich enw dangos (%1$s ydoedd)" + "Mae %1$s wedi gosod ei enw dangos i %2$s" + "Rydych chi wedi gosod eich enw dangos i %1$s" + "Cafodd %1$s ei godi i fod yn weinyddol" + "Cafodd %1$s ei godi i fod yn gymedrolwr" + "Mae %1$s wedi newid afatar yr ystafell" + "Rydych chi wedi newid afatar yr ystafell" + "Mae %1$s wedi tynnu afatar yr ystafell" + "Rydych chi wedi tynnu afatar yr ystafell" + "Mae %1$s wedi gwahardd %2$s" + "Rydych wedi gwahardd %1$s" + "Rydych wedi gwahardd %1$s: %2$s" + "Mae %1$s wedi gwahardd %2$s: %3$s" + "%1$s greodd yr ystafell" + "Chi greodd yr ystafell" + "Mae %1$s wedi gwahodd %2$s" + "Derbyniodd %1$s y gwahoddiad" + "Rydych chi wedi derbyn y gwahoddiad" + "Rydych wedi gwahodd %1$s" + "Mae %1$s wedi eich gwahodd" + "Ymunodd %1$s â\'r ystafell" + "Rydych chi wedi ymuno â\'r ystafell" + "Mae %1$s yn gofyn i gael ymuno" + "Mae %1$s wedi rhoi mynediad i %2$s" + "Rydych chi wedi caniatáu i %1$s ymuno" + "Rydych chi wedi gwneud cais i ymuno" + "Mae %1$s wedi gwrthod cais %2$s i ymuno" + "Rydych wedi gwrthod cais %1$s i ymuno" + "Mae %1$s wedi gwrthod eich cais i ymuno" + "Does gan %1$s ddim diddordeb mewn ymuno bellach" + "Rydych wedi diddymu\'ch cais i ymuno" + "Mae %1$s wedi gadael yr ystafell" + "Rydych wedi gadael yr ystafell" + "Mae %1$s wedi newid enw\'r ystafell i: %2$s" + "Rydych chi wedi newid enw\'r ystafell i: %1$s" + "Mae %1$s wedi tynnu enw\'r ystafell" + "Rydych wedi dileu enw\'r ystafell" + "Gwnaeth %1$s dim newidiadau" + "Rydych heb wneud unrhyw newidiadau" + "Mae %1$s wedi newid y negeseuon sydd wedi\'u pinio" + "Rydych wedi newid y negeseuon sydd wedi\'u pinio" + "Mae %1$s wedi pinio neges" + "Rydych chi wedi pinio neges" + "Mae %1$s wedi\'i dad-binio neges" + "Rydych chi wedi dad-binio neges" + "Mae %1$s wedi gwrthod y gwahoddiad" + "Rydych chi wedi gwrthod y gwahoddiad" + "Mae %1$s wedi tynnu %2$s" + "Rydych wedi dileu %1$s" + "Rydych wedi dileu %1$s: %2$s" + "Mae %1$s wedi tynnu %2$s: %3$s" + "Anfonodd %1$s wahoddiad at %2$s i ymuno â\'r ystafell" + "Rydych wedi anfon gwahoddiad i %1$s i ymuno â\'r ystafell" + "Dirymodd %1$s y gwahoddiad i %2$s ymuno â\'r ystafell" + "Rydych wedi dirymu\'r gwahoddiad i %1$s ymuno â\'r ystafell" + "Newidiodd %1$s y pwnc i: %2$s" + "Rydych wedi newid y pwnc i: %1$s" + "Mae %1$s wedi dileu pwnc yr ystafell" + "Rydych wedi dileu pwnc yr ystafell" + "Mae %1$s wedi dad-wahardd %2$s" + "Rydych wedi dad-wahardd %1$s" + "Mae %1$s wedi gwneud newid anhysbys i\'w aelodaeth" + diff --git a/libraries/eventformatter/impl/src/main/res/values-da/translations.xml b/libraries/eventformatter/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..db3be216f7f --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,73 @@ + + + "(avataren blev også ændret)" + "%1$s ændrede sin avatar" + "Du har ændret din avatar" + "%1$s blev nedgraderet til medlem" + "%1$s blev nedgraderet til moderator" + "%1$s ændrede sit viste navn fra %2$s til %3$s" + "Du har ændret dit viste navn fra %1$s til %2$s" + "%1$s fjernede sit viste navn (det var %2$s )" + "Du fjernede dit viste navn (det var%1$s)" + "%1$s har sat deres visningsnavn til %2$s" + "Du har indstillet dit viste navn til %1$s" + "%1$s blev forfremmet til admin" + "%1$s blev forfremmet til moderator" + "%1$s ændrede rummets avatar" + "Du ændrede rummets avatar" + "%1$s fjernede rummets avatar" + "Du fjernede rummets avatar" + "%1$s bortviste %2$s" + "Du bortviste %1$s" + "Du spærrede %1$s: %2$s" + "%1$s spærrede %2$s: %3$s" + "%1$s skabte rummet" + "Du skabte rummet" + "%1$sinviterede %2$s" + "%1$s accepterede invitationen" + "Du har accepteret invitationen" + "Du inviterede %1$s" + "%1$s inviterede dig" + "%1$s sluttede sig til rummet" + "Du sluttede dig til rummet" + "%1$s anmoder om at deltage" + "%1$s har givet adgang til %2$s" + "Du tillod %1$s at være med" + "Du har anmodet om at deltage" + "%1$s har afvist %2$ss anmodning om at deltage" + "Du har afvist %1$ss anmodning om at deltage" + "%1$s afviste din anmodning om at deltage" + "%1$s er ikke længere interesseret i at deltage" + "Du har annulleret din anmodning om at deltage" + "%1$s forlod rummet" + "Du forlod rummet" + "%1$s ændrede rummets navn til: %2$s" + "Du ændrede rummets navn til: %1$s" + "%1$s fjernede rummets navn" + "Du fjernede rummets navn" + "%1$s foretog ingen ændringer" + "Du har ikke foretaget nogen ændringer" + "%1$s ændrede de fastgjorte beskeder" + "Du har ændret de fastgjorte beskeder" + "%1$s fastgjorde en besked" + "Du har fastgjort en besked" + "%1$s frigjorde en besked" + "Du frigjorde en besked" + "%1$s afviste invitationen" + "Du afviste invitationen" + "%1$s fjernede %2$s" + "Du fjernede %1$s" + "Du fjernede %1$s :%2$s" + "%1$s fjernede %2$s: %3$s" + "%1$s har sendt en invitation til %2$s om at deltage i rummet" + "Du har sendt en invitation til %1$s om at deltage i rummet" + "%1$s tilbagekaldte invitationen til %2$s om at være med i rummet" + "Du tilbagekaldte invitationen til %1$s om at være med i rummet" + "%1$s ændrede emnet til: %2$s" + "Du har ændret emnet til: %1$s" + "%1$s fjernede rummets emne" + "Du har fjernet rummets emne" + "%1$s ophævede bortvisningen af %2$s" + "Du har fjernet bortvisningen af %1$s" + "%1$s har foretaget en ukendt ændring af deres medlemskab" + diff --git a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml index 8c99ba44ded..7069bf6b3fc 100644 --- a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml @@ -2,68 +2,72 @@ "(Avatar wurde auch geändert)" "%1$s hat den Avatar geändert" - "Du hast deinen Avatar geändert" + "Sie änderten ihren Avatar" "%1$s wurde zum Mitglied herabgestuft" "%1$s wurde zum Moderator herabgestuft" "%1$s hat den Anzeigenamen von %2$s auf %3$s geändert" "Du hast deinen Anzeigenamen von %1$s auf %2$s geändert" "%1$s hat den Anzeigenamen entfernt (war %2$s)" - "Du hast deinen Anzeigenamen entfernt (war %1$s)" + "Sie haben Ihren Anzeigenamen entfernt (er lautete %1$s)" "%1$s hat den Anzeigenamen auf %2$s geändert" "Du hast deinen Anzeigenamen zu %1$s geändert" "%1$s ist jetzt Administrator*in" "%1$s ist jetzt Moderator*in" "%1$s hat den Raum-Avatar geändert" - "Du hast den Raum-Avatar geändert" + "Dir haben den Zimmer-Avatar geändert" "%1$s hat den Raum-Avatar entfernt" - "Du hast den Raum-Avatar entfernt" + "Sie haben den Raum-Avatar entfernt" "%1$s hat %2$s gesperrt" - "Du hast %1$s gesperrt" + "Sie haben %1$s gesperrt" + "Sie haben %1$s gesperrt: %2$s" + "%1$s sperrte %2$s: %3$s" "%1$s hat den Raum erstellt" - "Du hast den Raum erstellt" + "Sie erstellten den Chatroom" "%1$s hat %2$s eingeladen" "%1$s hat die Einladung angenommen" - "Du hast die Einladung angenommen" - "Du hast %1$s eingeladen" + "Sie haben die Einladung angenommen" + "Sie luden %1$s ein" "%1$s hat dich eingeladen" "%1$s hat den Raum betreten" - "Du hast den Raum betreten" + "Sie sind dem Raum beigetreten" "%1$s beantragt den Beitritt" "%1$s hat %2$s den Beitritt erlaubt" - "Du hast %1$s den Beitritt erlaubt." - "Du hast angefragt beizutreten" + "Sie genehmigten die Beitrittsanfrage für %1$s" + "Sie haben um Beitritt gebeten" "%1$s hat die Beitrittsanfrage von %2$s abgelehnt" - "Du hast die Beitrittsanfrage von %1$s abgelehnt" + "Sie haben die Beitrittsanfrage für %1$s abgelehnt" "%1$s hat deine Beitrittsanfrage abgelehnt" "%1$s ist nicht mehr an einem Beitritt interessiert" - "Du hast deine Beitrittsanfrage zurückgezogen" + "Sie haben ihre Beitrittsanfrage widerrufen" "%1$s hat den Raum verlassen" - "Du hast den Raum verlassen" + "Sie verließen den Raum" "%1$s hat den Raum-Namen geändert in: %2$s" - "Du hast den Raum-Namen geändert in: %1$s" + "Sie haben den Raumnamen geändert in: %1$s" "%1$s hat den Raum-Namen entfernt" - "Du hast den Raum-Namen entfernt" - "%1$shat keine Änderungen vorgenommen" - "Du hast keine Änderungen vorgenommen" + "Sie haben den Raumnamen entfernt" + "%1$s hat keine Änderungen vorgenommen" + "Sie haben keine Änderungen vorgenommen" "%1$s hat die fixierten Nachrichten geändert" - "Du hast die fixierten Nachrichten geändert" + "Sie haben die angehefteten Nachrichten geändert." "%1$s fixierte eine Nachricht" - "Du hast eine Nachricht fixiert" + "Sie haben eine Nachricht angeheftet." "%1$s löste eine Nachricht" - "Du hast eine Nachricht gelöst" - "%1$s hat die Einladung abgelehnt" - "Du hast die Einladung abgelehnt" + "Sie haben eine angeheftete Nachricht entfernt." + "%1$s lehnte die Einladung ab" + "Sie lehnten die Einladung ab" "%1$s hat %2$s entfernt" - "Du hast %1$s entfernt" - "%1$s hat eine Einladung an %2$s gesendet, dem Raum beizutreten" - "Du hast eine Einladung an %1$s gesendet, dem Raum beizutreten" + "Sie haben %1$s entfernt" + "Sie haben %1$s entfernt: %2$s" + "%1$s entfernt %2$s: %3$s" + "%1$s hat %2$s eingeladen, den Raum zu beizutreten" + "Sie haben eine Einladung an %1$s gesendet, um dem Chatroom beizutreten." "%1$s hat die Einladung an %2$s zum Betreten des Raums zurückgezogen" - "Du hast die Einladung an %1$s zum Betreten des Raums zurückgezogen" + "Sie haben die Einladung für %1$s widerrufen" "%1$s hat das Thema geändert in: %2$s" - "Du hast das Thema geändert in: %1$s" + "Sie haben das Thema geändert in:%1$s" "%1$s hat das Raum-Thema entfernt" - "Du hast das Raum-Thema entfernt" + "Sie haben das Raumthema entfernt" "%1$s hat die Sperre für %2$s aufgehoben" - "Du hast die Sperre für %1$s aufgehoben" + "Sie haben die Sperre für %1$s aufgehoben" "%1$s hat eine unbekannte Änderung vorgenommen" diff --git a/libraries/eventformatter/impl/src/main/res/values-el/translations.xml b/libraries/eventformatter/impl/src/main/res/values-el/translations.xml index 8c89d65ea5b..f34016cd672 100644 --- a/libraries/eventformatter/impl/src/main/res/values-el/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-el/translations.xml @@ -13,21 +13,23 @@ "Όρισες το εμφανιζόμενο όνομά σου σε %1$s" "Ο χρήστης %1$s προήχθη σε διαχειριστής" "Ο χρήστης %1$s προήχθη σε συντονιστής" - "Ο χρήστης %1$s άλλαξε το άβαταρ του δωματίου" - "Άλλαξες το άβαταρ του δωματίου" - "Ο χρήστης %1$s αφαίρεσε το άβαταρ του δωματίου" - "Αφαίρεσες το άβαταρ του δωματίου" + "%1$s άλλαξε την εικόνα προφίλ της αίθουσας" + "Αλλάξατε την εικόνα προφίλ της αίθουσας" + "%1$s αφαίρεσε την εικόνα προφίλ της αίθουσας" + "Αφαιρέσατε την εικόνα προφίλ της αίθουσας" "Ο χρήστης %1$s απέκλεισε τον χρήστη %2$s" "Απέκλεισες τον χρήστη %1$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 ζητάει να συμμετάσχει" "Ο χρήστης %1$s επέτρεψε τον χρήστη %2$s" "Επέστρεψες στον χρήστη%1$s να συμμετάσχει" @@ -37,12 +39,12 @@ "Ο χρήστης %1$s απέρριψε το αίτημά σου για συμμετοχή" "Ο χρήστης %1$s δεν ενδιαφέρεται πλέον να συμμετάσχει" "Ακύρωσες το αίτημά σου για συμμετοχή" - "Ο χρήστης %1$s αποχώρησε από το δωμάτιο" - "Έφυγες από το δωμάτιο" - "Ο χρήστης %1$s άλλαξε το όνομα του δωματίου σε: %2$s" - "Άλλαξες το όνομα του δωματίου σε: %1$s" - "Ο χρήστης %1$s αφαίρεσε το όνομα του δωματίου" - "Αφαίρεσες το όνομα του δωματίου" + "%1$s αποχώρησε από την αίθουσα" + "Αποχωρήσατε από την αίθουσα" + "%1$s άλλαξε το όνομα της αίθουσας σε: %2$s" + "Αλλάξατε το όνομα της αίθουσας σε: %1$s" + "%1$s αφαίρεσε το όνομα της αίθουσας" + "Αφαιρέσατε το όνομα της αίθουσας" "Ο χρήστης %1$s δεν έκανε καμία αλλαγή" "Δεν έκανες καμία αλλαγή" "Ο χρήστης %1$s άλλαξε τα καρφιτσωμένα μηνύματα" @@ -55,14 +57,16 @@ "Απέρριψες την πρόσκληση" "Ο χρήστης %1$s αφαίρεσε τον χρήστη %2$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 αφαίρεσε το θέμα της αίθουσας" + "Αφαιρέσατε το θέμα της αίθουσας" "Ο χρήστης %1$s έκανε άρση αποκλεισμού στον χρήστη %2$s" "Έκανες άρση αποκλεισμού στον χρήστη %1$s" "Ο χρήστης %1$s έκανε μια άγνωστη αλλαγή στην ιδιότητα μέλους του." diff --git a/libraries/eventformatter/impl/src/main/res/values-es/translations.xml b/libraries/eventformatter/impl/src/main/res/values-es/translations.xml index 33ed87e4234..186aad54557 100644 --- a/libraries/eventformatter/impl/src/main/res/values-es/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-es/translations.xml @@ -19,6 +19,8 @@ "Eliminaste el avatar de la sala" "%1$s expulsó permanentemente a %2$s" "Expulsaste permanentemente a %1$s" + "Vetaste a %1$s: %2$s" + "%1$s vetó a %2$s: %3$s" "%1$s creó la sala" "Tú creaste la sala" "%1$s invitó a %2$s" @@ -30,7 +32,7 @@ "Te uniste a la sala" "%1$s solicitó unirse" "%1$s permitió que %2$s se uniera" - "%1$s te permitió unirte" + "Permitiste que %1$s se uniera" "Solicitaste unirte" "%1$s rechazó la solicitud de %2$s para unirse" "Rechazaste la solicitud de %1$s para unirte" @@ -45,10 +47,18 @@ "Eliminaste el nombre de la sala" "%1$s no hizo cambios" "No has hecho ningún cambio" + "%1$s cambió los mensajes fijados" + "Has cambiado los mensajes fijados" + "%1$s fijó un mensaje" + "Has fijado un mensaje" + "%1$s desprendió un mensaje" + "Desprendiste un mensaje" "%1$s rechazó la invitación" "Rechazaste la invitación" "%1$s echó a %2$s" "Echaste a %1$s" + "Echaste a %1$s: %2$s" + "%1$s echó a %2$s: %3$s" "%1$s envió una invitación a %2$s para unirse a la sala" "Enviaste una invitación a %1$s para unirse a la sala" "%1$s revocó la invitación a %2$s para unirse a la sala" diff --git a/libraries/eventformatter/impl/src/main/res/values-et/translations.xml b/libraries/eventformatter/impl/src/main/res/values-et/translations.xml index 3f5baafb0be..497d41307e9 100644 --- a/libraries/eventformatter/impl/src/main/res/values-et/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-et/translations.xml @@ -19,6 +19,8 @@ "Sina eemaldasid jututoa tunnuspildi" "%1$s keelas %2$s ligipääsu" "Sina keelasid %1$s ligipääsu" + "Sina seadsid ligipääsukeelu kasutajale %1$s: %2$s" + "%1$s seadis ligipääsukeelu kasutajale %2$s: %3$s" "%1$s lõi jututoa" "Sina lõid jututoa" "%1$s saatis kutse kasutajale %2$s" @@ -55,6 +57,8 @@ "Sina lükkasid kutse tagasi" "%1$s eemaldas jututoast kasutaja %2$s" "Sina eemaldasid jututoast kasutaja %1$s" + "Sina eemaldasid kasutaja %1$s: %2$s" + "%1$s eemaldas kasutaja %2$s: %3$s" "%1$s saatis jututoaga liitumiseks kutse kasutajale %2$s" "Sina saatsid kasutajale %1$s kutse jututoaga liitumiseks" "%1$s võttis tagasi jututoaga liitumise kutse kasutajalt %2$s" diff --git a/libraries/eventformatter/impl/src/main/res/values-eu/translations.xml b/libraries/eventformatter/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..20c4c8e1603 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,67 @@ + + + "(abatarra ere aldatu da)" + "%1$s(e)k abatarra aldatu du" + "Abatarra aldatu duzu" + "%1$s kide mailara jaitsi da" + "%1$s moderatzaile mailara jaitsi da" + "%1$s(e)k pantaila-izena %2$s(e)tik %3$s(e)ra aldatu du" + "Pantaila-izena %1$s(e)tik %2$s(e)ra aldatu duzu" + "%1$s(e)k pantaila-izena kendu du (%2$s zen)" + "Pantaila-izena kendu duzu (%1$s zen)" + "%1$s(e)k pantaila-izena %2$s(r)a aldatu du" + "Pantaila-izena %1$s(e)ra aldatu duzu" + "%1$s administratzaile mailara igo da" + "%1$s moderatzaile mailara igo da" + "%1$s(e)k gelako abatarra aldatu du" + "Gelako abatarra aldatu duzu" + "%1$s(e)k gelaren abatarra kendu du" + "Gelaren abatarra kendu duzu" + "%1$s(e)k %2$s(r) debekua ezarri dio" + "%1$s(r) debekua ezarri diozu" + "%1$s(e)k gela sortu du" + "Gela sortu duzu" + "%1$s(e)k %2$s gonbidatu du" + "%1$s(e)k gonbidapena onartu du" + "Gonbidapena onartu duzu" + "%1$s gonbidatu duzu" + "%1$s(e)k gonbidatu zaitu" + "%1$s gelara batu da" + "Gelara batu zara" + "%1$s(e)k batzeko eskaera egin du" + "%1$s(e)k batzeko baimena eman dio %2$s(r)i" + "Batzeko baimena eman diozu %1$s(r)i" + "Batzeko eskaera egin duzu" + "%1$s(e)k %2$s(r)en bat egiteko eskaera baztertu du" + "%1$s(r)en bat egiteko eskaera baztertu duzu" + "%1$s(e)k bat egiteko eskaera baztertu dizu" + "%1$s(e)k dagoeneko ez du bat egiteko interesik" + "Bat egiteko eskaera bertan behera utzi duzu" + "%1$s gelatik atera da" + "Gelatik atera zara" + "%1$s(e)k gelaren izena honakora aldatu du: %2$s" + "Gelaren izena honakora aldatu duzu: %1$s" + "%1$s(e)k gelaren izena kendu du" + "Gelaren izena kendu duzu" + "%1$s(e)k ez du aldaketarik egin" + "Ez duzu aldaketarik egin" + "%1$s(e)k finkatutako mezuak aldatu ditu" + "Finkatutako mezuak aldatu dituzu" + "%1$s(e)k mezu bat finkatu du" + "Mezu bat finkatu duzu" + "%1$s(e)k mezu bat finkatzeari utzi dio" + "Mezu bat finkatzeari utzi diozu" + "%1$s(e)k gonbidapena baztertu du" + "Gonbidapena baztertu duzu" + "%1$s(e)k %2$s kendu du" + "%1$s kendu duzu" + "%1$s(e)k %2$s(r)i gonbidapena bidali dio gelara batu dadin" + "Gonbidapena bidali diozu %1$s(r)i gelara batu dadin" + "%1$s(e)k gaia honakora aldatu du: %2$s" + "Gaia honakora aldatu duzu: %1$s" + "%1$s(e)k gelaren gaia aldatu du" + "Gelaren gaia aldatu duzu" + "%1$s(e)k %2$s(r)en debekua bertan behera utzi du" + "%1$s(r)en debekua bertan behera utzi duzu" + "%1$s(e)k kidetzan aldaketa ezezagun bat egin du" + diff --git a/libraries/eventformatter/impl/src/main/res/values-fi/translations.xml b/libraries/eventformatter/impl/src/main/res/values-fi/translations.xml index d3e2e6cc3b1..408935c7e88 100644 --- a/libraries/eventformatter/impl/src/main/res/values-fi/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-fi/translations.xml @@ -19,6 +19,8 @@ "Poistit huoneen avatarin" "%1$s antoi porttikiellon käyttäjälle %2$s" "Annoit porttikiellon käyttäjälle %1$s" + "Annoit porttikiellon käyttäjälle %1$s: %2$s" + "%1$s antoi porttikiellon käyttäjälle %2$s: %3$s" "%1$s loi huoneen" "Loit huoneen" "%1$s kutsui käyttäjän %2$s" @@ -55,6 +57,8 @@ "Hylkäsit kutsun" "%1$s poisti käyttäjän %2$s" "Poistit käyttäjän %1$s" + "Poistit käyttäjän %1$s: %2$s" + "%1$s poisti käyttäjän %2$s: %3$s" "%1$s kutsui käyttäjän %2$s huoneeseen" "Kutsuit käyttäjän %1$s huoneeseen" "%1$s peruutti käyttäjän %2$s kutsun huoneeseen" diff --git a/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml b/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml index dda1e4787d4..f8189ea040c 100644 --- a/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml @@ -19,6 +19,8 @@ "Vous avez supprimé l’avatar du salon" "%1$s a banni %2$s" "Vous avez banni %1$s" + "Vous avez banni %1$s: %2$s" + "%1$s a banni %2$s: %3$s" "%1$s a créé le salon" "Vous avez créé le salon" "%1$s a invité %2$s" @@ -55,6 +57,8 @@ "Vous avez refusé l’invitation" "%1$s a supprimé %2$s" "Vous avez supprimé %1$s" + "Vous avez supprimé %1$s: %2$s" + "%1$s a supprimé %2$s: %3$s" "%1$s a envoyé une invitation à %2$s à rejoindre le salon" "Vous avez envoyé une invitation à %1$s pour rejoindre le salon" "%1$s a révoqué l’invitation de %2$s à rejoindre le salon" diff --git a/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml b/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml index bfa63c86c32..311bf88df2a 100644 --- a/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml @@ -19,6 +19,8 @@ "Eltávolította a szoba profilképét" "%1$s kitiltotta: %2$s" "Kitiltotta: %1$s" + "Kitiltotta %1$s felhasználót: %2$s" + "%1$s kitiltotta %2$s felhasználót: %3$s" "%1$s létrehozta a szobát" "Létrehozta a szobát" "%1$s meghívta: %2$s" @@ -55,6 +57,8 @@ "Elutasította a meghívást" "%1$s eltávolította: %2$s" "Eltávolította: %1$s" + "Eltávolította %1$s felhasználót: %2$s" + "%1$s eltávolította %2$s felhasználót: %3$s" "%1$s meghívót küldött %2$s számára, hogy csatlakozzon a szobához" "Meghívót küldött %1$s számára, hogy csatlakozzon a szobához" "%1$s visszavonta %2$s meghívását, hogy csatlakozzon a szobához" diff --git a/libraries/eventformatter/impl/src/main/res/values-in/translations.xml b/libraries/eventformatter/impl/src/main/res/values-in/translations.xml index 3430b058132..89da513f91e 100644 --- a/libraries/eventformatter/impl/src/main/res/values-in/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-in/translations.xml @@ -19,6 +19,8 @@ "Anda menghapus avatar ruangan" "%1$s memblokir %2$s" "Anda memblokir %1$s" + "Anda mencekal %1$s: %2$s" + "%1$s mencekal %2$s: %3$s" "%1$s membuat ruangan" "Anda membuat ruangan" "%1$s mengundang %2$s" @@ -55,6 +57,8 @@ "Anda menolak undangan" "%1$s mengeluarkan %2$s" "Anda mengeluarkan %1$s" + "Anda menghapus %1$s: %2$s" + "%1$s menghapus %2$s: %3$s" "%1$s mengirimkan undangan kepada %2$s untuk bergabung ke ruangan" "Anda mengirimkan undangan kepada %1$s untuk bergabung ke ruangan" "%1$s menghapus undangan kepada %2$s untuk bergabung ke ruangan" diff --git a/libraries/eventformatter/impl/src/main/res/values-it/translations.xml b/libraries/eventformatter/impl/src/main/res/values-it/translations.xml index 1b7709dcb4b..d4b96e5c0e4 100644 --- a/libraries/eventformatter/impl/src/main/res/values-it/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-it/translations.xml @@ -19,6 +19,8 @@ "Hai rimosso l\'avatar della stanza" "%1$s ha escluso %2$s" "Hai escluso %1$s" + "Hai bannato %1$s: %2$s" + "%1$s ha bannato %2$s: %3$s" "%1$s ha creato la stanza" "Hai creato la stanza" "%1$s ha invitato %2$s" @@ -55,6 +57,8 @@ "Hai rifiutato l\'invito" "%1$s ha rimosso %2$s" "Hai rimosso %1$s" + "Hai rimosso%1$s: %2$s" + "%1$s ha rimosso %2$s: %3$s" "%1$s ha inviato un invito a %2$s per unirsi alla stanza" "Hai inviato un invito a %1$s per unirsi alla stanza" "%1$s ha revocato l\'invito di %2$s ad unirsi alla stanza." diff --git a/libraries/eventformatter/impl/src/main/res/values-ka/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ka/translations.xml index 7fa4f657163..f60914be436 100644 --- a/libraries/eventformatter/impl/src/main/res/values-ka/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-ka/translations.xml @@ -3,12 +3,16 @@ "(ფოტოც შეიცვალა)" "%1$s პროფილის ფოტო შეცვალა" "თქვენ შეცვალეთ პროფილის ფოტო" + "%1$s დაქვეითდა წევრამდე" + "%1$s დაქვეითდა მოდერატორამდე" "%1$s თავისი ნაჩვენები სახელი შეცვალა %2$s დან %3$s ზე" "თქვენ შეცვალეთ თქვენი ნაჩვენები სახელი %1$s -დან %2$s -ზე" "%1$s წაშალა თავისი ნაჩვენები სახელი (იყო %2$s)" "თქვენ წაშალეთ ნაჩვენები სახელი (იყო %1$s)" "%1$s თავისი ნაჩვენები სახელი შეცვალა %2$s" "თქვენი ახალი ნაჩვენები სახელი - %1$s" + "%1$s დაწინაურდა ადმინისტრატორამდე" + "%1$s დაწინაურდა მოდერატორამდე" "%1$s ოთახის ფოტო შეცვალა" "თქვენ შეცვალეთ ოთახის ფოტო" "%1$s წაშალა ოთახის ფოტო" @@ -39,6 +43,8 @@ "თქვენ შეცვალეთ ოთახის სახელი: %1$s" "%1$s წაშალა ოთახის სახელი" "თქვენ წაშალეთ ოთახის სახელი" + "%1$s ცვლილებები არ შეიტანა" + "თქვენ არაფერი არ შეგიცვლიათ" "%1$s მოწვევაზე უარი თქვა" "თქვენ უარი თქვით მოწვევაზე" "%1$s გააგდო %2$s" diff --git a/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml b/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..9fb1486d59d --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,57 @@ + + + "(taip pat buvo pakeistas ir avataras)" + "%1$s pakeitė savo avatarą" + "Jūs pakeitėte savo avatarą" + "%1$s pakeitė savo slapyvardį iš %2$s į %3$s" + "Jūs pakeitėte savo slapyvardį iš %1$s į %2$s" + "%1$s pašalino savo slapyvardį (jis buvo %2$s)" + "Jūs pašalinote savo slapyvardį (jis buvo %1$s)" + "%1$s pakeitė savo slapyvardį į %2$s" + "Jūs nustatėte savo slapyvardį į %1$s" + "%1$s pakeitė kambario avatarą" + "Jūs pakeitėte kambario avatarą" + "%1$s pašalino kambario avatarą" + "Jūs pašalinote kambario avatarą" + "%1$s uždraudė %2$s" + "Jūs uždraudėte %1$s" + "%1$s sukūrė kambarį" + "Jūs sukūrėte kambarį" + "%1$s pakvietė %2$s" + "%1$s priėmė kvietimą" + "Priėmėte kvietimą" + "Jūs pakvietėte %1$s" + "%1$s pakvietė Jus" + "%1$s prisijungė prie kambario" + "Jūs prisijungėte prie kambario" + "%1$s prašo prisijungti" + "%1$s suteikė prieigą %2$s" + "Jūs leidote %1$s prisijungti" + "Jūs paprašėte prisijungti" + "%1$s atmetė %2$s prisijungimo prašymą" + "Jūs atmetėte %1$s prisijungimo prašymą" + "%1$s atmetė Jūsų prisijungimo prašymą" + "%1$s nebenori prisijungti" + "Jūs atšaukėte savo prisijungimo prašymą" + "%1$s išėjo iš kambario" + "Jūs išėjote iš kambario" + "%1$s pakeitė kambario pavadinimą į: %2$s" + "Pakeitėte kambario pavadinimą į: %1$s" + "%1$s pašalino kambario pavadinimą" + "Jūs pašalinote kambario pavadinimą" + "%1$s atmetė kvietimą" + "Jūs atmetėte kvietimą" + "%1$s pašalino %2$s" + "Jūs pašalinote %1$s" + "%1$s išsiuntė kvietimą %2$s prisijungti prie kambario" + "Išsiuntėte kvietimą %1$s prisijungti prie kambario" + "%1$s atšaukė kvietimą %2$s prisijungti prie kambario" + "Jūs atšaukėte kvietimą %1$s prisijungti prie kambario" + "%1$s pakeitė temą į: %2$s" + "Pakeitėte temą į: %1$s" + "%1$s pašalino kambario temą" + "Jūs pašalinote kambario temą" + "%1$s panaikino uždraudimą %2$s" + "Jūs panaikinote uždraudimą %1$s" + "%1$s padarė nežinomą savo narystės pakeitimą" + diff --git a/libraries/eventformatter/impl/src/main/res/values-nb/translations.xml b/libraries/eventformatter/impl/src/main/res/values-nb/translations.xml index 6343ef89338..6faa6145eb5 100644 --- a/libraries/eventformatter/impl/src/main/res/values-nb/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-nb/translations.xml @@ -19,6 +19,8 @@ "Du fjernet romavataren" "%1$s utestengte %2$s" "Du utestengte %1$s" + "Du utestengte %1$s: %2$s" + "%1$s utestengte %2$s: %3$s" "%1$s opprettet rommet" "Du opprettet rommet" "%1$s inviterte %2$s" @@ -43,10 +45,20 @@ "Du endret romnavnet til: %1$s" "%1$s fjernet romnavnet" "Du fjernet romnavnet" + "%1$s gjorde ingen endringer" + "Du har ikke gjort noen endringer" + "%1$s endret de festede meldingene" + "Du endret de festede meldingene" + "%1$s festet en melding" + "Du festet en melding" + "%1$s løsnet en melding" + "Du løsnet en melding" "%1$s avviste invitasjonen" "Du avviste invitasjonen" "%1$s fjernet %2$s" "Du fjernet %1$s" + "Du fjernet %1$s: %2$s" + "%1$s fjernet %2$s: %3$s" "%1$s sendte en invitasjon til %2$s om å bli med i rommet" "Du sendte en invitasjon til %1$s om å bli med i rommet" "%1$s trakk tilbake invitasjonen til %2$s om å delta i rommet" diff --git a/libraries/eventformatter/impl/src/main/res/values-nl/translations.xml b/libraries/eventformatter/impl/src/main/res/values-nl/translations.xml index 1462ac4a24d..4cf94798d6d 100644 --- a/libraries/eventformatter/impl/src/main/res/values-nl/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-nl/translations.xml @@ -28,8 +28,8 @@ "%1$s heeft je uitgenodigd" "%1$s is tot de kamer toegetreden" "Je bent toegetreden tot de kamer" - "%1$s heeft gevraagd om toe te treden" - "%1$s heeft %2$s toegestaan toe te treden" + "%1$s vraagt om toe te treden" + "%1$s heeft %2$s toegang verleend" "Je hebt %1$s toegestaan toe te treden" "Je hebt gevraagd om toe te treden" "%1$s heeft %2$s\'s verzoek om toe te treden afgewezen" 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 60f442bb75f..e45227ad62f 100644 --- a/libraries/eventformatter/impl/src/main/res/values-pl/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-pl/translations.xml @@ -19,6 +19,8 @@ "Usunąłeś zdjęcie profilowe pokoju" "%1$s zbanował %2$s" "Zbanowałeś %1$s" + "Zbanowałeś %1$s: %2$s" + "%1$s zbanował %2$s: %3$s" "%1$s stworzył pokój" "Stworzyłeś pokój" "%1$s zaprosił %2$s" @@ -55,6 +57,8 @@ "Odrzuciłeś zaproszenie" "%1$s usunął %2$s" "Usunąłeś %1$s" + "Usunąłeś %1$s: %2$s" + "%1$s usunął %2$s: %3$s" "%1$s wysłał zaproszenie do %2$s, aby dołączył do pokoju" "Wysłano zaproszenie do %1$s, aby dołączył do pokoju" "%1$s cofnął zaproszenie dla %2$s do tego pokoju" diff --git a/libraries/eventformatter/impl/src/main/res/values-pt-rBR/translations.xml b/libraries/eventformatter/impl/src/main/res/values-pt-rBR/translations.xml index 5148cc7638f..f1ff1bd029e 100644 --- a/libraries/eventformatter/impl/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-pt-rBR/translations.xml @@ -19,6 +19,8 @@ "Você removeu o avatar da sala" "%1$s baniu %2$s" "Você baniu %1$s" + "Você baniu %1$s: %2$s" + "%1$s banido %2$s: %3$s" "%1$s criou a sala" "Você criou a sala" "%1$s convidou %2$s" @@ -29,7 +31,7 @@ "%1$s entrou na sala" "Você entrou na sala" "%1$s solicitou entrada" - "%1$s permitiu que o %2$s entrar" + "%1$sconcedeu acesso a %2$s" "Você permitiu que o %1$s entrasse" "Você solicitou entrada" "%1$s rejeitou a solicitação de %2$s para entrar" @@ -45,10 +47,18 @@ "Você removeu o nome da sala" "%1$s não fez alterações" "Você não fez nenhuma alteração" + "%1$s alterou as mensagens fixadas" + "Você alterou as mensagens fixadas" + "%1$s fixou uma mensagem" + "Você fixou uma mensagem" + "%1$s desfixou uma mensagem" + "Você desafixou uma mensagem" "%1$s rejeitou o convite" "Você rejeitou o convite" "%1$s removido %2$s" "Você removeu %1$s" + "Você removeu %1$s: %2$s" + "%1$s removido %2$s: %3$s" "%1$s enviou um convite para %2$s para entrar na sala" "Você enviou um convite para %1$s para entrar na sala" "%1$s revogou o convite para %2$s para entrar na sala" diff --git a/libraries/eventformatter/impl/src/main/res/values-pt/translations.xml b/libraries/eventformatter/impl/src/main/res/values-pt/translations.xml index 6733deb6a20..94f59049d67 100644 --- a/libraries/eventformatter/impl/src/main/res/values-pt/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-pt/translations.xml @@ -19,6 +19,8 @@ "Removeste o ícone da sala" "%1$s baniu %2$s" "Baniste %1$s" + "Baniste %1$s: %2$s" + "%1$s baniu %2$s: %3$s" "%1$s criou a sala" "Criaste a sala" "%1$s convidou %2$s" @@ -55,6 +57,8 @@ "Rejeitaste o convite" "%1$s removeu %2$s" "Removeste %1$s" + "Removeste %1$s: %2$s" + "%1$s removeu %2$s: %3$s" "%1$s enviou um convite a %2$s" "Enviaste um convite a %1$s" "%1$s revogou o convite de %2$s" diff --git a/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml index b2a86a6962d..d0f4a39773a 100644 --- a/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml @@ -19,6 +19,8 @@ "Вы удалили изображение комнаты" "%1$s заблокировал %2$s" "Вы заблокировали %1$s" + "Вы заблокировали %1$s: %2$s" + "%1$s заблокирован %2$s: %3$s" "%1$s создал комнату" "Вы создали комнату" "%1$s пригласил %2$s" @@ -55,6 +57,8 @@ "Вы отклонили приглашение" "%1$s удалил %2$s" "Вы удалили %1$s" + "Вы удалили %1$s: %2$s" + "%1$s удален %2$s: %3$s" "%1$s отправила приглашение %2$s присоединиться к комнате" "Вы отправили приглашение присоединиться к комнате %1$s" "%1$s отозвал приглашение %2$s присоединиться к комнате" diff --git a/libraries/eventformatter/impl/src/main/res/values-sk/translations.xml b/libraries/eventformatter/impl/src/main/res/values-sk/translations.xml index 3eee3541b18..c13ce852866 100644 --- a/libraries/eventformatter/impl/src/main/res/values-sk/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-sk/translations.xml @@ -19,6 +19,8 @@ "Odstránili ste obrázok miestnosti" "%1$s zakázal/a používateľa %2$s" "Zakázali ste používateľa %1$s" + "Zakázali ste %1$s: %2$s" + "%1$s zakázal/a %2$s: %3$s" "%1$s vytvoril/a miestnosť" "Vytvorili ste miestnosť" "%1$s pozval/a používateľa %2$s" @@ -55,6 +57,8 @@ "Odmietli ste pozvánku" "%1$s odstránil/a %2$s" "Odstránili ste %1$s" + "Odstránili ste %1$s: %2$s" + "%1$s odstránil/a %2$s: %3$s" "%1$s poslal/a pozvánku používateľovi %2$s, aby sa pripojil k miestnosti" "Poslali ste pozvánku používateľovi %1$s, aby sa pripojil do miestnosti" "%1$s zrušil/a pozvánku pre používateľa %2$s na vstup do miestnosti" diff --git a/libraries/eventformatter/impl/src/main/res/values-sv/translations.xml b/libraries/eventformatter/impl/src/main/res/values-sv/translations.xml index 6b31a6f47d9..473c73cb6d4 100644 --- a/libraries/eventformatter/impl/src/main/res/values-sv/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-sv/translations.xml @@ -19,6 +19,8 @@ "Du tog bort rummets avatar" "%1$s bannade %2$s" "Du bannade %1$s" + "Du bannade %1$s: %2$s" + "%1$s bannade %2$s: %3$s" "%1$s skapade rummet" "Du skapade rummet" "%1$s bjöd in %2$s" @@ -55,6 +57,8 @@ "Du avvisade inbjudan" "%1$s tog bort %2$s" "Du tog bort %1$s" + "Du tog bort %1$s: %2$s" + "%1$s tog bort %2$s: %3$s" "%1$s skickade en inbjudan till %2$s att gå med i rummet" "Du skickade en inbjudan till %1$s att gå med i rummet" "%1$s återkallade inbjudan för %2$s att gå med i rummet" diff --git a/libraries/eventformatter/impl/src/main/res/values-tr/translations.xml b/libraries/eventformatter/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..507804892ea --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,69 @@ + + + "(Profil fotoğrafı değiştirildi)" + "%1$s profil fotoğrafını değiştirdi" + "Profil fotoğrafını değiştirdin" + "%1$s üyeliğe düşürüldü" + "%1$s moderatörlüğe düşürüldü" + "%1$s görünen adını bundan %2$s şuna %3$s değiştirdi" + "Görünen adınızı bundan %1$s şuna %2$s değiştirdiniz" + "%1$s görünen adını kaldırdı (önceden %2$s)" + "Görünen adınızı kaldırdınız (önceden %1$s)" + "%1$s görünen adlarını şuna ayarla %2$s" + "Görünen adınız %1$s" + "%1$s yöneticiliğe terfi etti" + "%1$s moderatörlüğe terfi etti" + "%1$s odanın fotoğrafını değiştirdi" + "Odanın fotoğrafını değiştirdiniz" + "%1$s oda fotoğrafını kaldırdı" + "Oda fotoğrafını kaldırdınız" + "%1$syasaklandı%2$s" + "%1$s yasakladınız" + "%1$sodayı yarattı" + "Odayı sen yarattın" + "%1$s davet edildi %2$s" + "%1$s daveti kabul etti" + "Daveti kabul ettiniz" + "%1$s davet ettiniz" + "%1$s sizi davet etti" + "%1$sodaya katıldı" + "Odaya katıldınız" + "%1$s katılmak istiyor" + "%1$s, %2$s\'e erişim izni verdi" + "%1$s \'ın katılmasına izin verdiniz" + "Katılmayı talep ettiniz" + "%1$s, %2$s\'ın katılma isteğini reddetti" + "%1$s kullanıcısının katılma isteğini reddettiniz" + "%1$s katılma isteğinizi reddetti" + "%1$s artık katılmakla ilgilenmiyor" + "Katılma talebinizi iptal ettiniz" + "%1$sodadan ayrıldı" + "Odadan ayrıldın." + "%1$s Oda adını değiştirdi: %2$s" + "Odanın adını değiştridiniz: %1$s" + "%1$s Oda adını kaldırdı" + "Oda adını kaldırdınız" + "%1$s hiçbir değişiklik yapmadı" + "Hiçbir değişiklik yapmadınız" + "%1$s sabitlenmiş iletileri değiştirdi" + "Sabitlenmiş mesajları değiştirdiniz" + "%1$s bir mesaj sabitledi" + "Bir mesaj sabitlediniz" + "%1$s bir mesajın sabitlemesini kaldırdı" + "Bir mesajın sabitlemesini kaldırdınız" + "%1$sdaveti reddetti" + "Daveti reddettiniz" + "%1$skaldırıldı%2$s" + "%1$s kaldırdınız" + "%1$s odaya katılması için %2$s\'a davet gönderdi" + "Odaya katılması için %1$s\'a davet gönderdin" + "%1$s, %2$s\'nin odaya katılma davetini iptal etti" + "%1$s\'ın odaya katılma davetini iptal ettiniz" + "%1$s konuyu değiştirdi: %2$s" + "Konuyu değiştirdiniz: %1$s" + "%1$s oda konusunu kaldırdı" + "Oda konusunu kaldırdınız" + "%1$syasaklanmamış%2$s" + "%1$s yasağını kaldırdınız" + "%1$s üyeliğinde bilinmeyen bir değişiklik yaptı" + diff --git a/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml b/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml index 7e96bc81f57..82c41a55b26 100644 --- a/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml @@ -19,6 +19,8 @@ "Ви видалили аватар кімнати" "%1$s блокує %2$s" "Ви заблокували %1$s" + "Ви заблокували %1$s: %2$s" + "%1$s блокує %2$s: %3$s" "%1$s створює кімнату" "Ви створили кімнату" "%1$s запрошує %2$s" @@ -55,6 +57,8 @@ "Ви відхилили запрошення" "%1$s вилучає %2$s" "Ви видалили %1$s" + "Ви вилучили %1$s: %2$s" + "%1$s вилучає %2$s: %3$s" "%1$s запрошує %2$s приєднатися до кімнати" "Ви запросили %1$s приєднатися до кімнати" "%1$s відкликає запрошення приєднатися до кімнати для %2$s" diff --git a/libraries/eventformatter/impl/src/main/res/values-ur/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..6dba74fd2f1 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,69 @@ + + + "(اوتار بھی تبدیل ہوا)" + "%1$s نے اپنا اوتار بدل دیا" + "آپنے اپنا اوتار بدل دیا" + "%1$s کو تا رکن تنزل کیا گیا" + "%1$s کو تا ناظم تنزل کیا گیا" + "%1$s نے اپنا نمائشی نام %2$s سے %3$s پر بدل دیا" + "آپنے اپنا نمائشی نام %1$s سے %2$s پر بدل دیا" + "%1$s نے اپنا نمائشی نام ہٹادیا (%2$s تھا)" + "آپنے اپنا نمائشی نام ہٹادیا (%1$s تھا)" + "%1$s نے اپنا نمائشی نام %2$s پر متعین کردیا" + "آپنے اپنا نمائشی نام %1$s پر متعین کردیا" + "%1$s کو تا منتظم فروغ دیا گیا" + "%1$s کو تا ناظم فروغ دیا گیا" + "%1$s نے کمرے کا اوتار بدل دیا" + "آپنے کمرے کا اوتار بدل دیا" + "%1$s نے کمرے کا اوتار ہٹادیا" + "آپنے کمرے کا اوتار ہٹادیا" + "%1$s نے %2$s پر پابندی لگادی" + "آپنے %1$s پر پابندی لگادی" + "%1$s نے کمرہ تخلیق کیا" + "آپ نے کمرہ تخلیق کیا" + "%1$s نے %2$s کو مدعو کیا" + "%1$s نے دعوت قبول کرلی" + "آپنے دعوت قبول کرلی" + "آپنے %1$s مدعو کیا" + "%1$s نے آپکو مدعو کیا" + "%1$s کمرے میں شامل ہوگیا" + "آپ کمرے میں شامل ہو گئے" + "%1$s شامل ہونے کی درخواست کر رہا ہے" + "%1$s نے %2$s کو اجازت فراہم کی" + "آپنے %1$s کو شامل ہونے کی اجازت دی" + "آپنے شامل ہونے کی دعوت کی" + "%1$s نے %2$s کی شامل ہونے کی درخواست مسترد کردی" + "آپنے %1$s کی شامل ہونے کی درخواست مسترد کردی" + "%1$s نے آپکی شامل ہونے کی درخواست مسترد کردی" + "%1$s کی شامل ہونے میں دلچسپی نہیں" + "آپنے اپنی شامل ہونے کی درخواست منسوخ کردی" + "%1$s کمرے سے رخصت ہوگیا" + "آپ کمرے سے رخصت ہوگئے" + "%1$s نے کمرے کا نام بدل دیا تا: %2$s" + "آپنے کمرے کا نام بدل دیا تا: %1$s" + "%1$s نے کمرے کا نام ہٹادیا" + "آپنے کمرے کا نام ہٹادیا" + "%1$s نے کوئی تبدیلیاں نہیں کیں" + "آپ نے کوئی تبدیلیاں نہیں کیں" + "%1$s نے مثبوتہ پیغامات بدل دیے" + "آپنے مثبوتہ پیغامات بدل دیے" + "%1$s نے پیغام تثبیت کردیا" + "آپنے پیغام تثبیت کردیا" + "%1$s نے پیغام غیر مثبوت کردیا" + "آپنے پیغام غیر مثبوت کردیا" + "%1$s نے دعوت مسترد کر دی" + "آپنے دعوت مسترد کر دی" + "%1$s نے %2$s کو ہٹادیا" + "آپنے %1$s کو ہٹادیا" + "%1$s نے %2$s کو کمرے میں شامل ہونی کی دعوت بھیج دی" + "آپنے %1$s کو کمرے میں شامل ہونے کی دعوت بھیج دی" + "%1$s نے %2$s کی کمرے میں شامل ہونے کی دعوت منسوخ کردی" + "آپنے %1$s کے کمرے میں شامل ہونے کی دعوت منسوخ کردی" + "%1$s نے موضوع کو بدلا تا: %2$s" + "آپنے موضوع کو بدلا تا: %1$s" + "%1$s نے کمرے کا موضوع ہٹادیا" + "آپنے کمرے کا موضوع ہٹادیا" + "%1$s کے %2$s کو غیر پابندی یافتہ کردیا" + "آپنے %1$s کو غیر پابندی یافتہ کردیا" + "%1$s نے اپنی رکنیت میں ایک نامعلوم تبدیلی کی" + diff --git a/libraries/eventformatter/impl/src/main/res/values-uz/translations.xml b/libraries/eventformatter/impl/src/main/res/values-uz/translations.xml index 91ac1922058..aab758a1a1d 100644 --- a/libraries/eventformatter/impl/src/main/res/values-uz/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-uz/translations.xml @@ -24,7 +24,7 @@ "%1$ssizni taklif qildi" "%1$sxonaga qo\'shildi" "Siz xonaga qo\'shildingiz" - "%1$s qo\'shilishni so\'radi" + "%1$s qoʻshilishni soʻradi" "%1$s %2$sga qo\'shilishga ruxsat berdi" "Siz %1$sga qo\'shilishaga ruxsat berdingiz" "Siz qoʻshilishni soʻragansiz" diff --git a/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml index 11d891c215b..cd8137744fb 100644 --- a/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml @@ -19,6 +19,8 @@ "您移除了聊天室大頭照" "%1$s 將 %2$s 加入黑名單" "您將 %1$s 加入黑名單" + "您封鎖了 %1$s:%2$s" + "%1$s 封鎖了 %2$s:%3$s" "%1$s 建立此聊天室" "您建立此聊天室" "%1$s 已邀請 %2$s" @@ -55,6 +57,8 @@ "您拒絕了邀請" "%2$s 已被 %1$s 移除" "%1$s 已被您移除" + "您移除了 %1$s:%2$s" + "%1$s 移除了 %2$s:%3$s" "%1$s 已邀請 %2$s 加入聊天室" "您已邀請 %1$s 加入聊天室" "%1$s 撤銷了對 %2$s 的聊天室邀請" diff --git a/libraries/eventformatter/impl/src/main/res/values/localazy.xml b/libraries/eventformatter/impl/src/main/res/values/localazy.xml index 2579c98207a..dfa8a3b40bc 100644 --- a/libraries/eventformatter/impl/src/main/res/values/localazy.xml +++ b/libraries/eventformatter/impl/src/main/res/values/localazy.xml @@ -19,6 +19,8 @@ "You removed the room avatar" "%1$s banned %2$s" "You banned %1$s" + "You banned %1$s: %2$s" + "%1$s banned %2$s: %3$s" "%1$s created the room" "You created the room" "%1$s invited %2$s" @@ -55,6 +57,8 @@ "You rejected the invitation" "%1$s removed %2$s" "You removed %1$s" + "You removed %1$s: %2$s" + "%1$s removed %2$s: %3$s" "%1$s sent an invitation to %2$s to join the room" "You sent an invitation to %1$s to join the room" "%1$s revoked the invitation for %2$s to join the room" diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultBaseRoomLastMessageFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultBaseRoomLastMessageFormatterTest.kt new file mode 100644 index 00000000000..6d0ad1aa49a --- /dev/null +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultBaseRoomLastMessageFormatterTest.kt @@ -0,0 +1,943 @@ +/* + * 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. + */ + +package io.element.android.libraries.eventformatter.impl + +import android.content.Context +import androidx.compose.ui.text.AnnotatedString +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +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.timeline.item.event.AudioMessageType +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.EventTimelineItem +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 +import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageType +import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherState +import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent +import io.element.android.libraries.matrix.api.timeline.item.event.StateContent +import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType +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.VideoMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType +import io.element.android.libraries.matrix.test.A_REASON +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.media.aMediaSource +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser +import io.element.android.libraries.matrix.test.timeline.aPollContent +import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent +import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails +import io.element.android.libraries.matrix.test.timeline.aStickerContent +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent +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 + +@Suppress("LargeClass") +@RunWith(RobolectricTestRunner::class) +class DefaultBaseRoomLastMessageFormatterTest { + private lateinit var context: Context + private lateinit var fakeMatrixClient: FakeMatrixClient + private lateinit var formatter: DefaultRoomLastMessageFormatter + + @Before + fun setup() { + context = RuntimeEnvironment.getApplication() as Context + fakeMatrixClient = FakeMatrixClient() + val stringProvider = AndroidStringProvider(context.resources) + formatter = DefaultRoomLastMessageFormatter( + sp = AndroidStringProvider(context.resources), + roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider), + profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), + stateContentFormatter = StateContentFormatter(stringProvider), + permalinkParser = FakePermalinkParser(), + ) + } + + @Test + @Config(qualifiers = "en") + fun `Redacted content`() { + val expected = "Message removed" + val senderName = "Someone" + sequenceOf(false, true).forEach { isDm -> + val message = createRoomEvent(false, senderName, RedactedContent) + val result = formatter.format(message, isDm) + if (isDm) { + assertThat(result).isEqualTo(expected) + } else { + assertThat(result).isInstanceOf(AnnotatedString::class.java) + assertThat(result.toString()).isEqualTo("$senderName: $expected") + } + } + } + + @Test + @Config(qualifiers = "en") + fun `Sticker content`() { + val body = "a sticker body" + val info = ImageInfo(null, null, null, null, null, null, null) + val message = createRoomEvent(false, null, aStickerContent(body, info, aMediaSource(url = "url"))) + val result = formatter.format(message, false) + val expectedBody = someoneElseId.toString() + ": Sticker (a sticker body)" + assertThat(result.toString()).isEqualTo(expectedBody) + } + + @Test + @Config(qualifiers = "en") + fun `Unable to decrypt content`() { + val expected = "Waiting for this message" + val senderName = "Someone" + sequenceOf(false, true).forEach { isDm -> + val message = createRoomEvent(false, senderName, UnableToDecryptContent(UnableToDecryptContent.Data.Unknown)) + val result = formatter.format(message, isDm) + if (isDm) { + assertThat(result).isEqualTo(expected) + } else { + assertThat(result).isInstanceOf(AnnotatedString::class.java) + assertThat(result.toString()).isEqualTo("$senderName: $expected") + } + } + } + + @Test + @Config(qualifiers = "en") + fun `FailedToParseMessageLike, FailedToParseState & Unknown content`() { + val expected = "Unsupported event" + val senderName = "Someone" + sequenceOf(false, true).forEach { isDm -> + sequenceOf( + FailedToParseMessageLikeContent("", ""), + FailedToParseStateContent("", "", ""), + UnknownContent, + ).forEach { type -> + val message = createRoomEvent(false, senderName, type) + val result = formatter.format(message, isDm) + if (isDm) { + assertWithMessage("$type was not properly handled").that(result).isEqualTo(expected) + } else { + assertWithMessage("$type does not create an AnnotatedString").that(result).isInstanceOf(AnnotatedString::class.java) + assertWithMessage("$type was not properly handled").that(result.toString()).isEqualTo("$senderName: $expected") + } + } + } + } + + // region Message contents + + @Test + @Config(qualifiers = "en") + fun `Message contents sent by other user`() { + testMessageContents( + sentByYou = false, + senderName = "Alice", + expectedPrefix = "Alice", + ) + } + + @Test + @Config(qualifiers = "en") + fun `Message contents sent by current user`() { + testMessageContents( + sentByYou = true, + senderName = "Bob", + expectedPrefix = "You", + ) + } + + private fun testMessageContents( + sentByYou: Boolean, + senderName: String, + expectedPrefix: String, + ) { + val body = "Shared body" + fun createMessageContent(type: MessageType): MessageContent { + return MessageContent(body, null, false, false, type) + } + + val sharedContentMessagesTypes = arrayOf( + TextMessageType(body, null), + VideoMessageType(body, null, null, MediaSource("url"), null), + AudioMessageType(body, null, null, MediaSource("url"), null), + VoiceMessageType(body, null, null, MediaSource("url"), null, null), + ImageMessageType(body, null, null, MediaSource("url"), null), + StickerMessageType(body, null, null, MediaSource("url"), null), + FileMessageType(body, null, null, MediaSource("url"), null), + LocationMessageType(body, "geo:1,2", null), + NoticeMessageType(body, null), + EmoteMessageType(body, null), + OtherMessageType(msgType = "a_type", body = body), + ) + val resultsInRoom = mutableListOf>() + val resultsInDm = mutableListOf>() + + // Create messages for all types in DM and Room mode + sequenceOf(false, true).forEach { isDm -> + sharedContentMessagesTypes.forEach { type -> + val content = createMessageContent(type) + val message = createRoomEvent(sentByYou = sentByYou, senderDisplayName = senderName, content = content) + val result = formatter.format(message, isDmRoom = isDm) + if (isDm) { + resultsInDm.add(type to result) + } else { + resultsInRoom.add(type to result) + } + } + } + + // Verify results of DM mode + for ((type, result) in resultsInDm) { + val string = result.toString() + val expectedResult = when (type) { + is VideoMessageType -> "Video: Shared body" + is AudioMessageType -> "Audio: Shared body" + is VoiceMessageType -> "Voice message" + is ImageMessageType -> "Image: Shared body" + is StickerMessageType -> "Sticker: Shared body" + is FileMessageType -> "File: Shared body" + is LocationMessageType -> "Shared location" + is EmoteMessageType -> "* $senderName ${type.body}" + is TextMessageType, + is NoticeMessageType, + is OtherMessageType -> body + } + val shouldCreateAnnotatedString = when (type) { + is VideoMessageType -> true + is AudioMessageType -> true + is VoiceMessageType -> false + is ImageMessageType -> true + is StickerMessageType -> true + is FileMessageType -> true + is LocationMessageType -> false + is EmoteMessageType -> false + is TextMessageType -> false + is NoticeMessageType -> false + is OtherMessageType -> false + } + if (shouldCreateAnnotatedString) { + assertWithMessage("$type doesn't produce an AnnotatedString") + .that(result) + .isInstanceOf(AnnotatedString::class.java) + } + assertWithMessage("$type was not properly handled for DM").that(string).isEqualTo(expectedResult) + } + + // Verify results of Room mode + for ((type, result) in resultsInRoom) { + val string = result.toString() + val expectedResult = when (type) { + is VideoMessageType -> "$expectedPrefix: Video: Shared body" + is AudioMessageType -> "$expectedPrefix: Audio: Shared body" + is VoiceMessageType -> "$expectedPrefix: Voice message" + is ImageMessageType -> "$expectedPrefix: Image: Shared body" + is StickerMessageType -> "$expectedPrefix: Sticker: Shared body" + is FileMessageType -> "$expectedPrefix: File: Shared body" + is LocationMessageType -> "$expectedPrefix: Shared location" + is TextMessageType, + is NoticeMessageType, + is OtherMessageType -> "$expectedPrefix: $body" + is EmoteMessageType -> "* $senderName ${type.body}" + } + val shouldCreateAnnotatedString = when (type) { + is VideoMessageType -> true + is AudioMessageType -> true + is VoiceMessageType -> true + is ImageMessageType -> true + is StickerMessageType -> true + is FileMessageType -> true + is LocationMessageType -> false + is EmoteMessageType -> false + is TextMessageType -> true + is NoticeMessageType -> true + is OtherMessageType -> true + } + if (shouldCreateAnnotatedString) { + assertWithMessage("$type doesn't produce an AnnotatedString") + .that(result) + .isInstanceOf(AnnotatedString::class.java) + } + assertWithMessage("$type was not properly handled for room").that(string).isEqualTo(expectedResult) + } + } + + // endregion + + // region Membership change + + @Test + @Config(qualifiers = "en") + fun `Membership change - joined`() { + val otherName = "Other" + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.JOINED) + + val youJoinedRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youJoinedRoom = formatter.format(youJoinedRoomEvent, false) + assertThat(youJoinedRoom).isEqualTo("You joined the room") + + val someoneJoinedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneJoinedRoom = formatter.format(someoneJoinedRoomEvent, false) + assertThat(someoneJoinedRoom).isEqualTo("$otherName joined the room") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - left`() { + val otherName = "Other" + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.LEFT) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.LEFT) + + val youLeftRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youLeftRoom = formatter.format(youLeftRoomEvent, false) + assertThat(youLeftRoom).isEqualTo("You left the room") + + val someoneLeftRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneLeftRoom = formatter.format(someoneLeftRoomEvent, false) + assertThat(someoneLeftRoom).isEqualTo("$otherName left the room") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - banned`() { + val otherName = "Other" + val third = "Someone" + val youContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED) + val youKickedContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED) + val someoneKickedContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED) + + val youBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youBanned = formatter.format(youBannedEvent, false) + assertThat(youBanned).isEqualTo("You banned $third") + + val youKickBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youKickedContent) + val youKickedBanned = formatter.format(youKickBannedEvent, false) + assertThat(youKickedBanned).isEqualTo("You banned $third") + + val someoneBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneBanned = formatter.format(someoneBannedEvent, false) + assertThat(someoneBanned).isEqualTo("$otherName banned $third") + + val someoneKickBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneKickedContent) + val someoneKickBanned = formatter.format(someoneKickBannedEvent, false) + assertThat(someoneKickBanned).isEqualTo("$otherName banned $third") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - banned with reason`() { + val otherName = "Other" + val third = "Someone" + val youContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED, A_REASON) + val youKickedContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED, A_REASON) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED, A_REASON) + val someoneKickedContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED, A_REASON) + + val youBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youBanned = formatter.format(youBannedEvent, false) + assertThat(youBanned).isEqualTo("You banned $third: $A_REASON") + + val youKickBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youKickedContent) + val youKickedBanned = formatter.format(youKickBannedEvent, false) + assertThat(youKickedBanned).isEqualTo("You banned $third: $A_REASON") + + val someoneBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneBanned = formatter.format(someoneBannedEvent, false) + assertThat(someoneBanned).isEqualTo("$otherName banned $third: $A_REASON") + + val someoneKickBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneKickedContent) + val someoneKickBanned = formatter.format(someoneKickBannedEvent, false) + assertThat(someoneKickBanned).isEqualTo("$otherName banned $third: $A_REASON") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - unban`() { + val otherName = "Other" + val third = "Someone" + val youContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED) + + val youUnbannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youUnbanned = formatter.format(youUnbannedEvent, false) + assertThat(youUnbanned).isEqualTo("You unbanned $third") + + val someoneUnbannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneUnbanned = formatter.format(someoneUnbannedEvent, false) + assertThat(someoneUnbanned).isEqualTo("$otherName unbanned $third") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - kicked`() { + val otherName = "Other" + val third = "Someone" + val youContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED) + + val youKickedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youKicked = formatter.format(youKickedEvent, false) + assertThat(youKicked).isEqualTo("You removed $third") + + val someoneKickedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneKicked = formatter.format(someoneKickedEvent, false) + assertThat(someoneKicked).isEqualTo("$otherName removed $third") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - kicked with reason`() { + val otherName = "Other" + val third = "Someone" + val youContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED, A_REASON) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED, A_REASON) + + val youKickedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youKicked = formatter.format(youKickedEvent, false) + assertThat(youKicked).isEqualTo("You removed $third: $A_REASON") + + val someoneKickedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneKicked = formatter.format(someoneKickedEvent, false) + assertThat(someoneKicked).isEqualTo("$otherName removed $third: $A_REASON") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - invited`() { + val otherName = "Other" + val third = "Someone" + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.INVITED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITED) + + val youWereInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent) + val youWereInvited = formatter.format(youWereInvitedEvent, false) + assertThat(youWereInvited).isEqualTo("$otherName invited you") + + val youInvitedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) + val youInvited = formatter.format(youInvitedEvent, false) + assertThat(youInvited).isEqualTo("You invited $third") + + val someoneInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneInvited = formatter.format(someoneInvitedEvent, false) + assertThat(someoneInvited).isEqualTo("$otherName invited $third") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - invitation accepted`() { + val otherName = "Other" + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_ACCEPTED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_ACCEPTED) + + val youAcceptedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youAcceptedInvite = formatter.format(youAcceptedInviteEvent, false) + assertThat(youAcceptedInvite).isEqualTo("You accepted the invite") + + val someoneAcceptedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneAcceptedInvite = formatter.format(someoneAcceptedInviteEvent, false) + assertThat(someoneAcceptedInvite).isEqualTo("$otherName accepted the invite") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - invitation rejected`() { + val otherName = "Other" + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_REJECTED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_REJECTED) + + val youRejectedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youRejectedInvite = formatter.format(youRejectedInviteEvent, false) + assertThat(youRejectedInvite).isEqualTo("You rejected the invitation") + + val someoneRejectedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneRejectedInvite = formatter.format(someoneRejectedInviteEvent, false) + assertThat(someoneRejectedInvite).isEqualTo("$otherName rejected the invitation") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - invitation revoked`() { + val otherName = "Other" + val third = "Someone" + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITATION_REVOKED) + + val youRevokedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) + val youRevokedInvite = formatter.format(youRevokedInviteEvent, false) + assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for $third to join the room") + + val someoneRevokedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneRevokedInvite = formatter.format(someoneRevokedInviteEvent, false) + assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for $third to join the room") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - knocked`() { + val otherName = "Other" + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCKED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.KNOCKED) + + val youKnockedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youKnocked = formatter.format(youKnockedEvent, false) + assertThat(youKnocked).isEqualTo("You requested to join") + + val someoneKnockedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneKnocked = formatter.format(someoneKnockedEvent, false) + assertThat(someoneKnocked).isEqualTo("$otherName is requesting to join") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - knock accepted`() { + val otherName = "Other" + val third = "Someone" + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_ACCEPTED) + + val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) + val youAcceptedKnock = formatter.format(youAcceptedKnockEvent, false) + assertThat(youAcceptedKnock).isEqualTo("You allowed $third to join") + + val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false) + assertThat(someoneAcceptedKnock).isEqualTo("$otherName granted access to $third") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - knock retracted`() { + val otherName = "Other" + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCK_RETRACTED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), null, MembershipChange.KNOCK_RETRACTED) + + val youRetractedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youRetractedKnock = formatter.format(youRetractedKnockEvent, false) + assertThat(youRetractedKnock).isEqualTo("You cancelled your request to join") + + val someoneRetractedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneRetractedKnock = formatter.format(someoneRetractedKnockEvent, false) + assertThat(someoneRetractedKnock).isEqualTo("$otherName is no longer interested in joining") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - knock denied`() { + val otherName = "Other" + val third = "Someone" + val youContent = aRoomMembershipContent(A_USER_ID, third, MembershipChange.KNOCK_DENIED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_DENIED) + + val youDeniedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) + val youDeniedKnock = formatter.format(youDeniedKnockEvent, false) + assertThat(youDeniedKnock).isEqualTo("You rejected $third's request to join") + + val someoneDeniedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneDeniedKnock = formatter.format(someoneDeniedKnockEvent, false) + assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected $third's request to join") + + val someoneDeniedYourKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent) + val someoneDeniedYourKnock = formatter.format(someoneDeniedYourKnockEvent, false) + assertThat(someoneDeniedYourKnock).isEqualTo("$otherName rejected your request to join") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - None`() { + val otherName = "Other" + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.NONE) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.NONE) + + val youNoneRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) + val youNoneRoom = formatter.format(youNoneRoomEvent, false) + assertThat(youNoneRoom).isEqualTo("You made no changes") + + val someoneNoneRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) + val someoneNoneRoom = formatter.format(someoneNoneRoomEvent, false) + assertThat(someoneNoneRoom).isEqualTo("$otherName made no changes") + } + + @Test + @Config(qualifiers = "en") + fun `Membership change - others`() { + val otherChanges = arrayOf(MembershipChange.ERROR, MembershipChange.NOT_IMPLEMENTED, null) + + val results = otherChanges.map { change -> + val content = aRoomMembershipContent(A_USER_ID, null, change) + val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content) + val result = formatter.format(event, false) + change to result + } + val expected = otherChanges.map { it to null } + assertThat(results).isEqualTo(expected) + } + + // endregion + + // region Room State + + @Test + @Config(qualifiers = "en") + fun `Room state change - avatar`() { + val otherName = "Other" + val changedContent = StateContent("", OtherState.RoomAvatar("new_avatar")) + val removedContent = StateContent("", OtherState.RoomAvatar(null)) + + val youChangedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) + val youChangedRoomAvatar = formatter.format(youChangedRoomAvatarEvent, false) + assertThat(youChangedRoomAvatar).isEqualTo("You changed the room avatar") + + val someoneChangedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) + val someoneChangedRoomAvatar = formatter.format(someoneChangedRoomAvatarEvent, false) + assertThat(someoneChangedRoomAvatar).isEqualTo("$otherName changed the room avatar") + + val youRemovedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) + val youRemovedRoomAvatar = formatter.format(youRemovedRoomAvatarEvent, false) + assertThat(youRemovedRoomAvatar).isEqualTo("You removed the room avatar") + + val someoneRemovedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) + val someoneRemovedRoomAvatar = formatter.format(someoneRemovedRoomAvatarEvent, false) + assertThat(someoneRemovedRoomAvatar).isEqualTo("$otherName removed the room avatar") + } + + @Test + @Config(qualifiers = "en") + fun `Room state change - create`() { + val otherName = "Other" + val content = StateContent("", OtherState.RoomCreate) + + val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content) + val youCreatedRoom = formatter.format(youCreatedRoomMessage, false) + assertThat(youCreatedRoom).isEqualTo("You created the room") + + val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content) + val someoneCreatedRoom = formatter.format(someoneCreatedRoomEvent, false) + assertThat(someoneCreatedRoom).isEqualTo("$otherName created the room") + } + + @Test + @Config(qualifiers = "en") + fun `Room state change - encryption`() { + val otherName = "Other" + val content = StateContent("", OtherState.RoomEncryption) + + val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content) + val youCreatedRoom = formatter.format(youCreatedRoomMessage, false) + assertThat(youCreatedRoom).isEqualTo("Encryption enabled") + + val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content) + val someoneCreatedRoom = formatter.format(someoneCreatedRoomEvent, false) + assertThat(someoneCreatedRoom).isEqualTo("Encryption enabled") + } + + @Test + @Config(qualifiers = "en") + fun `Room state change - room name`() { + val otherName = "Other" + val newName = "New name" + val changedContent = StateContent("", OtherState.RoomName(newName)) + val removedContent = StateContent("", OtherState.RoomName(null)) + + val youChangedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) + val youChangedRoomName = formatter.format(youChangedRoomNameEvent, false) + assertThat(youChangedRoomName).isEqualTo("You changed the room name to: $newName") + + val someoneChangedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) + val someoneChangedRoomName = formatter.format(someoneChangedRoomNameEvent, false) + assertThat(someoneChangedRoomName).isEqualTo("$otherName changed the room name to: $newName") + + val youRemovedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) + val youRemovedRoomName = formatter.format(youRemovedRoomNameEvent, false) + assertThat(youRemovedRoomName).isEqualTo("You removed the room name") + + val someoneRemovedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) + val someoneRemovedRoomName = formatter.format(someoneRemovedRoomNameEvent, false) + assertThat(someoneRemovedRoomName).isEqualTo("$otherName removed the room name") + } + + @Test + @Config(qualifiers = "en") + fun `Room state change - third party invite`() { + val otherName = "Other" + val inviteeName = "Alice" + val changedContent = StateContent("", OtherState.RoomThirdPartyInvite(inviteeName)) + val removedContent = StateContent("", OtherState.RoomThirdPartyInvite(null)) + + val youInvitedSomeoneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) + val youInvitedSomeone = formatter.format(youInvitedSomeoneEvent, false) + assertThat(youInvitedSomeone).isEqualTo("You sent an invitation to $inviteeName to join the room") + + val someoneInvitedSomeoneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) + val someoneInvitedSomeone = formatter.format(someoneInvitedSomeoneEvent, false) + assertThat(someoneInvitedSomeone).isEqualTo("$otherName sent an invitation to $inviteeName to join the room") + + val youInvitedNoOneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) + val youInvitedNoOne = formatter.format(youInvitedNoOneEvent, false) + assertThat(youInvitedNoOne).isNull() + + val someoneInvitedNoOneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) + val someoneInvitedNoOne = formatter.format(someoneInvitedNoOneEvent, false) + assertThat(someoneInvitedNoOne).isNull() + } + + @Test + @Config(qualifiers = "en") + fun `Room state change - room topic`() { + val otherName = "Other" + val roomTopic = "New topic" + val changedContent = StateContent("", OtherState.RoomTopic(roomTopic)) + val removedContent = StateContent("", OtherState.RoomTopic(null)) + val blankContent = StateContent("", OtherState.RoomTopic("")) + + val youChangedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) + val youChangedRoomTopic = formatter.format(youChangedRoomTopicEvent, false) + assertThat(youChangedRoomTopic).isEqualTo("You changed the topic to: $roomTopic") + + val someoneChangedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) + val someoneChangedRoomTopic = formatter.format(someoneChangedRoomTopicEvent, false) + assertThat(someoneChangedRoomTopic).isEqualTo("$otherName changed the topic to: $roomTopic") + + val youRemovedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) + val youRemovedRoomTopic = formatter.format(youRemovedRoomTopicEvent, false) + assertThat(youRemovedRoomTopic).isEqualTo("You removed the room topic") + + val someoneRemovedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) + val someoneRemovedRoomTopic = formatter.format(someoneRemovedRoomTopicEvent, false) + assertThat(someoneRemovedRoomTopic).isEqualTo("$otherName removed the room topic") + + val youSetBlankRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = blankContent) + val youSetBlankRoomTopic = formatter.format(youSetBlankRoomTopicEvent, false) + assertThat(youSetBlankRoomTopic).isEqualTo("You removed the room topic") + + val someoneSetBlankRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = blankContent) + val someoneSetBlankRoomTopic = formatter.format(someoneSetBlankRoomTopicEvent, false) + assertThat(someoneSetBlankRoomTopic).isEqualTo("$otherName removed the room topic") + } + + @Test + @Config(qualifiers = "en") + fun `Room state change - others must return null`() { + val otherStates = arrayOf( + OtherState.PolicyRuleRoom, + OtherState.PolicyRuleServer, + OtherState.PolicyRuleUser, + OtherState.RoomAliases, + OtherState.RoomCanonicalAlias, + OtherState.RoomGuestAccess, + OtherState.RoomHistoryVisibility, + OtherState.RoomJoinRules, + OtherState.RoomPinnedEvents(OtherState.RoomPinnedEvents.Change.CHANGED), + OtherState.RoomUserPowerLevels(emptyMap()), + OtherState.RoomServerAcl, + OtherState.RoomTombstone, + OtherState.SpaceChild, + OtherState.SpaceParent, + OtherState.Custom("custom_event_type") + ) + + val results = otherStates.map { state -> + val content = StateContent("", state) + val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content) + val result = formatter.format(event, false) + state to result + } + val expected = otherStates.map { it to null } + assertThat(results).isEqualTo(expected) + } + + // endregion + + // region Profile change + + @Test + @Config(qualifiers = "en") + fun `Profile change - avatar`() { + val otherName = "Other" + val changedContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = "old_avatar_url") + val setContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = null) + val removedContent = aProfileChangeMessageContent(avatarUrl = null, prevAvatarUrl = "old_avatar_url") + val invalidContent = aProfileChangeMessageContent(avatarUrl = null, prevAvatarUrl = null) + val sameContent = aProfileChangeMessageContent(avatarUrl = "same_avatar_url", prevAvatarUrl = "same_avatar_url") + + val youChangedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) + val youChangedAvatar = formatter.format(youChangedAvatarEvent, false) + assertThat(youChangedAvatar).isEqualTo("You changed your avatar") + + val someoneChangeAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) + val someoneChangeAvatar = formatter.format(someoneChangeAvatarEvent, false) + assertThat(someoneChangeAvatar).isEqualTo("$otherName changed their avatar") + + val youSetAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent) + val youSetAvatar = formatter.format(youSetAvatarEvent, false) + assertThat(youSetAvatar).isEqualTo("You changed your avatar") + + val someoneSetAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent) + val someoneSetAvatar = formatter.format(someoneSetAvatarEvent, false) + assertThat(someoneSetAvatar).isEqualTo("$otherName changed their avatar") + + val youRemovedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) + val youRemovedAvatar = formatter.format(youRemovedAvatarEvent, false) + assertThat(youRemovedAvatar).isEqualTo("You changed your avatar") + + val someoneRemovedAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) + val someoneRemovedAvatar = formatter.format(someoneRemovedAvatarEvent, false) + assertThat(someoneRemovedAvatar).isEqualTo("$otherName changed their avatar") + + val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent) + val unchangedResult = formatter.format(unchangedEvent, false) + assertThat(unchangedResult).isNull() + + val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent) + val invalidResult = formatter.format(invalidEvent, false) + assertThat(invalidResult).isNull() + } + + @Test + @Config(qualifiers = "en") + fun `Profile change - display name`() { + val newDisplayName = "New" + val oldDisplayName = "Old" + val otherName = "Other" + val changedContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = oldDisplayName) + val setContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = null) + val removedContent = aProfileChangeMessageContent(displayName = null, prevDisplayName = oldDisplayName) + val sameContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = newDisplayName) + val invalidContent = aProfileChangeMessageContent(displayName = null, prevDisplayName = null) + + val youChangedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) + val youChangedDisplayName = formatter.format(youChangedDisplayNameEvent, false) + assertThat(youChangedDisplayName).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName") + + val someoneChangedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) + val someoneChangedDisplayName = formatter.format(someoneChangedDisplayNameEvent, false) + assertThat(someoneChangedDisplayName).isEqualTo("$someoneElseId changed their display name from $oldDisplayName to $newDisplayName") + + val youSetDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent) + val youSetDisplayName = formatter.format(youSetDisplayNameEvent, false) + assertThat(youSetDisplayName).isEqualTo("You set your display name to $newDisplayName") + + val someoneSetDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent) + val someoneSetDisplayName = formatter.format(someoneSetDisplayNameEvent, false) + assertThat(someoneSetDisplayName).isEqualTo("$someoneElseId set their display name to $newDisplayName") + + val youRemovedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) + val youRemovedDisplayName = formatter.format(youRemovedDisplayNameEvent, false) + assertThat(youRemovedDisplayName).isEqualTo("You removed your display name (it was $oldDisplayName)") + + val someoneRemovedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) + val someoneRemovedDisplayName = formatter.format(someoneRemovedDisplayNameEvent, false) + assertThat(someoneRemovedDisplayName).isEqualTo("$someoneElseId removed their display name (it was $oldDisplayName)") + + val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent) + val unchangedResult = formatter.format(unchangedEvent, false) + assertThat(unchangedResult).isNull() + + val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent) + val invalidResult = formatter.format(invalidEvent, false) + assertThat(invalidResult).isNull() + } + + @Test + @Config(qualifiers = "en") + fun `Profile change - display name & avatar`() { + val newDisplayName = "New" + val oldDisplayName = "Old" + val changedContent = aProfileChangeMessageContent( + displayName = newDisplayName, + prevDisplayName = oldDisplayName, + avatarUrl = "new_avatar_url", + prevAvatarUrl = "old_avatar_url", + ) + val invalidContent = aProfileChangeMessageContent( + displayName = null, + prevDisplayName = null, + avatarUrl = null, + prevAvatarUrl = null, + ) + val sameContent = aProfileChangeMessageContent( + displayName = newDisplayName, + prevDisplayName = newDisplayName, + avatarUrl = "same_avatar_url", + prevAvatarUrl = "same_avatar_url", + ) + + val youChangedBothEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) + val youChangedBoth = formatter.format(youChangedBothEvent, false) + assertThat(youChangedBoth).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName\n(avatar was changed too)") + + val invalidContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = invalidContent) + val invalidMessage = formatter.format(invalidContentEvent, false) + assertThat(invalidMessage).isNull() + + val sameContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = sameContent) + val sameMessage = formatter.format(sameContentEvent, false) + assertThat(sameMessage).isNull() + } + + // endregion + + // region Polls + + @Test + @Config(qualifiers = "en") + fun `Computes last message for poll in DM`() { + val pollContent = aPollContent() + + val mineContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = "Alice", content = pollContent) + assertThat(formatter.format(mineContentEvent, true)).isEqualTo("Poll: Do you like polls?") + + val contentEvent = createRoomEvent(sentByYou = false, senderDisplayName = "Bob", content = pollContent) + assertThat(formatter.format(contentEvent, true)).isEqualTo("Poll: Do you like polls?") + } + + @Test + @Config(qualifiers = "en") + fun `Computes last message for poll in room`() { + val pollContent = aPollContent() + + val mineContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = "Alice", content = pollContent) + assertThat(formatter.format(mineContentEvent, false).toString()).isEqualTo("You: Poll: Do you like polls?") + + val contentEvent = createRoomEvent(sentByYou = false, senderDisplayName = "Bob", content = pollContent) + assertThat(formatter.format(contentEvent, false).toString()).isEqualTo("Bob: Poll: Do you like polls?") + } + + // endregion + + private fun createRoomEvent( + sentByYou: Boolean, + senderDisplayName: String?, + content: EventContent, + ): EventTimelineItem { + val sender = if (sentByYou) A_USER_ID else someoneElseId + val profile = aProfileTimelineDetails(senderDisplayName) + return anEventTimelineItem( + content = content, + senderProfile = profile, + sender = sender, + isOwn = sentByYou, + ) + } + + private val someoneElseId = UserId("@someone_else:domain") +} diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt index ee4b827098f..4ff71608f89 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt @@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessage import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent -import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType @@ -47,6 +46,7 @@ import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageCo import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import org.junit.Before @@ -198,8 +198,8 @@ class DefaultPinnedMessagesBannerFormatterTest { @Config(qualifiers = "en") fun `Membership change - joined`() { val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.JOINED) + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.JOINED) val youJoinedRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youJoinedRoom = formatter.format(youJoinedRoomEvent) @@ -214,8 +214,8 @@ class DefaultPinnedMessagesBannerFormatterTest { @Config(qualifiers = "en") fun `Membership change - left`() { val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.LEFT) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.LEFT) + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.LEFT) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.LEFT) val youLeftRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youLeftRoom = formatter.format(youLeftRoomEvent) @@ -231,10 +231,10 @@ class DefaultPinnedMessagesBannerFormatterTest { fun `Membership change - banned`() { val otherName = "Other" val third = "Someone" - val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED) - val youKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED) - val someoneKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED) + val youContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED) + val youKickedContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED) + val someoneKickedContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED) val youBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youBanned = formatter.format(youBannedEvent) @@ -258,8 +258,8 @@ class DefaultPinnedMessagesBannerFormatterTest { fun `Membership change - unban`() { val otherName = "Other" val third = "Someone" - val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED) + val youContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED) val youUnbannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youUnbanned = formatter.format(youUnbannedEvent) @@ -275,8 +275,8 @@ class DefaultPinnedMessagesBannerFormatterTest { fun `Membership change - kicked`() { val otherName = "Other" val third = "Someone" - val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED) + val youContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED) val youKickedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youKicked = formatter.format(youKickedEvent) @@ -292,8 +292,8 @@ class DefaultPinnedMessagesBannerFormatterTest { fun `Membership change - invited`() { val otherName = "Other" val third = "Someone" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITED) + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.INVITED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITED) val youWereInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent) val youWereInvited = formatter.format(youWereInvitedEvent) @@ -312,8 +312,8 @@ class DefaultPinnedMessagesBannerFormatterTest { @Config(qualifiers = "en") fun `Membership change - invitation accepted`() { val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_ACCEPTED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_ACCEPTED) + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_ACCEPTED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_ACCEPTED) val youAcceptedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youAcceptedInvite = formatter.format(youAcceptedInviteEvent) @@ -328,8 +328,8 @@ class DefaultPinnedMessagesBannerFormatterTest { @Config(qualifiers = "en") fun `Membership change - invitation rejected`() { val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_REJECTED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_REJECTED) + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_REJECTED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_REJECTED) val youRejectedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youRejectedInvite = formatter.format(youRejectedInviteEvent) @@ -345,7 +345,7 @@ class DefaultPinnedMessagesBannerFormatterTest { fun `Membership change - invitation revoked`() { val otherName = "Other" val third = "Someone" - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITATION_REVOKED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITATION_REVOKED) val youRevokedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) val youRevokedInvite = formatter.format(youRevokedInviteEvent) @@ -360,8 +360,8 @@ class DefaultPinnedMessagesBannerFormatterTest { @Config(qualifiers = "en") fun `Membership change - knocked`() { val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCKED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.KNOCKED) + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCKED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.KNOCKED) val youKnockedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youKnocked = formatter.format(youKnockedEvent) @@ -377,7 +377,7 @@ class DefaultPinnedMessagesBannerFormatterTest { fun `Membership change - knock accepted`() { val otherName = "Other" val third = "Someone" - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_ACCEPTED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_ACCEPTED) val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) val youAcceptedKnock = formatter.format(youAcceptedKnockEvent) @@ -392,8 +392,8 @@ class DefaultPinnedMessagesBannerFormatterTest { @Config(qualifiers = "en") fun `Membership change - knock retracted`() { val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCK_RETRACTED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), null, MembershipChange.KNOCK_RETRACTED) + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCK_RETRACTED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), null, MembershipChange.KNOCK_RETRACTED) val youRetractedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youRetractedKnock = formatter.format(youRetractedKnockEvent) @@ -409,8 +409,8 @@ class DefaultPinnedMessagesBannerFormatterTest { fun `Membership change - knock denied`() { val otherName = "Other" val third = "Someone" - val youContent = RoomMembershipContent(A_USER_ID, third, MembershipChange.KNOCK_DENIED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_DENIED) + val youContent = aRoomMembershipContent(A_USER_ID, third, MembershipChange.KNOCK_DENIED) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_DENIED) val youDeniedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) val youDeniedKnock = formatter.format(youDeniedKnockEvent) @@ -429,8 +429,8 @@ class DefaultPinnedMessagesBannerFormatterTest { @Config(qualifiers = "en") fun `Membership change - None`() { val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.NONE) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.NONE) + val youContent = aRoomMembershipContent(A_USER_ID, null, MembershipChange.NONE) + val someoneContent = aRoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.NONE) val youNoneRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) val youNoneRoom = formatter.format(youNoneRoomEvent) @@ -447,7 +447,7 @@ class DefaultPinnedMessagesBannerFormatterTest { val otherChanges = arrayOf(MembershipChange.ERROR, MembershipChange.NOT_IMPLEMENTED, null) val results = otherChanges.map { change -> - val content = RoomMembershipContent(A_USER_ID, null, change) + val content = aRoomMembershipContent(A_USER_ID, null, change) val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content) val result = formatter.format(event) change to result diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt deleted file mode 100644 index 11b809258e9..00000000000 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt +++ /dev/null @@ -1,898 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.eventformatter.impl - -import android.content.Context -import androidx.compose.ui.text.AnnotatedString -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage -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.timeline.item.event.AudioMessageType -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.EventTimelineItem -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 -import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange -import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent -import io.element.android.libraries.matrix.api.timeline.item.event.MessageType -import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.OtherState -import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent -import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent -import io.element.android.libraries.matrix.api.timeline.item.event.StateContent -import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType -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.VideoMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType -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.media.aMediaSource -import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser -import io.element.android.libraries.matrix.test.timeline.aPollContent -import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent -import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails -import io.element.android.libraries.matrix.test.timeline.aStickerContent -import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem -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 - -@Suppress("LargeClass") -@RunWith(RobolectricTestRunner::class) -class DefaultRoomLastMessageFormatterTest { - private lateinit var context: Context - private lateinit var fakeMatrixClient: FakeMatrixClient - private lateinit var formatter: DefaultRoomLastMessageFormatter - - @Before - fun setup() { - context = RuntimeEnvironment.getApplication() as Context - fakeMatrixClient = FakeMatrixClient() - val stringProvider = AndroidStringProvider(context.resources) - formatter = DefaultRoomLastMessageFormatter( - sp = AndroidStringProvider(context.resources), - roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider), - profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), - stateContentFormatter = StateContentFormatter(stringProvider), - permalinkParser = FakePermalinkParser(), - ) - } - - @Test - @Config(qualifiers = "en") - fun `Redacted content`() { - val expected = "Message removed" - val senderName = "Someone" - sequenceOf(false, true).forEach { isDm -> - val message = createRoomEvent(false, senderName, RedactedContent) - val result = formatter.format(message, isDm) - if (isDm) { - assertThat(result).isEqualTo(expected) - } else { - assertThat(result).isInstanceOf(AnnotatedString::class.java) - assertThat(result.toString()).isEqualTo("$senderName: $expected") - } - } - } - - @Test - @Config(qualifiers = "en") - fun `Sticker content`() { - val body = "a sticker body" - val info = ImageInfo(null, null, null, null, null, null, null) - val message = createRoomEvent(false, null, aStickerContent(body, info, aMediaSource(url = "url"))) - val result = formatter.format(message, false) - val expectedBody = someoneElseId.toString() + ": Sticker (a sticker body)" - assertThat(result.toString()).isEqualTo(expectedBody) - } - - @Test - @Config(qualifiers = "en") - fun `Unable to decrypt content`() { - val expected = "Waiting for this message" - val senderName = "Someone" - sequenceOf(false, true).forEach { isDm -> - val message = createRoomEvent(false, senderName, UnableToDecryptContent(UnableToDecryptContent.Data.Unknown)) - val result = formatter.format(message, isDm) - if (isDm) { - assertThat(result).isEqualTo(expected) - } else { - assertThat(result).isInstanceOf(AnnotatedString::class.java) - assertThat(result.toString()).isEqualTo("$senderName: $expected") - } - } - } - - @Test - @Config(qualifiers = "en") - fun `FailedToParseMessageLike, FailedToParseState & Unknown content`() { - val expected = "Unsupported event" - val senderName = "Someone" - sequenceOf(false, true).forEach { isDm -> - sequenceOf( - FailedToParseMessageLikeContent("", ""), - FailedToParseStateContent("", "", ""), - UnknownContent, - ).forEach { type -> - val message = createRoomEvent(false, senderName, type) - val result = formatter.format(message, isDm) - if (isDm) { - assertWithMessage("$type was not properly handled").that(result).isEqualTo(expected) - } else { - assertWithMessage("$type does not create an AnnotatedString").that(result).isInstanceOf(AnnotatedString::class.java) - assertWithMessage("$type was not properly handled").that(result.toString()).isEqualTo("$senderName: $expected") - } - } - } - } - - // region Message contents - - @Test - @Config(qualifiers = "en") - fun `Message contents sent by other user`() { - testMessageContents( - sentByYou = false, - senderName = "Alice", - expectedPrefix = "Alice", - ) - } - - @Test - @Config(qualifiers = "en") - fun `Message contents sent by current user`() { - testMessageContents( - sentByYou = true, - senderName = "Bob", - expectedPrefix = "You", - ) - } - - private fun testMessageContents( - sentByYou: Boolean, - senderName: String, - expectedPrefix: String, - ) { - val body = "Shared body" - fun createMessageContent(type: MessageType): MessageContent { - return MessageContent(body, null, false, false, type) - } - - val sharedContentMessagesTypes = arrayOf( - TextMessageType(body, null), - VideoMessageType(body, null, null, MediaSource("url"), null), - AudioMessageType(body, null, null, MediaSource("url"), null), - VoiceMessageType(body, null, null, MediaSource("url"), null, null), - ImageMessageType(body, null, null, MediaSource("url"), null), - StickerMessageType(body, null, null, MediaSource("url"), null), - FileMessageType(body, null, null, MediaSource("url"), null), - LocationMessageType(body, "geo:1,2", null), - NoticeMessageType(body, null), - EmoteMessageType(body, null), - OtherMessageType(msgType = "a_type", body = body), - ) - val resultsInRoom = mutableListOf>() - val resultsInDm = mutableListOf>() - - // Create messages for all types in DM and Room mode - sequenceOf(false, true).forEach { isDm -> - sharedContentMessagesTypes.forEach { type -> - val content = createMessageContent(type) - val message = createRoomEvent(sentByYou = sentByYou, senderDisplayName = senderName, content = content) - val result = formatter.format(message, isDmRoom = isDm) - if (isDm) { - resultsInDm.add(type to result) - } else { - resultsInRoom.add(type to result) - } - } - } - - // Verify results of DM mode - for ((type, result) in resultsInDm) { - val string = result.toString() - val expectedResult = when (type) { - is VideoMessageType -> "Video: Shared body" - is AudioMessageType -> "Audio: Shared body" - is VoiceMessageType -> "Voice message" - is ImageMessageType -> "Image: Shared body" - is StickerMessageType -> "Sticker: Shared body" - is FileMessageType -> "File: Shared body" - is LocationMessageType -> "Shared location" - is EmoteMessageType -> "* $senderName ${type.body}" - is TextMessageType, - is NoticeMessageType, - is OtherMessageType -> body - } - val shouldCreateAnnotatedString = when (type) { - is VideoMessageType -> true - is AudioMessageType -> true - is VoiceMessageType -> false - is ImageMessageType -> true - is StickerMessageType -> true - is FileMessageType -> true - is LocationMessageType -> false - is EmoteMessageType -> false - is TextMessageType -> false - is NoticeMessageType -> false - is OtherMessageType -> false - } - if (shouldCreateAnnotatedString) { - assertWithMessage("$type doesn't produce an AnnotatedString") - .that(result) - .isInstanceOf(AnnotatedString::class.java) - } - assertWithMessage("$type was not properly handled for DM").that(string).isEqualTo(expectedResult) - } - - // Verify results of Room mode - for ((type, result) in resultsInRoom) { - val string = result.toString() - val expectedResult = when (type) { - is VideoMessageType -> "$expectedPrefix: Video: Shared body" - is AudioMessageType -> "$expectedPrefix: Audio: Shared body" - is VoiceMessageType -> "$expectedPrefix: Voice message" - is ImageMessageType -> "$expectedPrefix: Image: Shared body" - is StickerMessageType -> "$expectedPrefix: Sticker: Shared body" - is FileMessageType -> "$expectedPrefix: File: Shared body" - is LocationMessageType -> "$expectedPrefix: Shared location" - is TextMessageType, - is NoticeMessageType, - is OtherMessageType -> "$expectedPrefix: $body" - is EmoteMessageType -> "* $senderName ${type.body}" - } - val shouldCreateAnnotatedString = when (type) { - is VideoMessageType -> true - is AudioMessageType -> true - is VoiceMessageType -> true - is ImageMessageType -> true - is StickerMessageType -> true - is FileMessageType -> true - is LocationMessageType -> false - is EmoteMessageType -> false - is TextMessageType -> true - is NoticeMessageType -> true - is OtherMessageType -> true - } - if (shouldCreateAnnotatedString) { - assertWithMessage("$type doesn't produce an AnnotatedString") - .that(result) - .isInstanceOf(AnnotatedString::class.java) - } - assertWithMessage("$type was not properly handled for room").that(string).isEqualTo(expectedResult) - } - } - - // endregion - - // region Membership change - - @Test - @Config(qualifiers = "en") - fun `Membership change - joined`() { - val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.JOINED) - - val youJoinedRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youJoinedRoom = formatter.format(youJoinedRoomEvent, false) - assertThat(youJoinedRoom).isEqualTo("You joined the room") - - val someoneJoinedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneJoinedRoom = formatter.format(someoneJoinedRoomEvent, false) - assertThat(someoneJoinedRoom).isEqualTo("$otherName joined the room") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - left`() { - val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.LEFT) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.LEFT) - - val youLeftRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youLeftRoom = formatter.format(youLeftRoomEvent, false) - assertThat(youLeftRoom).isEqualTo("You left the room") - - val someoneLeftRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneLeftRoom = formatter.format(someoneLeftRoomEvent, false) - assertThat(someoneLeftRoom).isEqualTo("$otherName left the room") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - banned`() { - val otherName = "Other" - val third = "Someone" - val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED) - val youKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED) - val someoneKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED) - - val youBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youBanned = formatter.format(youBannedEvent, false) - assertThat(youBanned).isEqualTo("You banned $third") - - val youKickBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youKickedContent) - val youKickedBanned = formatter.format(youKickBannedEvent, false) - assertThat(youKickedBanned).isEqualTo("You banned $third") - - val someoneBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneBanned = formatter.format(someoneBannedEvent, false) - assertThat(someoneBanned).isEqualTo("$otherName banned $third") - - val someoneKickBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneKickedContent) - val someoneKickBanned = formatter.format(someoneKickBannedEvent, false) - assertThat(someoneKickBanned).isEqualTo("$otherName banned $third") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - unban`() { - val otherName = "Other" - val third = "Someone" - val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED) - - val youUnbannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youUnbanned = formatter.format(youUnbannedEvent, false) - assertThat(youUnbanned).isEqualTo("You unbanned $third") - - val someoneUnbannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneUnbanned = formatter.format(someoneUnbannedEvent, false) - assertThat(someoneUnbanned).isEqualTo("$otherName unbanned $third") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - kicked`() { - val otherName = "Other" - val third = "Someone" - val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED) - - val youKickedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youKicked = formatter.format(youKickedEvent, false) - assertThat(youKicked).isEqualTo("You removed $third") - - val someoneKickedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneKicked = formatter.format(someoneKickedEvent, false) - assertThat(someoneKicked).isEqualTo("$otherName removed $third") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - invited`() { - val otherName = "Other" - val third = "Someone" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITED) - - val youWereInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent) - val youWereInvited = formatter.format(youWereInvitedEvent, false) - assertThat(youWereInvited).isEqualTo("$otherName invited you") - - val youInvitedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) - val youInvited = formatter.format(youInvitedEvent, false) - assertThat(youInvited).isEqualTo("You invited $third") - - val someoneInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneInvited = formatter.format(someoneInvitedEvent, false) - assertThat(someoneInvited).isEqualTo("$otherName invited $third") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - invitation accepted`() { - val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_ACCEPTED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_ACCEPTED) - - val youAcceptedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youAcceptedInvite = formatter.format(youAcceptedInviteEvent, false) - assertThat(youAcceptedInvite).isEqualTo("You accepted the invite") - - val someoneAcceptedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneAcceptedInvite = formatter.format(someoneAcceptedInviteEvent, false) - assertThat(someoneAcceptedInvite).isEqualTo("$otherName accepted the invite") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - invitation rejected`() { - val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_REJECTED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_REJECTED) - - val youRejectedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youRejectedInvite = formatter.format(youRejectedInviteEvent, false) - assertThat(youRejectedInvite).isEqualTo("You rejected the invitation") - - val someoneRejectedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneRejectedInvite = formatter.format(someoneRejectedInviteEvent, false) - assertThat(someoneRejectedInvite).isEqualTo("$otherName rejected the invitation") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - invitation revoked`() { - val otherName = "Other" - val third = "Someone" - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITATION_REVOKED) - - val youRevokedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) - val youRevokedInvite = formatter.format(youRevokedInviteEvent, false) - assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for $third to join the room") - - val someoneRevokedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneRevokedInvite = formatter.format(someoneRevokedInviteEvent, false) - assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for $third to join the room") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - knocked`() { - val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCKED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.KNOCKED) - - val youKnockedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youKnocked = formatter.format(youKnockedEvent, false) - assertThat(youKnocked).isEqualTo("You requested to join") - - val someoneKnockedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneKnocked = formatter.format(someoneKnockedEvent, false) - assertThat(someoneKnocked).isEqualTo("$otherName is requesting to join") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - knock accepted`() { - val otherName = "Other" - val third = "Someone" - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_ACCEPTED) - - val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) - val youAcceptedKnock = formatter.format(youAcceptedKnockEvent, false) - assertThat(youAcceptedKnock).isEqualTo("You allowed $third to join") - - val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false) - assertThat(someoneAcceptedKnock).isEqualTo("$otherName granted access to $third") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - knock retracted`() { - val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCK_RETRACTED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), null, MembershipChange.KNOCK_RETRACTED) - - val youRetractedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youRetractedKnock = formatter.format(youRetractedKnockEvent, false) - assertThat(youRetractedKnock).isEqualTo("You cancelled your request to join") - - val someoneRetractedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneRetractedKnock = formatter.format(someoneRetractedKnockEvent, false) - assertThat(someoneRetractedKnock).isEqualTo("$otherName is no longer interested in joining") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - knock denied`() { - val otherName = "Other" - val third = "Someone" - val youContent = RoomMembershipContent(A_USER_ID, third, MembershipChange.KNOCK_DENIED) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_DENIED) - - val youDeniedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) - val youDeniedKnock = formatter.format(youDeniedKnockEvent, false) - assertThat(youDeniedKnock).isEqualTo("You rejected $third's request to join") - - val someoneDeniedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneDeniedKnock = formatter.format(someoneDeniedKnockEvent, false) - assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected $third's request to join") - - val someoneDeniedYourKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent) - val someoneDeniedYourKnock = formatter.format(someoneDeniedYourKnockEvent, false) - assertThat(someoneDeniedYourKnock).isEqualTo("$otherName rejected your request to join") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - None`() { - val otherName = "Other" - val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.NONE) - val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.NONE) - - val youNoneRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youNoneRoom = formatter.format(youNoneRoomEvent, false) - assertThat(youNoneRoom).isEqualTo("You made no changes") - - val someoneNoneRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneNoneRoom = formatter.format(someoneNoneRoomEvent, false) - assertThat(someoneNoneRoom).isEqualTo("$otherName made no changes") - } - - @Test - @Config(qualifiers = "en") - fun `Membership change - others`() { - val otherChanges = arrayOf(MembershipChange.ERROR, MembershipChange.NOT_IMPLEMENTED, null) - - val results = otherChanges.map { change -> - val content = RoomMembershipContent(A_USER_ID, null, change) - val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content) - val result = formatter.format(event, false) - change to result - } - val expected = otherChanges.map { it to null } - assertThat(results).isEqualTo(expected) - } - - // endregion - - // region Room State - - @Test - @Config(qualifiers = "en") - fun `Room state change - avatar`() { - val otherName = "Other" - val changedContent = StateContent("", OtherState.RoomAvatar("new_avatar")) - val removedContent = StateContent("", OtherState.RoomAvatar(null)) - - val youChangedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedRoomAvatar = formatter.format(youChangedRoomAvatarEvent, false) - assertThat(youChangedRoomAvatar).isEqualTo("You changed the room avatar") - - val someoneChangedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangedRoomAvatar = formatter.format(someoneChangedRoomAvatarEvent, false) - assertThat(someoneChangedRoomAvatar).isEqualTo("$otherName changed the room avatar") - - val youRemovedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedRoomAvatar = formatter.format(youRemovedRoomAvatarEvent, false) - assertThat(youRemovedRoomAvatar).isEqualTo("You removed the room avatar") - - val someoneRemovedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedRoomAvatar = formatter.format(someoneRemovedRoomAvatarEvent, false) - assertThat(someoneRemovedRoomAvatar).isEqualTo("$otherName removed the room avatar") - } - - @Test - @Config(qualifiers = "en") - fun `Room state change - create`() { - val otherName = "Other" - val content = StateContent("", OtherState.RoomCreate) - - val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content) - val youCreatedRoom = formatter.format(youCreatedRoomMessage, false) - assertThat(youCreatedRoom).isEqualTo("You created the room") - - val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content) - val someoneCreatedRoom = formatter.format(someoneCreatedRoomEvent, false) - assertThat(someoneCreatedRoom).isEqualTo("$otherName created the room") - } - - @Test - @Config(qualifiers = "en") - fun `Room state change - encryption`() { - val otherName = "Other" - val content = StateContent("", OtherState.RoomEncryption) - - val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content) - val youCreatedRoom = formatter.format(youCreatedRoomMessage, false) - assertThat(youCreatedRoom).isEqualTo("Encryption enabled") - - val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content) - val someoneCreatedRoom = formatter.format(someoneCreatedRoomEvent, false) - assertThat(someoneCreatedRoom).isEqualTo("Encryption enabled") - } - - @Test - @Config(qualifiers = "en") - fun `Room state change - room name`() { - val otherName = "Other" - val newName = "New name" - val changedContent = StateContent("", OtherState.RoomName(newName)) - val removedContent = StateContent("", OtherState.RoomName(null)) - - val youChangedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedRoomName = formatter.format(youChangedRoomNameEvent, false) - assertThat(youChangedRoomName).isEqualTo("You changed the room name to: $newName") - - val someoneChangedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangedRoomName = formatter.format(someoneChangedRoomNameEvent, false) - assertThat(someoneChangedRoomName).isEqualTo("$otherName changed the room name to: $newName") - - val youRemovedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedRoomName = formatter.format(youRemovedRoomNameEvent, false) - assertThat(youRemovedRoomName).isEqualTo("You removed the room name") - - val someoneRemovedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedRoomName = formatter.format(someoneRemovedRoomNameEvent, false) - assertThat(someoneRemovedRoomName).isEqualTo("$otherName removed the room name") - } - - @Test - @Config(qualifiers = "en") - fun `Room state change - third party invite`() { - val otherName = "Other" - val inviteeName = "Alice" - val changedContent = StateContent("", OtherState.RoomThirdPartyInvite(inviteeName)) - val removedContent = StateContent("", OtherState.RoomThirdPartyInvite(null)) - - val youInvitedSomeoneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youInvitedSomeone = formatter.format(youInvitedSomeoneEvent, false) - assertThat(youInvitedSomeone).isEqualTo("You sent an invitation to $inviteeName to join the room") - - val someoneInvitedSomeoneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneInvitedSomeone = formatter.format(someoneInvitedSomeoneEvent, false) - assertThat(someoneInvitedSomeone).isEqualTo("$otherName sent an invitation to $inviteeName to join the room") - - val youInvitedNoOneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youInvitedNoOne = formatter.format(youInvitedNoOneEvent, false) - assertThat(youInvitedNoOne).isNull() - - val someoneInvitedNoOneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneInvitedNoOne = formatter.format(someoneInvitedNoOneEvent, false) - assertThat(someoneInvitedNoOne).isNull() - } - - @Test - @Config(qualifiers = "en") - fun `Room state change - room topic`() { - val otherName = "Other" - val roomTopic = "New topic" - val changedContent = StateContent("", OtherState.RoomTopic(roomTopic)) - val removedContent = StateContent("", OtherState.RoomTopic(null)) - val blankContent = StateContent("", OtherState.RoomTopic("")) - - val youChangedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedRoomTopic = formatter.format(youChangedRoomTopicEvent, false) - assertThat(youChangedRoomTopic).isEqualTo("You changed the topic to: $roomTopic") - - val someoneChangedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangedRoomTopic = formatter.format(someoneChangedRoomTopicEvent, false) - assertThat(someoneChangedRoomTopic).isEqualTo("$otherName changed the topic to: $roomTopic") - - val youRemovedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedRoomTopic = formatter.format(youRemovedRoomTopicEvent, false) - assertThat(youRemovedRoomTopic).isEqualTo("You removed the room topic") - - val someoneRemovedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedRoomTopic = formatter.format(someoneRemovedRoomTopicEvent, false) - assertThat(someoneRemovedRoomTopic).isEqualTo("$otherName removed the room topic") - - val youSetBlankRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = blankContent) - val youSetBlankRoomTopic = formatter.format(youSetBlankRoomTopicEvent, false) - assertThat(youSetBlankRoomTopic).isEqualTo("You removed the room topic") - - val someoneSetBlankRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = blankContent) - val someoneSetBlankRoomTopic = formatter.format(someoneSetBlankRoomTopicEvent, false) - assertThat(someoneSetBlankRoomTopic).isEqualTo("$otherName removed the room topic") - } - - @Test - @Config(qualifiers = "en") - fun `Room state change - others must return null`() { - val otherStates = arrayOf( - OtherState.PolicyRuleRoom, - OtherState.PolicyRuleServer, - OtherState.PolicyRuleUser, - OtherState.RoomAliases, - OtherState.RoomCanonicalAlias, - OtherState.RoomGuestAccess, - OtherState.RoomHistoryVisibility, - OtherState.RoomJoinRules, - OtherState.RoomPinnedEvents(OtherState.RoomPinnedEvents.Change.CHANGED), - OtherState.RoomUserPowerLevels(emptyMap()), - OtherState.RoomServerAcl, - OtherState.RoomTombstone, - OtherState.SpaceChild, - OtherState.SpaceParent, - OtherState.Custom("custom_event_type") - ) - - val results = otherStates.map { state -> - val content = StateContent("", state) - val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content) - val result = formatter.format(event, false) - state to result - } - val expected = otherStates.map { it to null } - assertThat(results).isEqualTo(expected) - } - - // endregion - - // region Profile change - - @Test - @Config(qualifiers = "en") - fun `Profile change - avatar`() { - val otherName = "Other" - val changedContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = "old_avatar_url") - val setContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = null) - val removedContent = aProfileChangeMessageContent(avatarUrl = null, prevAvatarUrl = "old_avatar_url") - val invalidContent = aProfileChangeMessageContent(avatarUrl = null, prevAvatarUrl = null) - val sameContent = aProfileChangeMessageContent(avatarUrl = "same_avatar_url", prevAvatarUrl = "same_avatar_url") - - val youChangedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedAvatar = formatter.format(youChangedAvatarEvent, false) - assertThat(youChangedAvatar).isEqualTo("You changed your avatar") - - val someoneChangeAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangeAvatar = formatter.format(someoneChangeAvatarEvent, false) - assertThat(someoneChangeAvatar).isEqualTo("$otherName changed their avatar") - - val youSetAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent) - val youSetAvatar = formatter.format(youSetAvatarEvent, false) - assertThat(youSetAvatar).isEqualTo("You changed your avatar") - - val someoneSetAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent) - val someoneSetAvatar = formatter.format(someoneSetAvatarEvent, false) - assertThat(someoneSetAvatar).isEqualTo("$otherName changed their avatar") - - val youRemovedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedAvatar = formatter.format(youRemovedAvatarEvent, false) - assertThat(youRemovedAvatar).isEqualTo("You changed your avatar") - - val someoneRemovedAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedAvatar = formatter.format(someoneRemovedAvatarEvent, false) - assertThat(someoneRemovedAvatar).isEqualTo("$otherName changed their avatar") - - val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent) - val unchangedResult = formatter.format(unchangedEvent, false) - assertThat(unchangedResult).isNull() - - val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent) - val invalidResult = formatter.format(invalidEvent, false) - assertThat(invalidResult).isNull() - } - - @Test - @Config(qualifiers = "en") - fun `Profile change - display name`() { - val newDisplayName = "New" - val oldDisplayName = "Old" - val otherName = "Other" - val changedContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = oldDisplayName) - val setContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = null) - val removedContent = aProfileChangeMessageContent(displayName = null, prevDisplayName = oldDisplayName) - val sameContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = newDisplayName) - val invalidContent = aProfileChangeMessageContent(displayName = null, prevDisplayName = null) - - val youChangedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedDisplayName = formatter.format(youChangedDisplayNameEvent, false) - assertThat(youChangedDisplayName).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName") - - val someoneChangedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangedDisplayName = formatter.format(someoneChangedDisplayNameEvent, false) - assertThat(someoneChangedDisplayName).isEqualTo("$someoneElseId changed their display name from $oldDisplayName to $newDisplayName") - - val youSetDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent) - val youSetDisplayName = formatter.format(youSetDisplayNameEvent, false) - assertThat(youSetDisplayName).isEqualTo("You set your display name to $newDisplayName") - - val someoneSetDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent) - val someoneSetDisplayName = formatter.format(someoneSetDisplayNameEvent, false) - assertThat(someoneSetDisplayName).isEqualTo("$someoneElseId set their display name to $newDisplayName") - - val youRemovedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedDisplayName = formatter.format(youRemovedDisplayNameEvent, false) - assertThat(youRemovedDisplayName).isEqualTo("You removed your display name (it was $oldDisplayName)") - - val someoneRemovedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedDisplayName = formatter.format(someoneRemovedDisplayNameEvent, false) - assertThat(someoneRemovedDisplayName).isEqualTo("$someoneElseId removed their display name (it was $oldDisplayName)") - - val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent) - val unchangedResult = formatter.format(unchangedEvent, false) - assertThat(unchangedResult).isNull() - - val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent) - val invalidResult = formatter.format(invalidEvent, false) - assertThat(invalidResult).isNull() - } - - @Test - @Config(qualifiers = "en") - fun `Profile change - display name & avatar`() { - val newDisplayName = "New" - val oldDisplayName = "Old" - val changedContent = aProfileChangeMessageContent( - displayName = newDisplayName, - prevDisplayName = oldDisplayName, - avatarUrl = "new_avatar_url", - prevAvatarUrl = "old_avatar_url", - ) - val invalidContent = aProfileChangeMessageContent( - displayName = null, - prevDisplayName = null, - avatarUrl = null, - prevAvatarUrl = null, - ) - val sameContent = aProfileChangeMessageContent( - displayName = newDisplayName, - prevDisplayName = newDisplayName, - avatarUrl = "same_avatar_url", - prevAvatarUrl = "same_avatar_url", - ) - - val youChangedBothEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedBoth = formatter.format(youChangedBothEvent, false) - assertThat(youChangedBoth).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName\n(avatar was changed too)") - - val invalidContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = invalidContent) - val invalidMessage = formatter.format(invalidContentEvent, false) - assertThat(invalidMessage).isNull() - - val sameContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = sameContent) - val sameMessage = formatter.format(sameContentEvent, false) - assertThat(sameMessage).isNull() - } - - // endregion - - // region Polls - - @Test - @Config(qualifiers = "en") - fun `Computes last message for poll in DM`() { - val pollContent = aPollContent() - - val mineContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = "Alice", content = pollContent) - assertThat(formatter.format(mineContentEvent, true)).isEqualTo("Poll: Do you like polls?") - - val contentEvent = createRoomEvent(sentByYou = false, senderDisplayName = "Bob", content = pollContent) - assertThat(formatter.format(contentEvent, true)).isEqualTo("Poll: Do you like polls?") - } - - @Test - @Config(qualifiers = "en") - fun `Computes last message for poll in room`() { - val pollContent = aPollContent() - - val mineContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = "Alice", content = pollContent) - assertThat(formatter.format(mineContentEvent, false).toString()).isEqualTo("You: Poll: Do you like polls?") - - val contentEvent = createRoomEvent(sentByYou = false, senderDisplayName = "Bob", content = pollContent) - assertThat(formatter.format(contentEvent, false).toString()).isEqualTo("Bob: Poll: Do you like polls?") - } - - // endregion - - private fun createRoomEvent( - sentByYou: Boolean, - senderDisplayName: String?, - content: EventContent, - ): EventTimelineItem { - val sender = if (sentByYou) A_USER_ID else someoneElseId - val profile = aProfileTimelineDetails(senderDisplayName) - return anEventTimelineItem( - content = content, - senderProfile = profile, - sender = sender, - isOwn = sentByYou, - ) - } - - private val someoneElseId = UserId("@someone_else:domain") -} 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 289e7132b5e..6f1d4b10c61 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 @@ -54,20 +54,6 @@ enum class FeatureFlags( defaultValue = { true }, isFinished = true, ), - Mentions( - key = "feature.mentions", - title = "Mentions", - description = "Type `@` to get mention suggestions and insert them", - defaultValue = { true }, - isFinished = false, - ), - RoomAliasSuggestions( - key = "feature.roomAliasSuggestions", - title = "Room alias suggestions", - description = "Type `#` to get room alias suggestions and insert them", - defaultValue = { false }, - isFinished = false, - ), MarkAsUnread( key = "feature.markAsUnread", title = "Mark as unread", @@ -126,6 +112,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", @@ -133,6 +130,13 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + Space( + key = "feature.space", + title = "Spaces", + description = "Spaces are under active development, only developers should enable this flog for now.", + defaultValue = { false }, + isFinished = false, + ), MediaUploadOnSendQueue( key = "feature.media_upload_through_send_queue", title = "Media upload through send queue", @@ -161,11 +165,14 @@ enum class FeatureFlags( defaultValue = { true }, isFinished = false, ), - EventCache( - key = "feature.event_cache", - title = "Use SDK Event cache", - description = "Warning: you must kill and restart the app for the change to take effect.", - defaultValue = { false }, + PrintLogsToLogcat( + key = "feature.print_logs_to_logcat", + title = "Print logs to logcat", + description = "Print logs to logcat in addition to log files. Requires an app restart to take effect." + + "\n\nWARNING: this will make the logs visible in the device logs and may affect performance. " + + "It's not intended for daily usage in release builds.", + defaultValue = { buildMeta -> buildMeta.buildType != BuildType.RELEASE }, + // False so it's displayed in the developer options screen isFinished = false, ), } diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt new file mode 100644 index 00000000000..e0b84337499 --- /dev/null +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt @@ -0,0 +1,13 @@ +/* + * 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.fullscreenintent.api + +sealed interface FullScreenIntentPermissionsEvents { + data object Dismiss : FullScreenIntentPermissionsEvents + data object OpenSettings : FullScreenIntentPermissionsEvents +} diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt index 3d77ebb4b57..5b3f84f977c 100644 --- a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt @@ -10,6 +10,5 @@ package io.element.android.libraries.fullscreenintent.api data class FullScreenIntentPermissionsState( val permissionGranted: Boolean, val shouldDisplayBanner: Boolean, - val dismissFullScreenIntentBanner: () -> Unit, - val openFullScreenIntentSettings: () -> Unit, + val eventSink: (FullScreenIntentPermissionsEvents) -> Unit, ) diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt index 7dbe340c396..25ab2ab5220 100644 --- a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt @@ -10,11 +10,9 @@ package io.element.android.libraries.fullscreenintent.api fun aFullScreenIntentPermissionsState( permissionGranted: Boolean = true, shouldDisplay: Boolean = false, - openFullScreenIntentSettings: () -> Unit = {}, - dismissFullScreenIntentBanner: () -> Unit = {}, + eventSink: (FullScreenIntentPermissionsEvents) -> Unit = {}, ) = FullScreenIntentPermissionsState( permissionGranted = permissionGranted, shouldDisplayBanner = shouldDisplay, - openFullScreenIntentSettings = openFullScreenIntentSettings, - dismissFullScreenIntentBanner = dismissFullScreenIntentBanner, + eventSink = eventSink, ) diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt index 992c345e5ca..14cc438f41f 100644 --- a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt +++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.fullscreenintent.impl import android.content.ActivityNotFoundException import android.content.Intent -import android.net.Uri import android.os.Build import android.provider.Settings import androidx.compose.runtime.Composable @@ -17,12 +16,14 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.core.app.NotificationManagerCompat +import androidx.core.net.toUri import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher @@ -60,15 +61,20 @@ class FullScreenIntentPermissionsPresenter @Inject constructor( val coroutineScope = rememberCoroutineScope() val isGranted = notificationManagerCompat.canUseFullScreenIntent() val isBannerDismissed by isFullScreenIntentBannerDismissed.collectAsState(initial = true) + + fun handleEvents(event: FullScreenIntentPermissionsEvents) { + when (event) { + FullScreenIntentPermissionsEvents.Dismiss -> coroutineScope.launch { + dismissFullScreenIntentBanner() + } + FullScreenIntentPermissionsEvents.OpenSettings -> openFullScreenIntentSettings() + } + } + return FullScreenIntentPermissionsState( permissionGranted = isGranted, shouldDisplayBanner = !isBannerDismissed && !isGranted, - dismissFullScreenIntentBanner = { - coroutineScope.launch { - dismissFullScreenIntentBanner() - } - }, - openFullScreenIntentSettings = ::openFullScreenIntentSettings, + eventSink = ::handleEvents, ) } @@ -77,7 +83,7 @@ class FullScreenIntentPermissionsPresenter @Inject constructor( try { val intent = Intent( Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT, - Uri.parse("package:${buildMeta.applicationId}") + "package:${buildMeta.applicationId}".toUri() ) externalIntentLauncher.launch(intent) } catch (e: ActivityNotFoundException) { diff --git a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt index 8cf508562b9..0d10032da0a 100644 --- a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt +++ b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt @@ -15,6 +15,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents import io.element.android.libraries.fullscreenintent.impl.FullScreenIntentPermissionsPresenter import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory @@ -76,10 +77,8 @@ class FullScreenIntentPermissionsPresenterTest { }.test { skipItems(1) val loadedItem = awaitItem() - loadedItem.dismissFullScreenIntentBanner() - + loadedItem.eventSink(FullScreenIntentPermissionsEvents.Dismiss) runCurrent() - assertThat(awaitItem().shouldDisplayBanner).isFalse() } } @@ -94,10 +93,8 @@ class FullScreenIntentPermissionsPresenterTest { }.test { skipItems(1) val loadedItem = awaitItem() - loadedItem.openFullScreenIntentSettings() - + loadedItem.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) launchLambda.assertions().isCalledOnce() - cancelAndIgnoreRemainingEvents() } } @@ -115,10 +112,8 @@ class FullScreenIntentPermissionsPresenterTest { }.test { skipItems(1) val loadedItem = awaitItem() - loadedItem.openFullScreenIntentSettings() - + loadedItem.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) launchLambda.assertions().isNeverCalled() - cancelAndIgnoreRemainingEvents() } } diff --git a/libraries/indicator/impl/build.gradle.kts b/libraries/indicator/impl/build.gradle.kts index 4ce892325c7..d5046eb00cd 100644 --- a/libraries/indicator/impl/build.gradle.kts +++ b/libraries/indicator/impl/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) testImplementation(libs.test.turbine) testImplementation(libs.test.truth) } diff --git a/libraries/indicator/impl/src/test/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorServiceTest.kt b/libraries/indicator/impl/src/test/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorServiceTest.kt new file mode 100644 index 00000000000..9e325a65739 --- /dev/null +++ b/libraries/indicator/impl/src/test/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorServiceTest.kt @@ -0,0 +1,102 @@ +/* + * 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.indicator.impl + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultIndicatorServiceTest { + @Test + fun `test - showRoomListTopBarIndicator`() = runTest { + val encryptionService = FakeEncryptionService() + val sessionVerificationService = FakeSessionVerificationService() + val sut = DefaultIndicatorService( + sessionVerificationService = sessionVerificationService, + encryptionService = encryptionService, + ) + moleculeFlow(RecompositionMode.Immediate) { + sut.showRoomListTopBarIndicator().value + }.test { + assertThat(awaitItem()).isTrue() + sessionVerificationService.emitNeedsSessionVerification(false) + encryptionService.emitBackupState(BackupState.ENABLED) + encryptionService.emitRecoveryState(RecoveryState.ENABLED) + assertThat(awaitItem()).isFalse() + sessionVerificationService.emitNeedsSessionVerification(true) + assertThat(awaitItem()).isTrue() + } + } + + @Test + fun `test - showSettingChatBackupIndicator is true when BackupState is UNKNOWN`() = runTest { + val encryptionService = FakeEncryptionService() + val sessionVerificationService = FakeSessionVerificationService() + val sut = DefaultIndicatorService( + sessionVerificationService = sessionVerificationService, + encryptionService = encryptionService, + ) + moleculeFlow(RecompositionMode.Immediate) { + sut.showSettingChatBackupIndicator().value + }.test { + assertThat(awaitItem()).isTrue() + encryptionService.emitBackupState(BackupState.ENABLED) + encryptionService.emitRecoveryState(RecoveryState.ENABLED) + assertThat(awaitItem()).isFalse() + encryptionService.emitBackupState(BackupState.UNKNOWN) + assertThat(awaitItem()).isTrue() + } + } + + @Test + fun `test - showSettingChatBackupIndicator is true when recoveryState is DISABLED`() = runTest { + val encryptionService = FakeEncryptionService() + val sessionVerificationService = FakeSessionVerificationService() + val sut = DefaultIndicatorService( + sessionVerificationService = sessionVerificationService, + encryptionService = encryptionService, + ) + moleculeFlow(RecompositionMode.Immediate) { + sut.showSettingChatBackupIndicator().value + }.test { + assertThat(awaitItem()).isTrue() + encryptionService.emitBackupState(BackupState.ENABLED) + encryptionService.emitRecoveryState(RecoveryState.ENABLED) + assertThat(awaitItem()).isFalse() + encryptionService.emitRecoveryState(RecoveryState.DISABLED) + assertThat(awaitItem()).isTrue() + } + } + + @Test + fun `test - showSettingChatBackupIndicator is true when recoveryState is INCOMPLETE`() = runTest { + val encryptionService = FakeEncryptionService() + val sessionVerificationService = FakeSessionVerificationService() + val sut = DefaultIndicatorService( + sessionVerificationService = sessionVerificationService, + encryptionService = encryptionService, + ) + moleculeFlow(RecompositionMode.Immediate) { + sut.showSettingChatBackupIndicator().value + }.test { + assertThat(awaitItem()).isTrue() + encryptionService.emitBackupState(BackupState.ENABLED) + encryptionService.emitRecoveryState(RecoveryState.ENABLED) + assertThat(awaitItem()).isFalse() + encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) + assertThat(awaitItem()).isTrue() + } + } +} diff --git a/libraries/indicator/test/build.gradle.kts b/libraries/indicator/test/build.gradle.kts new file mode 100644 index 00000000000..5d61dde8be9 --- /dev/null +++ b/libraries/indicator/test/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.indicator.test" +} + +dependencies { + implementation(projects.libraries.matrix.api) + api(projects.libraries.indicator.api) +} diff --git a/libraries/indicator/test/src/main/kotlin/io/element/android/libraries/indicator/test/FakeIndicatorService.kt b/libraries/indicator/test/src/main/kotlin/io/element/android/libraries/indicator/test/FakeIndicatorService.kt new file mode 100644 index 00000000000..26f8547b66d --- /dev/null +++ b/libraries/indicator/test/src/main/kotlin/io/element/android/libraries/indicator/test/FakeIndicatorService.kt @@ -0,0 +1,37 @@ +/* + * 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.indicator.test + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import io.element.android.libraries.indicator.api.IndicatorService + +class FakeIndicatorService : IndicatorService { + private val showRoomListTopBarIndicatorResult: MutableState = mutableStateOf(false) + private val showSettingChatBackupIndicatorResult: MutableState = mutableStateOf(false) + + fun setShowRoomListTopBarIndicator(value: Boolean) { + showRoomListTopBarIndicatorResult.value = value + } + + fun setShowSettingChatBackupIndicator(value: Boolean) { + showSettingChatBackupIndicatorResult.value = value + } + + @Composable + override fun showRoomListTopBarIndicator(): State { + return showRoomListTopBarIndicatorResult + } + + @Composable + override fun showSettingChatBackupIndicator(): State { + return showSettingChatBackupIndicatorResult + } +} diff --git a/libraries/maplibre-compose/build.gradle.kts b/libraries/maplibre-compose/build.gradle.kts index a2c1b5de1c0..756a09c32ca 100644 --- a/libraries/maplibre-compose/build.gradle.kts +++ b/libraries/maplibre-compose/build.gradle.kts @@ -13,8 +13,10 @@ plugins { android { namespace = "io.element.android.libraries.maplibre.compose" - kotlinOptions { - freeCompilerArgs += "-Xexplicit-api=strict" + kotlin { + compilerOptions { + explicitApi() + } } } diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index d96613b1e7c..9cd7e6ac7f3 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -1,3 +1,5 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.setupAnvil /* @@ -19,6 +21,25 @@ android { buildFeatures { buildConfig = true } + + defaultConfig { + buildConfigFieldStr( + name = "CLIENT_URI", + value = BuildTimeConfig.URL_WEBSITE ?: "https://element.io" + ) + buildConfigFieldStr( + name = "LOGO_URI", + value = BuildTimeConfig.URL_LOGO ?: "https://element.io/mobile-icon.png" + ) + buildConfigFieldStr( + name = "TOS_URI", + value = BuildTimeConfig.URL_ACCEPTABLE_USE ?: "https://element.io/acceptable-use-policy-terms" + ) + buildConfigFieldStr( + name = "POLICY_URI", + value = BuildTimeConfig.URL_POLICY ?: "https://element.io/privacy" + ) + } } setupAnvil() 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 cae7ee1bad6..d968ae63de7 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 @@ -7,7 +7,9 @@ package io.element.android.libraries.matrix.api +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.core.DeviceId +import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId @@ -17,19 +19,19 @@ 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.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo -import io.element.android.libraries.matrix.api.room.PendingRoom +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom +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.preview.RoomPreviewInfo 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.roomlist.RoomSummary import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults @@ -39,12 +41,9 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import java.io.Closeable import java.util.Optional -interface MatrixClient : Closeable { +interface MatrixClient { val sessionId: SessionId val deviceId: DeviceId val userProfile: StateFlow @@ -52,9 +51,9 @@ interface MatrixClient : Closeable { val mediaLoader: MatrixMediaLoader val sessionCoroutineScope: CoroutineScope val ignoredUsersFlow: StateFlow> - suspend fun getRoom(roomId: RoomId): MatrixRoom? - suspend fun getPendingRoom(roomId: RoomId): PendingRoom? - suspend fun findDM(userId: UserId): RoomId? + suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? + suspend fun getRoom(roomId: RoomId): BaseRoom? + suspend fun findDM(userId: UserId): Result suspend fun ignoreUser(userId: UserId): Result suspend fun unignoreUser(userId: UserId): Result suspend fun createRoom(createRoomParams: CreateRoomParameters): Result @@ -64,9 +63,9 @@ interface MatrixClient : Closeable { suspend fun setDisplayName(displayName: String): Result suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result - suspend fun joinRoom(roomId: RoomId): Result - suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result - suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result + suspend fun joinRoom(roomId: RoomId): Result + suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result + suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result fun syncService(): SyncService fun sessionVerificationService(): SessionVerificationService fun pushersService(): PushersService @@ -74,6 +73,7 @@ interface MatrixClient : Closeable { fun notificationSettingsService(): NotificationSettingsService fun encryptionService(): EncryptionService fun roomDirectoryService(): RoomDirectoryService + fun mediaPreviewService(): MediaPreviewService suspend fun getCacheSize(): Long /** @@ -83,12 +83,11 @@ interface MatrixClient : Closeable { /** * Logout the user. - * Returns an optional URL. When the URL is there, it should be presented to the user after logout for - * Relying Party (RP) initiated logout on their account page. + * * @param userInitiated if false, the logout came from the HS, no request will be made and the session entry will be kept in the store. * @param ignoreSdkError if true, the SDK will ignore any error and delete the session data anyway. */ - suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? + suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) /** * Retrieve the user profile, will also eventually emit a new value to [userProfile]. @@ -99,11 +98,11 @@ interface MatrixClient : Closeable { fun roomMembershipObserver(): RoomMembershipObserver /** - * Get a room summary flow for a given room ID or alias. - * The flow will emit a new value whenever the room summary is updated. + * Get a room info flow for a given room ID. + * The flow will emit a new value whenever the room info is updated. * The flow will emit Optional.empty item if the room is not found. */ - fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow> + fun getRoomInfoFlow(roomId: RoomId): Flow> fun isMe(userId: UserId?) = userId == sessionId @@ -144,7 +143,11 @@ interface MatrixClient : Closeable { * Execute generic GET requests through the SDKs internal HTTP client. */ suspend fun getUrl(url: String): Result - suspend fun getRoomPreviewInfo(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result + + /** + * Get a room preview for a given room ID or alias. This is especially useful for rooms that the user is not a member of, or hasn't joined yet. + */ + suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result /** * Returns the currently used sliding sync version. @@ -158,25 +161,25 @@ interface MatrixClient : Closeable { fun canDeactivateAccount(): Boolean suspend fun deactivateAccount(password: String, eraseData: Boolean): Result -} -/** - * Get a room info flow for a given room ID or alias. - * The flow will emit a new value whenever the room info is updated. - * The flow will emit Optional.empty item if the room is not found. - */ -fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow> { - return getRoomSummaryFlow(roomIdOrAlias) - .map { roomSummary -> roomSummary.map { it.info } } - .distinctUntilChanged() + /** + * Check if the user can report a room. + */ + suspend fun canReportRoom(): Boolean + + /** + * Return true if Livekit Rtc is supported, i.e. if Element Call is available. + */ + suspend fun isLivekitRtcSupported(): Boolean } /** - * Returns a room alias from a room alias name. + * Returns a room alias from a room alias name, or null if the name is not valid. * @param name the room alias name ie. the local part of the room alias. */ -fun MatrixClient.roomAliasFromName(name: String): Result { - return runCatching { - RoomAlias("#$name:${userIdServerName()}") - } +fun MatrixClient.roomAliasFromName(name: String): RoomAlias? { + return name.takeIf { it.isNotEmpty() } + ?.let { "#$it:${userIdServerName()}" } + ?.takeIf { MatrixPatterns.isRoomAlias(it) } + ?.let { tryOrNull { RoomAlias(it) } } } 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 ebded7fbe97..ec5a3d84318 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 @@ -8,24 +8,24 @@ package io.element.android.libraries.matrix.api.analytics import im.vector.app.features.analytics.plan.ViewRoom -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom -fun MatrixRoom.toAnalyticsViewRoom( +fun BaseRoom.toAnalyticsViewRoom( trigger: ViewRoom.Trigger? = null, - selectedSpace: MatrixRoom? = null, + selectedSpace: BaseRoom? = null, viaKeyboard: Boolean? = null, ): ViewRoom { val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home return ViewRoom( - isDM = isDirect, - isSpace = isSpace, + isDM = info().isDirect, + isSpace = info().isSpace, trigger = trigger, activeSpace = activeSpace, viaKeyboard = viaKeyboard ) } -private fun MatrixRoom.toActiveSpace(): ViewRoom.ActiveSpace { - return if (isPublic) ViewRoom.ActiveSpace.Public else ViewRoom.ActiveSpace.Private +private fun BaseRoom.toActiveSpace(): ViewRoom.ActiveSpace { + return if (info().isPublic == true) ViewRoom.ActiveSpace.Public else ViewRoom.ActiveSpace.Private } 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 e3153cbbb84..ef73edfaf50 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 @@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.auth sealed class AuthenticationException(message: String) : Exception(message) { class InvalidServerName(message: String) : AuthenticationException(message) class SlidingSyncVersion(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 d423c231b0f..d1c47ae663c 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 @@ -43,7 +43,10 @@ interface MatrixAuthenticationService { /** * Get the Oidc url to display to the user. */ - suspend fun getOidcUrl(prompt: OidcPrompt): Result + suspend fun getOidcUrl( + prompt: OidcPrompt, + loginHint: String?, + ): Result /** * Cancel Oidc login sequence. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt index 23d55b74ea0..f8434a9d4a0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt @@ -7,6 +7,22 @@ package io.element.android.libraries.matrix.api.auth +import io.element.android.libraries.matrix.api.BuildConfig + object OidcConfig { - const val REDIRECT_URI = "io.element:/callback" + const val CLIENT_URI = BuildConfig.CLIENT_URI + + // Note: host must match with the host of CLIENT_URI + const val LOGO_URI = BuildConfig.LOGO_URI + + // Note: host must match with the host of CLIENT_URI + const val TOS_URI = BuildConfig.TOS_URI + + // Note: host must match with the host of CLIENT_URI + const val POLICY_URI = BuildConfig.POLICY_URI + + // Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually + val STATIC_REGISTRATIONS = mapOf( + "https://id.thirdroom.io/realms/thirdroom" to "elementx", + ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt index 86c9e4e825b..79576e0adb8 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt @@ -8,34 +8,12 @@ package io.element.android.libraries.matrix.api.auth sealed interface OidcPrompt { - /** - * The Authorization Server must not display any authentication or consent - * user interface pages. - */ - data object None : OidcPrompt - /** * The Authorization Server should prompt the End-User for * reauthentication. */ data object Login : OidcPrompt - /** - * The Authorization Server should prompt the End-User for consent before - * returning information to the Client. - */ - data object Consent : OidcPrompt - - /** - * The Authorization Server should prompt the End-User to select a user - * account. - * - * This enables an End-User who has multiple accounts at the Authorization - * Server to select amongst the multiple accounts that they might have - * current sessions for. - */ - data object SelectAccount : OidcPrompt - /** * The Authorization Server should prompt the End-User to create a user * account. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcRedirectUrlProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcRedirectUrlProvider.kt new file mode 100644 index 00000000000..1dba996ea94 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcRedirectUrlProvider.kt @@ -0,0 +1,12 @@ +/* + * 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.matrix.api.auth + +interface OidcRedirectUrlProvider { + fun provide(): String +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt index 7a01b8f586f..48532e44b96 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt @@ -7,4 +7,6 @@ package io.element.android.libraries.matrix.api.auth.qrlogin -interface MatrixQrCodeLoginData +interface MatrixQrCodeLoginData { + fun serverName(): String? +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt deleted file mode 100644 index 26422b65bdc..00000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.api.call - -import io.element.android.libraries.matrix.api.MatrixClient - -interface ElementCallBaseUrlProvider { - suspend fun provides(matrixClient: MatrixClient): String? -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt index 431a49fbead..3a4aaeea4ff 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.api.createroom import io.element.android.libraries.matrix.api.core.UserId +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.roomdirectory.RoomVisibility import java.util.Optional @@ -22,5 +23,6 @@ data class CreateRoomParameters( val invite: List? = null, val avatar: String? = null, val joinRuleOverride: JoinRule? = null, + val historyVisibilityOverride: RoomHistoryVisibility? = null, val roomAliasName: Optional = Optional.empty(), ) 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 bc009d3ee89..1635cce375d 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 @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.api.encryption import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -66,6 +67,20 @@ interface EncryptionService { * Remember this identity, ensuring it does not result in a pin violation. */ suspend fun pinUserIdentity(userId: UserId): Result + + /** + * Withdraw the verification for that user (also pin the identity). + * + * Useful when a user that was verified is not anymore, but it is not + * possible to re-verify immediately. This allows to restore communication by reverting the + * user trust from verified to TOFU verified. + */ + suspend fun withdrawVerification(userId: UserId): Result + + /** + * Get the identity state of a user, if known. + */ + suspend fun getUserIdentity(userId: UserId): Result } /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt index d05b8618e32..d565c473360 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt @@ -32,3 +32,5 @@ enum class IdentityState { */ VerificationViolation, } + +fun IdentityState.isAViolation() = this == IdentityState.PinViolation || this == IdentityState.VerificationViolation diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt index fcafe08569d..56497e8fa6a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt @@ -7,9 +7,10 @@ package io.element.android.libraries.matrix.api.exception -sealed class ClientException(message: String) : Exception(message) { - class Generic(message: String) : ClientException(message) - class Other(message: String) : ClientException(message) +sealed class ClientException(message: String, val details: String?) : Exception(message) { + class Generic(message: String, details: String?) : ClientException(message, details) + class MatrixApi(val kind: ErrorKind, val code: String, message: String, details: String?) : ClientException(message, details) + class Other(message: String) : ClientException(message, null) } fun ClientException.isNetworkError(): Boolean { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ErrorKind.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ErrorKind.kt new file mode 100644 index 00000000000..8ca7ce55798 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ErrorKind.kt @@ -0,0 +1,457 @@ +/* + * 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.matrix.api.exception + +sealed interface ErrorKind { + /** + * M_BAD_ALIAS + * + * One or more room aliases within the m.room.canonical_alias event do + * not point to the room ID for which the state event is to be sent to. + * + * room aliases: https://spec.matrix.org/latest/client-server-api/#room-aliases + */ + data object BadAlias : ErrorKind + + /** + * M_BAD_JSON + * + * The request contained valid JSON, but it was malformed in some way, e.g. + * missing required keys, invalid values for keys. + */ + data object BadJson : ErrorKind + + /** + * M_BAD_STATE + * + * The state change requested cannot be performed, such as attempting to + * unban a user who is not banned. + */ + data object BadState : ErrorKind + + /** + * M_BAD_STATUS + * + * The application service returned a bad status. + */ + data class BadStatus( + /** + * The HTTP status code of the response. + */ + val status: Int?, + /** + * The body of the response. + */ + val body: String? + ) : ErrorKind + + /** + * M_CANNOT_LEAVE_SERVER_NOTICE_ROOM + * + * The user is unable to reject an invite to join the server notices + * room. + * + * server notices: https://spec.matrix.org/latest/client-server-api/#server-notices + */ + data object CannotLeaveServerNoticeRoom : ErrorKind + + /** + * M_CANNOT_OVERWRITE_MEDIA + * + * The create_content_async endpoint was called with a media ID that + * already has content. + * + */ + data object CannotOverwriteMedia : ErrorKind + + /** + * M_CAPTCHA_INVALID + * + * The Captcha provided did not match what was expected. + */ + data object CaptchaInvalid : ErrorKind + + /** + * M_CAPTCHA_NEEDED + * + * A Captcha is required to complete the request. + */ + data object CaptchaNeeded : ErrorKind + + /** + * M_CONNECTION_FAILED + * + * The connection to the application service failed. + */ + data object ConnectionFailed : ErrorKind + + /** + * M_CONNECTION_TIMEOUT + * + * The connection to the application service timed out. + */ + data object ConnectionTimeout : ErrorKind + + /** + * M_DUPLICATE_ANNOTATION + * + * The request is an attempt to send a duplicate annotation. + * + * duplicate annotation: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations + */ + data object DuplicateAnnotation : ErrorKind + + /** + * M_EXCLUSIVE + * + * The resource being requested is reserved by an application service, or + * the application service making the request has not created the + * resource. + */ + data object Exclusive : ErrorKind + + /** + * M_FORBIDDEN + * + * Forbidden access, e.g. joining a room without permission, failed login. + */ + data object Forbidden : ErrorKind + + /** + * M_GUEST_ACCESS_FORBIDDEN + * + * The room or resource does not permit guests to access it. + * + * guests: https://spec.matrix.org/latest/client-server-api/#guest-access + */ + data object GuestAccessForbidden : ErrorKind + + /** + * M_INCOMPATIBLE_ROOM_VERSION + * + * The client attempted to join a room that has a version the server does + * not support. + */ + data class IncompatibleRoomVersion( + /** + * The room's version. + */ + val roomVersion: String + ) : ErrorKind + + /** + * M_INVALID_PARAM + * + * A parameter that was specified has the wrong value. For example, the + * server expected an integer and instead received a string. + */ + data object InvalidParam : ErrorKind + + /** + * M_INVALID_ROOM_STATE + * + * The initial state implied by the parameters to the create_room + * request is invalid, e.g. the user's power_level is set below that + * necessary to set the room name. + * + */ + data object InvalidRoomState : ErrorKind + + /** + * M_INVALID_USERNAME + * + * The desired user name is not valid. + */ + data object InvalidUsername : ErrorKind + + /** + * M_LIMIT_EXCEEDED + * + * The request has been refused due to rate limiting: too many requests + * have been sent in a short period of time. + * + * rate limiting: https://spec.matrix.org/latest/client-server-api/#rate-limiting + */ + data class LimitExceeded( + /** + * How long a client should wait before they can try again. + */ + val retryAfterMs: Long? + ) : ErrorKind + + /** + * M_MISSING_PARAM + * + * A required parameter was missing from the request. + */ + data object MissingParam : ErrorKind + + /** + * M_MISSING_TOKEN + * + * No access token was specified for the request, but one is required. + * + * access token: https://spec.matrix.org/latest/client-server-api/#client-authentication + */ + data object MissingToken : ErrorKind + + /** + * M_NOT_FOUND + * + * No resource was found for this request. + */ + data object NotFound : ErrorKind + + /** + * M_NOT_JSON + * + * The request did not contain valid JSON. + */ + data object NotJson : ErrorKind + + /** + * M_NOT_YET_UPLOADED + * + * An mxc URI generated was used and the content is not yet available. + * + */ + data object NotYetUploaded : ErrorKind + + /** + * M_RESOURCE_LIMIT_EXCEEDED + * + * The request cannot be completed because the homeserver has reached a + * resource limit imposed on it. For example, a homeserver held in a + * shared hosting environment may reach a resource limit if it starts + * using too much memory or disk space. + */ + data class ResourceLimitExceeded( + /** + * A URI giving a contact method for the server administrator. + */ + val adminContact: String + ) : ErrorKind + + /** + * M_ROOM_IN_USE + * + * The room alias specified in the request is already taken. + * + * room alias: https://spec.matrix.org/latest/client-server-api/#room-aliases + */ + data object RoomInUse : ErrorKind + + /** + * M_SERVER_NOT_TRUSTED + * + * The client's request used a third-party server, e.g. identity server, + * that this server does not trust. + */ + data object ServerNotTrusted : ErrorKind + + /** + * M_THREEPID_AUTH_FAILED + * + * Authentication could not be performed on the third-party identifier. + * + * third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information + */ + data object ThreepidAuthFailed : ErrorKind + + /** + * M_THREEPID_DENIED + * + * The server does not permit this third-party identifier. This may + * happen if the server only permits, for example, email addresses from + * a particular domain. + * + * third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information + */ + data object ThreepidDenied : ErrorKind + + /** + * M_THREEPID_IN_USE + * + * The third-party identifier is already in use by another user. + * + * third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information + */ + data object ThreepidInUse : ErrorKind + + /** + * M_THREEPID_MEDIUM_NOT_SUPPORTED + * + * The homeserver does not support adding a third-party identifier of the + * given medium. + * + * third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information + */ + data object ThreepidMediumNotSupported : ErrorKind + + /** + * M_THREEPID_NOT_FOUND + * + * No account matching the given third-party identifier could be found. + * + * third-party identifier: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information + */ + data object ThreepidNotFound : ErrorKind + + /** + * M_TOO_LARGE + * + * The request or entity was too large. + */ + data object TooLarge : ErrorKind + + /** + * M_UNABLE_TO_AUTHORISE_JOIN + * + * The room is restricted and none of the conditions can be validated by + * the homeserver. This can happen if the homeserver does not know + * about any of the rooms listed as conditions, for example. + * + * restricted: https://spec.matrix.org/latest/client-server-api/#restricted-rooms + */ + data object UnableToAuthorizeJoin : ErrorKind + + /** + * M_UNABLE_TO_GRANT_JOIN + * + * A different server should be attempted for the join. This is typically + * because the resident server can see that the joining user satisfies + * one or more conditions, such as in the case of restricted rooms, + * but the resident server would be unable to meet the authorization + * rules. + * + * restricted rooms: https://spec.matrix.org/latest/client-server-api/#restricted-rooms + */ + data object UnableToGrantJoin : ErrorKind + + /** + * M_UNAUTHORIZED + * + * The request was not correctly authorized. Usually due to login failures. + */ + data object Unauthorized : ErrorKind + + /** + * M_UNKNOWN + * + * An unknown error has occurred. + */ + data object Unknown : ErrorKind + + /** + * M_UNKNOWN_TOKEN + * + * The access or refresh token specified was not recognized. + * + * access or refresh token: https://spec.matrix.org/latest/client-server-api/#client-authentication + */ + data class UnknownToken( + /** + * If this is true, the client is in a "soft logout" state, i.e. + * the server requires re-authentication but the session is not + * invalidated. The client can acquire a new access token by + * specifying the device ID it is already using to the login API. + * + * soft logout: https://spec.matrix.org/latest/client-server-api/#soft-logout + */ + val softLogout: Boolean + ) : ErrorKind + + /** + * M_UNRECOGNIZED + * + * The server did not understand the request. + * + * This is expected to be returned with a 404 HTTP status code if the + * endpoint is not implemented or a 405 HTTP status code if the + * endpoint is implemented, but the incorrect HTTP method is used. + */ + data object Unrecognized : ErrorKind + + /** + * M_UNSUPPORTED_ROOM_VERSION + * + * The request to create_room used a room version that the server does + * not support. + * + */ + data object UnsupportedRoomVersion : ErrorKind + + /** + * M_URL_NOT_SET + * + * The application service doesn't have a URL configured. + */ + data object UrlNotSet : ErrorKind + + /** + * M_USER_DEACTIVATED + * + * The user ID associated with the request has been deactivated. + */ + data object UserDeactivated : ErrorKind + + /** + * M_USER_IN_USE + * + * The desired user ID is already taken. + */ + data object UserInUse : ErrorKind + + /** + * M_USER_LOCKED + * + * The account has been locked and cannot be used at this time. + * + * locked: https://spec.matrix.org/latest/client-server-api/#account-locking + */ + data object UserLocked : ErrorKind + + /** + * M_USER_SUSPENDED + * + * The account has been suspended and can only be used for limited + * actions at this time. + * + * suspended: https://spec.matrix.org/latest/client-server-api/#account-suspension + */ + data object UserSuspended : ErrorKind + + /** + * M_WEAK_PASSWORD + * + * The password was rejected by the server for being too weak. + * + * rejected: https://spec.matrix.org/latest/client-server-api/#notes-on-password-management + */ + data object WeakPassword : ErrorKind + + /** + * M_WRONG_ROOM_KEYS_VERSION + * + * The version of the room keys backup provided in the request does not + * match the current backup version. + * + * room keys backup: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups + */ + data class WrongRoomKeysVersion( + /** + * The currently active backup version. + */ + val currentVersion: String? + ) : ErrorKind + + /** + * A custom API error. + */ + data class Custom(val errcode: String) : ErrorKind +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt new file mode 100644 index 00000000000..66a53b3ad33 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt @@ -0,0 +1,26 @@ +/* + * 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.matrix.api.media + +/** + * Configuration for media preview ie. invite avatars and timeline media. + */ +data class MediaPreviewConfig( + val mediaPreviewValue: MediaPreviewValue, + val hideInviteAvatar: Boolean, +) { + companion object { + /** + * The default config if unknown (no local nor server config). + */ + val DEFAULT = MediaPreviewConfig( + mediaPreviewValue = MediaPreviewValue.On, + hideInviteAvatar = false + ) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt new file mode 100644 index 00000000000..dffa2d25e4f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt @@ -0,0 +1,35 @@ +/* + * 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.matrix.api.media + +import kotlinx.coroutines.flow.StateFlow + +interface MediaPreviewService { + /** + * Will fetch the media preview config from the server. + */ + suspend fun fetchMediaPreviewConfig(): Result + + /** + * Will emit the media preview config known by the client. + * This will emit a new value when received from sync. + */ + val mediaPreviewConfigFlow: StateFlow + + /** + * Set the media preview display policy. This will update the value on the server and update the local value when successful. + */ + suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result + + /** + * Set the invite avatars display policy. This will update the value on the server and update the local value when successful. + */ + suspend fun setHideInviteAvatars(hide: Boolean): Result +} + +fun MediaPreviewService.getMediaPreviewValue() = mediaPreviewConfigFlow.value.mediaPreviewValue diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewValue.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewValue.kt new file mode 100644 index 00000000000..25d489a48c1 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewValue.kt @@ -0,0 +1,40 @@ +/* + * 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.matrix.api.media + +import io.element.android.libraries.matrix.api.media.MediaPreviewValue.Off +import io.element.android.libraries.matrix.api.media.MediaPreviewValue.On +import io.element.android.libraries.matrix.api.media.MediaPreviewValue.Private +import io.element.android.libraries.matrix.api.room.join.JoinRule + +/** + * Represents the values for media preview settings. + * - [On] means that media preview are enabled + * - [Off] means that media preview are disabled + * - [Private] means that media preview are enabled only for private chats. + */ +enum class MediaPreviewValue { + On, + Off, + Private +} + +fun MediaPreviewValue.isPreviewEnabled(joinRule: JoinRule?): Boolean { + return when (this) { + On -> true + Off -> false + Private -> when (joinRule) { + is JoinRule.Private, + is JoinRule.Knock, + is JoinRule.Invite, + is JoinRule.Restricted, + is JoinRule.KnockRestricted -> true + else -> false + } + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 915e1584a94..f90b70171ee 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -9,12 +9,16 @@ package io.element.android.libraries.matrix.api.notification 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.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.MessageType data class NotificationData( + val sessionId: SessionId, val eventId: EventId, + val threadId: ThreadId?, val roomId: RoomId, // mxc url val senderAvatarUrl: String?, @@ -64,6 +68,7 @@ sealed interface NotificationContent { ) : MessageLike data object RoomEncrypted : MessageLike + data object UnableToResolve : MessageLike data class RoomMessage( val senderId: UserId, val messageType: MessageType @@ -104,7 +109,7 @@ sealed interface NotificationContent { data object RoomServerAcl : StateEvent data object RoomThirdPartyInvite : StateEvent data object RoomTombstone : StateEvent - data object RoomTopic : StateEvent + data class RoomTopic(val topic: String) : StateEvent data object SpaceChild : StateEvent data object SpaceParent : StateEvent } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt index f229a46677a..1e1c8b7fb77 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt @@ -11,5 +11,5 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId interface NotificationService { - suspend fun getNotification(roomId: RoomId, eventId: EventId): Result + suspend fun getNotifications(ids: Map>): Result> } 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 3e7266c444d..703ec1205e8 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 @@ -8,14 +8,14 @@ package io.element.android.libraries.matrix.api.notificationsettings import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState import kotlinx.coroutines.flow.SharedFlow interface NotificationSettingsService { /** - * State of the current room notification settings flow ([MatrixRoomNotificationSettingsState.Unknown] if not started). + * State of the current room notification settings flow ([RoomNotificationSettingsState.Unknown] if not started). */ val notificationSettingsChangeFlow: SharedFlow suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt index 6057656091c..f51856db337 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt @@ -12,6 +12,7 @@ package io.element.android.libraries.matrix.api.permalink * element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks * or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org) * or client permalinks (e.g. user/@chagai95:matrix.org) + * or matrix: permalinks (e.g. matrix:u/chagai95:matrix.org) */ interface PermalinkParser { /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/platform/InitPlatformService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/platform/InitPlatformService.kt new file mode 100644 index 00000000000..6199d1704d2 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/platform/InitPlatformService.kt @@ -0,0 +1,21 @@ +/* + * 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.matrix.api.platform + +import io.element.android.libraries.matrix.api.tracing.TracingConfiguration + +/** + * This service is responsible for initializing the platform-related settings of the SDK. + */ +interface InitPlatformService { + /** + * Initialize the platform-related settings of the SDK. + * @param tracingConfiguration the tracing configuration to use for logging. + */ + fun init(tracingConfiguration: TracingConfiguration) +} 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 new file mode 100644 index 00000000000..7e902a66faf --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -0,0 +1,248 @@ +/* + * 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. + */ + +package io.element.android.libraries.matrix.api.room + +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.core.ThreadId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import java.io.Closeable + +/** + * This interface represents the common functionality for a local room, whether it's joined, invited, knocked, or left. + */ +interface BaseRoom : Closeable { + /** + * The session id of the current user. + */ + val sessionId: SessionId + + /** + * The id of the room. + */ + val roomId: RoomId + + /** + * The coroutine scope that will handle all jobs related to this room. + */ + val roomCoroutineScope: CoroutineScope + + /** + * The current loaded members as a StateFlow. + * Initial value is [RoomMembersState.Unknown]. + * To update them you should call [updateMembers]. + */ + val membersStateFlow: StateFlow + + /** + * A flow that emits the current [RoomInfo] state. + */ + val roomInfoFlow: StateFlow + + /** + * Get the latest room info we have received from the SDK stream. + */ + fun info(): RoomInfo = roomInfoFlow.value + + fun predecessorRoom(): PredecessorRoom? + + /** + * A one-to-one is a room with exactly 2 members. + * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). + */ + val isOneToOne: Boolean get() = info().activeMembersCount == 2L + + /** + * Try to load the room members and update the membersFlow. + */ + suspend fun updateMembers() + + /** + * Get the members of the room. Note: generally this should not be used, please use + * [membersStateFlow] and [updateMembers] instead. + */ + suspend fun getMembers(limit: Int = 5): Result> + + /** + * Will return an updated member or an error. + */ + suspend fun getUpdatedMember(userId: UserId): Result + + /** + * Adds the room to the sync subscription list. + */ + suspend fun subscribeToSync() + + /** + * Gets the power levels of the room. + */ + suspend fun powerLevels(): Result + + /** + * Gets the role of the user with the provided [userId] in the room. + */ + suspend fun userRole(userId: UserId): Result + + /** + * Gets the display name of the user with the provided [userId] in the room. + */ + suspend fun userDisplayName(userId: UserId): Result + + /** + * Gets the avatar of the user with the provided [userId] in the room. + */ + suspend fun userAvatarUrl(userId: UserId): Result + + /** + * Leaves and forgets the room. Only joined, invited or knocked rooms can be left. + */ + suspend fun leave(): Result + + /** + * Joins the room. Only invited rooms can be joined. + */ + suspend fun join(): Result + + /** + * Forgets about the room, removing it from the server and the local cache. Only left and banned rooms can be forgotten. + */ + suspend fun forget(): Result + + /** + * Returns `true` if the user with the provided [userId] can invite other users to the room. + */ + suspend fun canUserInvite(userId: UserId): Result + + /** + * Returns `true` if the user with the provided [userId] can kick other users from the room. + */ + suspend fun canUserKick(userId: UserId): Result + + /** + * Returns `true` if the user with the provided [userId] can ban other users from the room. + */ + suspend fun canUserBan(userId: UserId): Result + + /** + * Returns `true` if the user with the provided [userId] can redact their own messages. + */ + suspend fun canUserRedactOwn(userId: UserId): Result + + /** + * Returns `true` if the user with the provided [userId] can redact messages from other users. + */ + suspend fun canUserRedactOther(userId: UserId): Result + + /** + * Returns `true` if the user with the provided [userId] can send state events. + */ + suspend fun canUserSendState(userId: UserId, type: StateEventType): Result + + /** + * Returns `true` if the user with the provided [userId] can send messages. + */ + suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result + + /** + * Returns `true` if the user with the provided [userId] can trigger an `@room` notification. + */ + suspend fun canUserTriggerRoomNotification(userId: UserId): Result + + /** + * Returns `true` if the user with the provided [userId] can pin or unpin messages. + */ + suspend fun canUserPinUnpin(userId: UserId): Result + + /** + * Returns `true` if the user with the provided [userId] can join or starts calls. + */ + suspend fun canUserJoinCall(userId: UserId): Result = + canUserSendState(userId, StateEventType.CALL_MEMBER) + + /** + * Sets the room as favorite or not, based on the [isFavorite] parameter. + */ + suspend fun setIsFavorite(isFavorite: Boolean): Result + + /** + * Mark the room as read by trying to attach an unthreaded read receipt to the latest room event. + * @param receiptType The type of receipt to send. + */ + suspend fun markAsRead(receiptType: ReceiptType): Result + + /** + * Sets a flag on the room to indicate that the user has explicitly marked it as unread, or reverts the flag. + * @param isUnread true to mark the room as unread, false to remove the flag. + */ + suspend fun setUnreadFlag(isUnread: Boolean): Result + + /** + * Clear the event cache storage for the current room. + */ + suspend fun clearEventCacheStorage(): Result + + /** + * Get the permalink for the room. + */ + suspend fun getPermalink(): Result + + /** + * Get the permalink for the provided [eventId]. + * @param eventId The event id to get the permalink for. + * @return The permalink, or a failure. + */ + suspend fun getPermalinkFor(eventId: EventId): Result + + /** + * Returns the visibility for this room in the room directory. + * If the room is not published, the result will be [RoomVisibility.Private]. + */ + suspend fun getRoomVisibility(): Result + + /** + * Returns the visibility for this room in the room directory, fetching it from the homeserver if needed. + */ + suspend fun getUpdatedIsEncrypted(): Result + + /** + * Store the given `ComposerDraft` in the state store of this room. + */ + suspend fun saveComposerDraft(composerDraft: ComposerDraft, threadRoot: ThreadId?): Result + + /** + * Retrieve the `ComposerDraft` stored in the state store for this room. + */ + suspend fun loadComposerDraft(threadRoot: ThreadId?): Result + + /** + * Clear the `ComposerDraft` stored in the state store for this room. + */ + suspend fun clearComposerDraft(threadRoot: ThreadId?): Result + + /** + * Reports a room as inappropriate to the server. + * The caller is not required to be joined to the room to report it. + * @param reason - The reason the room is being reported. + */ + suspend fun reportRoom(reason: String?): Result + + /** + * Destroy the room and release all resources associated to it. + */ + fun destroy() + + override fun close() = destroy() +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt new file mode 100644 index 00000000000..0bcfb0bf541 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CreateTimelineParams.kt @@ -0,0 +1,17 @@ +/* + * 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.matrix.api.room + +import io.element.android.libraries.matrix.api.core.EventId + +sealed interface CreateTimelineParams { + data class Focused(val focusedEventId: EventId) : CreateTimelineParams + data object MediaOnly : CreateTimelineParams + data class MediaOnlyFocused(val focusedEventId: EventId) : CreateTimelineParams + data object PinnedOnly : CreateTimelineParams +} 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 new file mode 100644 index 00000000000..e6d17e27a59 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt @@ -0,0 +1,184 @@ +/* + * 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.matrix.api.room + +import io.element.android.libraries.matrix.api.core.DeviceId +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.SendHandle +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange +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.knock.KnockRequest +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import io.element.android.libraries.matrix.api.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 kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface JoinedRoom : BaseRoom { + val syncUpdateFlow: StateFlow + + val roomTypingMembersFlow: Flow> + val identityStateChangesFlow: Flow> + val roomNotificationSettingsStateFlow: StateFlow + + /** + * The current knock requests in the room as a Flow. + */ + val knockRequestsFlow: Flow> + + /** + * The live timeline of the room. Must be used to send Event to a room. + */ + val liveTimeline: Timeline + + /** + * Create a new timeline. + * @param createTimelineParams contains parameters about how to filter the timeline. Will also configure the date separators. + */ + suspend fun createTimeline( + createTimelineParams: CreateTimelineParams, + ): Result + + suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List): Result + + /** + * Send a typing notification. + * @param isTyping True if the user is typing, false otherwise. + */ + suspend fun typingNotice(isTyping: Boolean): Result + + suspend fun inviteUserById(id: UserId): Result + + suspend fun updateAvatar(mimeType: String, data: ByteArray): Result + + suspend fun removeAvatar(): Result + + suspend fun updateRoomNotificationSettings(): Result + + /** + * Update the canonical alias of the room. + * + * Note that publishing the alias in the room directory is done separately. + */ + suspend fun updateCanonicalAlias( + canonicalAlias: RoomAlias?, + alternativeAliases: List + ): Result + + /** + * Update the room's visibility in the room directory. + */ + suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result + + /** + * Update room history visibility for this room. + */ + suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result + + /** + * Publish a new room alias for this room in the room directory. + * + * Returns: + * - `true` if the room alias didn't exist and it's now published. + * - `false` if the room alias was already present so it couldn't be + * published. + */ + suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result + + /** + * Remove an existing room alias for this room in the room directory. + * + * Returns: + * - `true` if the room alias was present and it's now removed from the + * room directory. + * - `false` if the room alias didn't exist so it couldn't be removed. + */ + suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result + + /** + * Enable End-to-end encryption in this room. + */ + suspend fun enableEncryption(): Result + + /** + * Update the join rule for this room. + */ + suspend fun updateJoinRule(joinRule: JoinRule): Result + + suspend fun updateUsersRoles(changes: List): Result + + suspend fun updatePowerLevels(roomPowerLevelsValues: RoomPowerLevelsValues): Result + + suspend fun resetPowerLevels(): Result + + suspend fun setName(name: String): Result + + suspend fun setTopic(topic: String): Result + + suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result + + suspend fun kickUser(userId: UserId, reason: String? = null): Result + + suspend fun banUser(userId: UserId, reason: String? = null): Result + + suspend fun unbanUser(userId: UserId, reason: String? = null): Result + + /** + * Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters. + * @param widgetSettings The widget settings to use. + * @param clientId The client id to use. It should be unique per app install. + * @param languageTag The language tag to use. If null, the default language will be used. + * @param theme The theme to use. If null, the default theme will be used. + * @return The resulting url, or a failure. + */ + suspend fun generateWidgetWebViewUrl( + widgetSettings: MatrixWidgetSettings, + clientId: String, + languageTag: String?, + theme: String?, + ): Result + + /** + * Get a [MatrixWidgetDriver] for the provided [widgetSettings]. + * @param widgetSettings The widget settings to use. + * @return The resulting [MatrixWidgetDriver], or a failure. + */ + fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result + + /** + * Send an Element Call started notification if needed. + */ + suspend fun sendCallNotificationIfNeeded(): Result + + suspend fun setSendQueueEnabled(enabled: Boolean) + + /** + * Ignore the local trust for the given devices and resend messages that failed to send because said devices are unverified. + * + * @param devices The map of users identifiers to device identifiers received in the error + * @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload. + * + */ + suspend fun ignoreDeviceTrustAndResend(devices: Map>, sendHandle: SendHandle): Result + + /** + * Remove verification requirements for the given users and + * resend messages that failed to send because their identities were no longer verified. + * + * @param userIds The list of users identifiers received in the error. + * @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload. + * + */ + suspend fun withdrawVerificationAndResend(userIds: List, sendHandle: SendHandle): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt deleted file mode 100644 index 1d8d1517163..00000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ /dev/null @@ -1,465 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.matrix.api.room - -import io.element.android.libraries.matrix.api.core.DeviceId -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.ProgressCallback -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.SendHandle -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.TransactionId -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange -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 -import io.element.android.libraries.matrix.api.media.MediaUploadHandler -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.draft.ComposerDraft -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.knock.KnockRequest -import io.element.android.libraries.matrix.api.room.location.AssetType -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels -import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange -import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility -import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId -import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver -import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import java.io.Closeable -import java.io.File - -interface MatrixRoom : Closeable { - val sessionId: SessionId - val roomId: RoomId - val displayName: String - val canonicalAlias: RoomAlias? - val alternativeAliases: List - val topic: String? - val avatarUrl: String? - val isEncrypted: Boolean - val isSpace: Boolean - val isDirect: Boolean - val isPublic: Boolean - val activeMemberCount: Long - val joinedMemberCount: Long - - val roomCoroutineScope: CoroutineScope - - val roomInfoFlow: Flow - val roomTypingMembersFlow: Flow> - val identityStateChangesFlow: Flow> - - /** - * The current knock requests in the room as a Flow. - */ - val knockRequestsFlow: Flow> - - /** - * 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() = activeMemberCount == 2L - - /** - * The current loaded members as a StateFlow. - * Initial value is [MatrixRoomMembersState.Unknown]. - * To update them you should call [updateMembers]. - */ - val membersStateFlow: StateFlow - - val roomNotificationSettingsStateFlow: StateFlow - - /** - * Try to load the room members and update the membersFlow. - */ - suspend fun updateMembers() - - /** - * Get the members of the room. Note: generally this should not be used, please use - * [membersStateFlow] and [updateMembers] instead. - */ - suspend fun getMembers(limit: Int = 5): Result> - - /** - * Will return an updated member or an error. - */ - suspend fun getUpdatedMember(userId: UserId): Result - - suspend fun updateRoomNotificationSettings(): Result - - val syncUpdateFlow: StateFlow - - /** - * The live timeline of the room. Must be used to send Event to a room. - */ - val liveTimeline: Timeline - - /** - * Create a new timeline, focused on the provided Event. - * Should not be used directly, see `TimelineController` to manage the various timelines. - */ - suspend fun timelineFocusedOnEvent(eventId: EventId): Result - - /** - * Create a new timeline for the pinned events of the room. - */ - suspend fun pinnedEventsTimeline(): Result - - /** - * Create a new timeline for the media events of the room. - * @param eventId The event to focus on, if any. - */ - suspend fun mediaTimeline(eventId: EventId?): Result - - fun destroy() - - suspend fun subscribeToSync() - - suspend fun powerLevels(): Result - - suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result - - suspend fun resetPowerLevels(): Result - - suspend fun userRole(userId: UserId): Result - - suspend fun updateUsersRoles(changes: List): Result - - suspend fun userDisplayName(userId: UserId): Result - - suspend fun userAvatarUrl(userId: UserId): Result - - suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List): Result - - suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List): Result - - suspend fun sendImage( - file: File, - thumbnailFile: File?, - imageInfo: ImageInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback? - ): Result - - suspend fun sendVideo( - file: File, - thumbnailFile: File?, - videoInfo: VideoInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback? - ): Result - - suspend fun sendAudio( - file: File, - audioInfo: AudioInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - ): Result - - suspend fun sendFile( - file: File, - fileInfo: FileInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - ): Result - - suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result - - suspend fun forwardEvent(eventId: EventId, roomIds: List): Result - - suspend fun cancelSend(transactionId: TransactionId): Result - - suspend fun leave(): Result - - suspend fun join(): Result - - suspend fun inviteUserById(id: UserId): Result - - suspend fun canUserInvite(userId: UserId): Result - - suspend fun canUserKick(userId: UserId): Result - - suspend fun canUserBan(userId: UserId): Result - - suspend fun canUserRedactOwn(userId: UserId): Result - - suspend fun canUserRedactOther(userId: UserId): Result - - suspend fun canUserSendState(userId: UserId, type: StateEventType): Result - - suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result - - suspend fun canUserTriggerRoomNotification(userId: UserId): Result - - suspend fun canUserPinUnpin(userId: UserId): Result - - suspend fun canUserJoinCall(userId: UserId): Result = - canUserSendState(userId, StateEventType.CALL_MEMBER) - - suspend fun updateAvatar(mimeType: String, data: ByteArray): Result - - suspend fun removeAvatar(): Result - - suspend fun setName(name: String): Result - - suspend fun setTopic(topic: String): Result - - suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result - - suspend fun kickUser(userId: UserId, reason: String? = null): Result - - suspend fun banUser(userId: UserId, reason: String? = null): Result - - suspend fun unbanUser(userId: UserId, reason: String? = null): Result - - suspend fun setIsFavorite(isFavorite: Boolean): Result - - /** - * Mark the room as read by trying to attach an unthreaded read receipt to the latest room event. - * @param receiptType The type of receipt to send. - */ - suspend fun markAsRead(receiptType: ReceiptType): Result - - /** - * Sets a flag on the room to indicate that the user has explicitly marked it as unread, or reverts the flag. - * @param isUnread true to mark the room as unread, false to remove the flag. - * - */ - suspend fun setUnreadFlag(isUnread: Boolean): Result - - /** - * Clear the event cache storage for the current room. - */ - suspend fun clearEventCacheStorage(): Result - - /** - * Share a location message in the room. - * - * @param body A human readable textual representation of the location. - * @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`. - * Respectively: latitude, longitude, and (optional) uncertainty. - * @param description Optional description of the location to display to the user. - * @param zoomLevel Optional zoom level to display the map at. - * @param assetType Optional type of the location asset. - * Set to SENDER if sharing own location. Set to PIN if sharing any location. - */ - suspend fun sendLocation( - body: String, - geoUri: String, - description: String? = null, - zoomLevel: Int? = null, - assetType: AssetType? = null, - ): Result - - /** - * Create a poll in the room. - * - * @param question The question to ask. - * @param answers The list of answers. - * @param maxSelections The maximum number of answers that can be selected. - * @param pollKind The kind of poll to create. - */ - suspend fun createPoll( - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result - - /** - * Edit a poll in the room. - * - * @param pollStartId The event ID of the poll start event. - * @param question The question to ask. - * @param answers The list of answers. - * @param maxSelections The maximum number of answers that can be selected. - * @param pollKind The kind of poll to create. - */ - suspend fun editPoll( - pollStartId: EventId, - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result - - /** - * Send a response to a poll. - * - * @param pollStartId The event ID of the poll start event. - * @param answers The list of answer ids to send. - */ - suspend fun sendPollResponse(pollStartId: EventId, answers: List): Result - - /** - * Ends a poll in the room. - * - * @param pollStartId The event ID of the poll start event. - * @param text Fallback text of the poll end event. - */ - suspend fun endPoll(pollStartId: EventId, text: String): Result - - suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback? - ): Result - - /** - * Send a typing notification. - * @param isTyping True if the user is typing, false otherwise. - */ - suspend fun typingNotice(isTyping: Boolean): Result - - /** - * Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters. - * @param widgetSettings The widget settings to use. - * @param clientId The client id to use. It should be unique per app install. - * @param languageTag The language tag to use. If null, the default language will be used. - * @param theme The theme to use. If null, the default theme will be used. - * @return The resulting url, or a failure. - */ - suspend fun generateWidgetWebViewUrl( - widgetSettings: MatrixWidgetSettings, - clientId: String, - languageTag: String?, - theme: String?, - ): Result - - /** - * Get a [MatrixWidgetDriver] for the provided [widgetSettings]. - * @param widgetSettings The widget settings to use. - * @return The resulting [MatrixWidgetDriver], or a failure. - */ - fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result - - /** - * Get the permalink for the room. - */ - suspend fun getPermalink(): Result - - /** - * Get the permalink for the provided [eventId]. - * @param eventId The event id to get the permalink for. - * @return The permalink, or a failure. - */ - suspend fun getPermalinkFor(eventId: EventId): Result - - /** - * Send an Element Call started notification if needed. - */ - suspend fun sendCallNotificationIfNeeded(): Result - - suspend fun setSendQueueEnabled(enabled: Boolean) - - /** - * Store the given `ComposerDraft` in the state store of this room. - */ - suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result - - /** - * Retrieve the `ComposerDraft` stored in the state store for this room. - */ - suspend fun loadComposerDraft(): Result - - /** - * Clear the `ComposerDraft` stored in the state store for this room. - */ - suspend fun clearComposerDraft(): Result - - /** - * Ignore the local trust for the given devices and resend messages that failed to send because said devices are unverified. - * - * @param devices The map of users identifiers to device identifiers received in the error - * @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload. - * - */ - suspend fun ignoreDeviceTrustAndResend(devices: Map>, sendHandle: SendHandle): Result - - /** - * Remove verification requirements for the given users and - * resend messages that failed to send because their identities were no longer verified. - * - * @param userIds The list of users identifiers received in the error. - * @param sendHandle The send queue handle of the local echo the send error applies to. It can be used to retry the upload. - * - */ - suspend fun withdrawVerificationAndResend(userIds: List, sendHandle: SendHandle): Result - - override fun close() = destroy() - - /** - * Update the canonical alias of the room. - * - * Note that publishing the alias in the room directory is done separately. - */ - suspend fun updateCanonicalAlias( - canonicalAlias: RoomAlias?, - alternativeAliases: List - ): Result - - /** - * Update the room's visibility in the room directory. - */ - suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result - - /** - * Update room history visibility for this room. - */ - suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result - - /** - * Returns the visibility for this room in the room directory. - * If the room is not published, the result will be [RoomVisibility.Private]. - */ - suspend fun getRoomVisibility(): Result - - /** - * Publish a new room alias for this room in the room directory. - * - * Returns: - * - `true` if the room alias didn't exist and it's now published. - * - `false` if the room alias was already present so it couldn't be - * published. - */ - suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result - - /** - * Remove an existing room alias for this room in the room directory. - * - * Returns: - * - `true` if the room alias was present and it's now removed from the - * room directory. - * - `false` if the room alias didn't exist so it couldn't be removed. - */ - suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result - - /** - * Enable End-to-end encryption in this room. - */ - suspend fun enableEncryption(): Result - - /** - * Update the join rule for this room. - */ - suspend fun updateJoinRule(joinRule: JoinRule): Result -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt deleted file mode 100644 index ff525c403d6..00000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.matrix.api.room - -import androidx.compose.runtime.Immutable -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility -import io.element.android.libraries.matrix.api.room.join.JoinRule -import io.element.android.libraries.matrix.api.user.MatrixUser -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.ImmutableMap - -@Immutable -data class MatrixRoomInfo( - val id: RoomId, - /** The room's name from the room state event if received from sync, or one that's been computed otherwise. */ - val name: String?, - /** Room name as defined by the room state event only. */ - val rawName: String?, - val topic: String?, - val avatarUrl: String?, - val isDirect: Boolean, - val isPublic: Boolean, - val joinRule: JoinRule?, - val isSpace: Boolean, - val isTombstoned: Boolean, - val isFavorite: Boolean, - val canonicalAlias: RoomAlias?, - val alternativeAliases: ImmutableList, - val currentUserMembership: CurrentUserMembership, - /** - * Member who invited the current user to a room that's in the invited - * state. - * - * Can be missing if the room membership invite event is missing from the - * store. - */ - val inviter: RoomMember?, - val activeMembersCount: Long, - val invitedMembersCount: Long, - val joinedMembersCount: Long, - val userPowerLevels: ImmutableMap, - val highlightCount: Long, - val notificationCount: Long, - val userDefinedNotificationMode: RoomNotificationMode?, - val hasRoomCall: Boolean, - val activeRoomCallParticipants: ImmutableList, - val isMarkedUnread: Boolean, - /** - * "Interesting" messages received in that room, independently of the - * notification settings. - */ - val numUnreadMessages: Long, - /** - * Events that will notify the user, according to their - * notification settings. - */ - val numUnreadNotifications: Long, - /** - * Events causing mentions/highlights for the user, according to their - * notification settings. - */ - val numUnreadMentions: Long, - val heroes: ImmutableList, - val pinnedEventIds: ImmutableList, - val creator: UserId?, - val historyVisibility: RoomHistoryVisibility, -) { - val aliases: List - get() = listOfNotNull(canonicalAlias) + alternativeAliases -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt deleted file mode 100644 index 860367b2028..00000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.matrix.api.room - -import androidx.compose.runtime.Immutable -import kotlinx.collections.immutable.ImmutableList - -@Immutable -sealed interface MatrixRoomMembersState { - data object Unknown : MatrixRoomMembersState - data class Pending(val prevRoomMembers: ImmutableList? = null) : MatrixRoomMembersState - data class Error(val failure: Throwable, val prevRoomMembers: ImmutableList? = null) : MatrixRoomMembersState - data class Ready(val roomMembers: ImmutableList) : MatrixRoomMembersState -} - -fun MatrixRoomMembersState.roomMembers(): List? { - return when (this) { - is MatrixRoomMembersState.Ready -> roomMembers - is MatrixRoomMembersState.Pending -> prevRoomMembers - is MatrixRoomMembersState.Error -> prevRoomMembers - else -> null - } -} - -fun MatrixRoomMembersState.joinedRoomMembers(): List { - return roomMembers().orEmpty().filter { it.membership == RoomMembershipState.JOIN } -} - -fun MatrixRoomMembersState.activeRoomMembers(): List { - return roomMembers().orEmpty().filter { it.membership.isActive() } -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt deleted file mode 100644 index 05d9abb30b5..00000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.matrix.api.room - -sealed interface MatrixRoomNotificationSettingsState { - data object Unknown : MatrixRoomNotificationSettingsState - data class Pending(val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState - data class Error(val failure: Throwable, val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState - data class Ready(val roomNotificationSettings: RoomNotificationSettings) : MatrixRoomNotificationSettingsState -} - -fun MatrixRoomNotificationSettingsState.roomNotificationSettings(): RoomNotificationSettings? { - return when (this) { - is MatrixRoomNotificationSettingsState.Ready -> roomNotificationSettings - is MatrixRoomNotificationSettingsState.Pending -> prevRoomNotificationSettings - is MatrixRoomNotificationSettingsState.Error -> prevRoomNotificationSettings - else -> null - } -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/NotJoinedRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/NotJoinedRoom.kt new file mode 100644 index 00000000000..b83292e0f24 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/NotJoinedRoom.kt @@ -0,0 +1,21 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.api.room + +import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo + +/** A reference to a room either invited, knocked or banned. */ +interface NotJoinedRoom : AutoCloseable { + val previewInfo: RoomPreviewInfo + val localRoom: BaseRoom? + + /** + * Get the membership details of the user in the room, as well as from the user who sent the `m.room.member` event. + */ + suspend fun membershipDetails(): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/PendingRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/PendingRoom.kt deleted file mode 100644 index 3c7a6c6fe09..00000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/PendingRoom.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.api.room - -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.SessionId - -/** A reference to a room the current user has knocked to or has been invited to, with the ability to leave the room. */ -interface PendingRoom : AutoCloseable { - val sessionId: SessionId - val roomId: RoomId - - /** Leave the room ie.decline invite or cancel knock. */ - suspend fun leave(): 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 new file mode 100644 index 00000000000..261a484fd67 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomInfo.kt @@ -0,0 +1,81 @@ +/* + * 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. + */ + +package io.element.android.libraries.matrix.api.room + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility +import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels +import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +@Immutable +data class RoomInfo( + val id: RoomId, + /** The room's name from the room state event if received from sync, or one that's been computed otherwise. */ + val name: String?, + /** Room name as defined by the room state event only. */ + val rawName: String?, + val topic: String?, + val avatarUrl: String?, + val isPublic: Boolean?, + val isDirect: Boolean, + val isEncrypted: Boolean?, + val joinRule: JoinRule?, + val isSpace: Boolean, + val isFavorite: Boolean, + val canonicalAlias: RoomAlias?, + val alternativeAliases: ImmutableList, + val currentUserMembership: CurrentUserMembership, + /** + * Member who invited the current user to a room that's in the invited + * state. + * + * Can be missing if the room membership invite event is missing from the + * store. + */ + val inviter: RoomMember?, + val activeMembersCount: Long, + val invitedMembersCount: Long, + val joinedMembersCount: Long, + val roomPowerLevels: RoomPowerLevels?, + val highlightCount: Long, + val notificationCount: Long, + val userDefinedNotificationMode: RoomNotificationMode?, + val hasRoomCall: Boolean, + val activeRoomCallParticipants: ImmutableList, + val isMarkedUnread: Boolean, + /** + * "Interesting" messages received in that room, independently of the + * notification settings. + */ + val numUnreadMessages: Long, + /** + * Events that will notify the user, according to their + * notification settings. + */ + val numUnreadNotifications: Long, + /** + * Events causing mentions/highlights for the user, according to their + * notification settings. + */ + val numUnreadMentions: Long, + val heroes: ImmutableList, + val pinnedEventIds: ImmutableList, + val creator: UserId?, + val historyVisibility: RoomHistoryVisibility, + val successorRoom: SuccessorRoom?, +) { + 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 index b1c9787bfa2..4f0bf2dcaec 100644 --- 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 @@ -7,6 +7,8 @@ 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). @@ -19,11 +21,11 @@ fun isDm(isDirect: Boolean, activeMembersCount: Int): Boolean { } /** - * Returns whether the [MatrixRoom] is a DM. + * Returns whether the [BaseRoom] is a DM, with an updated state from the latest [RoomInfo]. */ -val MatrixRoom.isDm get() = isDm(isDirect, activeMemberCount.toInt()) +suspend fun BaseRoom.isDm() = roomInfoFlow.first().isDm /** - * Returns whether the [MatrixRoomInfo] is from a DM. + * Returns whether the [RoomInfo] is from a DM. */ -val MatrixRoomInfo.isDm get() = isDm(isDirect, activeMembersCount.toInt()) +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 758136c3a77..00da7e4137f 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 @@ -20,6 +20,7 @@ data class RoomMember( val normalizedPowerLevel: Long, val isIgnored: Boolean, val role: Role, + val membershipChangeReason: String?, ) { /** * 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 new file mode 100644 index 00000000000..ab857e5d637 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembersState.kt @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package io.element.android.libraries.matrix.api.room + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +sealed interface RoomMembersState { + data object Unknown : RoomMembersState + data class Pending(val prevRoomMembers: ImmutableList? = null) : RoomMembersState + data class Error(val failure: Throwable, val prevRoomMembers: ImmutableList? = null) : RoomMembersState + data class Ready(val roomMembers: ImmutableList) : RoomMembersState +} + +fun RoomMembersState.roomMembers(): List? { + return when (this) { + is RoomMembersState.Ready -> roomMembers + is RoomMembersState.Pending -> prevRoomMembers + is RoomMembersState.Error -> prevRoomMembers + else -> null + } +} + +fun RoomMembersState.joinedRoomMembers(): List { + return roomMembers().orEmpty().filter { it.membership == RoomMembershipState.JOIN } +} + +fun RoomMembersState.activeRoomMembers(): List { + return roomMembers().orEmpty().filter { it.membership.isActive() } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipDetails.kt new file mode 100644 index 00000000000..20d7b0e2ad5 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipDetails.kt @@ -0,0 +1,20 @@ +/* + * 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.matrix.api.room + +/** + * Room membership details for the current user and the sender of the membership event. + * + * It also includes the reason the current user's membership changed, if any. + */ +data class RoomMembershipDetails( + val currentUserMember: RoomMember, + val senderMember: RoomMember?, +) { + val membershipChangeReason: String? = currentUserMember.membershipChangeReason +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt index 66725e5cc12..19d7fdaaf25 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt @@ -22,7 +22,12 @@ class RoomMembershipObserver { private val _updates = MutableSharedFlow(extraBufferCapacity = 10) val updates = _updates.asSharedFlow() - suspend fun notifyUserLeftRoom(roomId: RoomId) { - _updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.LEFT)) + suspend fun notifyUserLeftRoom(roomId: RoomId, membershipBeforeLeft: CurrentUserMembership) { + val membershipChange = when (membershipBeforeLeft) { + CurrentUserMembership.INVITED -> MembershipChange.INVITATION_REJECTED + CurrentUserMembership.KNOCKED -> MembershipChange.KNOCK_RETRACTED + else -> MembershipChange.LEFT + } + _updates.emit(RoomMembershipUpdate(roomId, false, membershipChange)) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettingsState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettingsState.kt new file mode 100644 index 00000000000..15c1003317a --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomNotificationSettingsState.kt @@ -0,0 +1,24 @@ +/* + * 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. + */ + +package io.element.android.libraries.matrix.api.room + +sealed interface RoomNotificationSettingsState { + data object Unknown : RoomNotificationSettingsState + data class Pending(val prevRoomNotificationSettings: RoomNotificationSettings? = null) : RoomNotificationSettingsState + data class Error(val failure: Throwable, val prevRoomNotificationSettings: RoomNotificationSettings? = null) : RoomNotificationSettingsState + data class Ready(val roomNotificationSettings: RoomNotificationSettings) : RoomNotificationSettingsState +} + +fun RoomNotificationSettingsState.roomNotificationSettings(): RoomNotificationSettings? { + return when (this) { + is RoomNotificationSettingsState.Ready -> roomNotificationSettings + is RoomNotificationSettingsState.Pending -> prevRoomNotificationSettings + is RoomNotificationSettingsState.Error -> prevRoomNotificationSettings + else -> null + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt index 973ecc74e21..f9b0de2449b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt @@ -12,21 +12,34 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId /** - * Try to find an existing DM with the given user, or create one if none exists. + * Try to find an existing DM with the given user, or create one if none exists and [createIfDmDoesNotExist] is true. */ -suspend fun MatrixClient.startDM(userId: UserId): StartDMResult { - val existingDM = findDM(userId) - return if (existingDM != null) { - StartDMResult.Success(existingDM, isNew = false) - } else { - createDM(userId).fold( - { StartDMResult.Success(it, isNew = true) }, - { StartDMResult.Failure(it) } +suspend fun MatrixClient.startDM( + userId: UserId, + createIfDmDoesNotExist: Boolean, +): StartDMResult { + return findDM(userId) + .fold( + onSuccess = { existingDM -> + if (existingDM != null) { + StartDMResult.Success(existingDM, isNew = false) + } else if (createIfDmDoesNotExist) { + createDM(userId).fold( + { StartDMResult.Success(it, isNew = true) }, + { StartDMResult.Failure(it) } + ) + } else { + StartDMResult.DmDoesNotExist + } + }, + onFailure = { error -> + StartDMResult.Failure(error) + } ) - } } sealed interface StartDMResult { data class Success(val roomId: RoomId, val isNew: Boolean) : StartDMResult + data object DmDoesNotExist : StartDMResult data class Failure(val throwable: Throwable) : StartDMResult } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt index f9158ae3601..0b3bcdbc1c7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/MatrixRoomAlias.kt @@ -8,18 +8,18 @@ package io.element.android.libraries.matrix.api.room.alias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom /** * Return true if the given roomIdOrAlias is the same room as this room. */ -fun MatrixRoom.matches(roomIdOrAlias: RoomIdOrAlias): Boolean { +fun BaseRoom.matches(roomIdOrAlias: RoomIdOrAlias): Boolean { return when (roomIdOrAlias) { is RoomIdOrAlias.Id -> { roomIdOrAlias.roomId == roomId } is RoomIdOrAlias.Alias -> { - roomIdOrAlias.roomAlias == canonicalAlias || roomIdOrAlias.roomAlias in alternativeAliases + roomIdOrAlias.roomAlias == info().canonicalAlias || roomIdOrAlias.roomAlias in info().alternativeAliases } } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt index cfa29cee6c2..731512b2e9b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt @@ -16,4 +16,8 @@ interface JoinRoom { serverNames: List, trigger: JoinedRoom.Trigger, ): Result + + sealed class Failures : Exception() { + data object UnauthorizedJoin : Failures() + } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/message/ReplyParameters.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/message/ReplyParameters.kt new file mode 100644 index 00000000000..6157989a43b --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/message/ReplyParameters.kt @@ -0,0 +1,22 @@ +/* + * 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.matrix.api.room.message + +import io.element.android.libraries.matrix.api.core.EventId + +data class ReplyParameters( + val inReplyToEventId: EventId, + val enforceThreadReply: Boolean, + val replyWithinThread: Boolean, +) + +fun replyInThread(eventId: EventId, explicitReply: Boolean = false) = ReplyParameters( + inReplyToEventId = eventId, + enforceThreadReply = true, + replyWithinThread = explicitReply, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt index 3206657f12d..eb3afaf994d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.matrix.api.room.powerlevels -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.activeRoomMembers import kotlinx.collections.immutable.ImmutableList @@ -20,9 +20,9 @@ import kotlinx.coroutines.flow.map /** * Return a flow of the list of active room members who have the given role. */ -fun MatrixRoom.usersWithRole(role: RoomMember.Role): Flow> { +fun BaseRoom.usersWithRole(role: RoomMember.Role): Flow> { return roomInfoFlow - .map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } } + .map { it.roomPowerLevels?.users.orEmpty().filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } } .combine(membersStateFlow) { powerLevels, membersState -> membersState.activeRoomMembers() .filter { powerLevels.containsKey(it.userId) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt deleted file mode 100644 index a17d4ad6100..00000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.matrix.api.room.powerlevels - -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MessageEventType -import io.element.android.libraries.matrix.api.room.StateEventType - -data class MatrixRoomPowerLevels( - val ban: Long, - val invite: Long, - val kick: Long, - val sendEvents: Long, - val redactEvents: Long, - val roomName: Long, - val roomAvatar: Long, - val roomTopic: Long, -) - -/** - * Shortcut for calling [MatrixRoom.canUserInvite] with our own user. - */ -suspend fun MatrixRoom.canInvite(): Result = canUserInvite(sessionId) - -/** - * Shortcut for calling [MatrixRoom.canUserKick] with our own user. - */ -suspend fun MatrixRoom.canKick(): Result = canUserKick(sessionId) - -/** - * Shortcut for calling [MatrixRoom.canUserBan] with our own user. - */ -suspend fun MatrixRoom.canBan(): Result = canUserBan(sessionId) - -/** - * Shortcut for calling [MatrixRoom.canUserSendState] with our own user. - */ -suspend fun MatrixRoom.canSendState(type: StateEventType): Result = canUserSendState(sessionId, type) - -/** - * Shortcut for calling [MatrixRoom.canUserSendMessage] with our own user. - */ -suspend fun MatrixRoom.canSendMessage(type: MessageEventType): Result = canUserSendMessage(sessionId, type) - -/** - * Shortcut for calling [MatrixRoom.canUserRedactOwn] with our own user. - */ -suspend fun MatrixRoom.canRedactOwn(): Result = canUserRedactOwn(sessionId) - -/** - * Shortcut for calling [MatrixRoom.canRedactOther] with our own user. - */ -suspend fun MatrixRoom.canRedactOther(): Result = canUserRedactOther(sessionId) - -/** - * Shortcut for checking if current user can handle knock requests. - */ -suspend fun MatrixRoom.canHandleKnockRequests(): Result = runCatching { - canInvite().getOrThrow() || canBan().getOrThrow() || canKick().getOrThrow() -} - -/** - * Shortcut for calling [MatrixRoom.canUserPinUnpin] with our own user. - */ -suspend fun MatrixRoom.canPinUnpin(): Result = canUserPinUnpin(sessionId) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevels.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevels.kt new file mode 100644 index 00000000000..08f2166494b --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevels.kt @@ -0,0 +1,16 @@ +/* + * 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.matrix.api.room.powerlevels + +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.collections.immutable.ImmutableMap + +data class RoomPowerLevels( + val values: RoomPowerLevelsValues, + val users: ImmutableMap, +) 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 new file mode 100644 index 00000000000..0acafc2a438 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevelsValues.kt @@ -0,0 +1,71 @@ +/* + * 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. + */ + +package io.element.android.libraries.matrix.api.room.powerlevels + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.StateEventType + +data class RoomPowerLevelsValues( + val ban: Long, + val invite: Long, + val kick: Long, + val sendEvents: Long, + val redactEvents: Long, + val roomName: Long, + val roomAvatar: Long, + val roomTopic: Long, +) + +/** + * Shortcut for calling [BaseRoom.canUserInvite] with our own user. + */ +suspend fun BaseRoom.canInvite(): Result = canUserInvite(sessionId) + +/** + * Shortcut for calling [BaseRoom.canUserKick] with our own user. + */ +suspend fun BaseRoom.canKick(): Result = canUserKick(sessionId) + +/** + * Shortcut for calling [BaseRoom.canUserBan] with our own user. + */ +suspend fun BaseRoom.canBan(): Result = canUserBan(sessionId) + +/** + * Shortcut for calling [BaseRoom.canUserSendState] with our own user. + */ +suspend fun BaseRoom.canSendState(type: StateEventType): Result = canUserSendState(sessionId, type) + +/** + * Shortcut for calling [BaseRoom.canUserSendMessage] with our own user. + */ +suspend fun BaseRoom.canSendMessage(type: MessageEventType): Result = canUserSendMessage(sessionId, type) + +/** + * Shortcut for calling [BaseRoom.canUserRedactOwn] with our own user. + */ +suspend fun BaseRoom.canRedactOwn(): Result = canUserRedactOwn(sessionId) + +/** + * Shortcut for calling [BaseRoom.canRedactOther] with our own user. + */ +suspend fun BaseRoom.canRedactOther(): Result = canUserRedactOther(sessionId) + +/** + * Shortcut for checking if current user can handle knock requests. + */ +suspend fun BaseRoom.canHandleKnockRequests(): Result = runCatchingExceptions { + canInvite().getOrThrow() || canBan().getOrThrow() || canKick().getOrThrow() +} + +/** + * Shortcut for calling [BaseRoom.canUserPinUnpin] with our own user. + */ +suspend fun BaseRoom.canPinUnpin(): Result = canUserPinUnpin(sessionId) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/preview/RoomPreviewInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/preview/RoomPreviewInfo.kt index e256cf7d0e1..61de6f7d15a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/preview/RoomPreviewInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/preview/RoomPreviewInfo.kt @@ -9,7 +9,9 @@ package io.element.android.libraries.matrix.api.room.preview 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 import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.matrix.api.room.join.JoinRule data class RoomPreviewInfo( /** The room id for this room. */ @@ -28,12 +30,8 @@ data class RoomPreviewInfo( val roomType: RoomType, /** Is the history world-readable for this room? */ val isHistoryWorldReadable: Boolean, - /** Is the room joined by the current user? */ - val isJoined: Boolean, - /** Is the current user invited to this room? */ - val isInvited: Boolean, - /** is the join rule public for this room? */ - val isPublic: Boolean, - /** Can we knock (or restricted-knock) to this room? */ - val canKnock: Boolean, + /** the membership of the current user. */ + val membership: CurrentUserMembership?, + /** The room's join rule. */ + val joinRule: JoinRule?, ) 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 3e1d77fd3cf..e0a51ef0059 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 @@ -10,8 +10,8 @@ package io.element.android.libraries.matrix.api.room.recent 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.BaseRoom import io.element.android.libraries.matrix.api.room.CurrentUserMembership -import io.element.android.libraries.matrix.api.room.MatrixRoom 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 @@ -32,7 +32,7 @@ suspend fun MatrixClient.getRecentDirectRooms( getRecentlyVisitedRooms().getOrNull()?.let { roomIds -> roomIds .mapNotNull { roomId -> getRoom(roomId) } - .filter { it.isDm && it.isJoined() } + .filter { it.isDm() && it.isJoined() } .map { room -> val otherUser = room.getMembers().getOrNull() ?.firstOrNull { it.userId != sessionId } @@ -52,6 +52,6 @@ suspend fun MatrixClient.getRecentDirectRooms( return result } -suspend fun MatrixRoom.isJoined(): Boolean { +suspend fun BaseRoom.isJoined(): Boolean { return roomInfoFlow.first().currentUserMembership == CurrentUserMembership.JOINED } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/PredecessorRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/PredecessorRoom.kt new file mode 100644 index 00000000000..cd54b5a07b6 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/PredecessorRoom.kt @@ -0,0 +1,31 @@ +/* + * 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.matrix.api.room.tombstone + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId + +/** + * + * When a room A is tombstoned, it is replaced by a room B. The room A is the + * predecessor of B, and B is the successor of A. This type holds information + * about the predecessor room. + * + * A room is tombstoned if it has received a m.room.tombstone state event. + * + */ +data class PredecessorRoom( + /** + * The ID of the replaced room. + */ + val roomId: RoomId, + /** + * The event ID of the last known event in the predecessor room. + */ + val lastEventId: EventId, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/SuccessorRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/SuccessorRoom.kt new file mode 100644 index 00000000000..957585a464b --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tombstone/SuccessorRoom.kt @@ -0,0 +1,30 @@ +/* + * 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.matrix.api.room.tombstone + +import io.element.android.libraries.matrix.api.core.RoomId + +/** + * + * When a room A is tombstoned, it is replaced by a room B. The room A is the + * predecessor of B, and B is the successor of A. This type holds information + * about the successor room. + * + * A room is tombstoned if it has received a m.room.tombstone state event. + * + */ +data class SuccessorRoom( + /** + * The ID of the replacement room. + */ + val roomId: RoomId, + /** + * The message explaining why the room has been tombstoned. + */ + val reason: String?, +) 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 099f2d865ce..c4f18510891 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 @@ -7,11 +7,11 @@ package io.element.android.libraries.matrix.api.roomlist -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.message.RoomMessage data class RoomSummary( - val info: MatrixRoomInfo, + val info: RoomInfo, val lastMessage: RoomMessage?, ) { val roomId = info.id diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt index 21554dbbd6c..13cb54b5001 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.matrix.api.sync -import io.element.android.libraries.core.coroutine.mapState import kotlinx.coroutines.flow.StateFlow interface SyncService { @@ -25,6 +24,6 @@ interface SyncService { * Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes. */ val syncState: StateFlow -} -fun SyncService.isOnline(): StateFlow = syncState.mapState { it != SyncState.Offline } + val isOnline: StateFlow +} 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 c74f7b2c00e..00652a4fa92 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 @@ -19,6 +19,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.location.AssetType +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId @@ -49,7 +50,10 @@ interface Timeline : AutoCloseable { val membershipChangeEventReceived: Flow suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result suspend fun paginate(direction: PaginationDirection): Result - fun paginationStatus(direction: PaginationDirection): StateFlow + + val backwardPaginationStatus: StateFlow + val forwardPaginationStatus: StateFlow + val timelineItems: Flow> suspend fun sendMessage( @@ -72,7 +76,7 @@ interface Timeline : AutoCloseable { ): Result suspend fun replyMessage( - eventId: EventId, + replyParameters: ReplyParameters, body: String, htmlBody: String?, intentionalMentions: List, @@ -85,7 +89,8 @@ interface Timeline : AutoCloseable { imageInfo: ImageInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result suspend fun sendVideo( @@ -94,18 +99,18 @@ interface Timeline : AutoCloseable { videoInfo: VideoInfo, caption: String?, formattedCaption: String?, - progressCallback: ProgressCallback? + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result - suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result - suspend fun sendAudio( file: File, audioInfo: AudioInfo, caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, - ): Result + replyParameters: ReplyParameters?, + ): Result suspend fun sendFile( file: File, @@ -113,15 +118,9 @@ interface Timeline : AutoCloseable { caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result - suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result - - suspend fun forwardEvent(eventId: EventId, roomIds: List): Result - - suspend fun cancelSend(transactionId: TransactionId): Result = - redactEvent(transactionId.toEventOrTransactionId(), reason = null) - /** * Share a location message in the room. * @@ -132,6 +131,7 @@ interface Timeline : AutoCloseable { * @param zoomLevel Optional zoom level to display the map at. * @param assetType Optional type of the location asset. * Set to SENDER if sharing own location. Set to PIN if sharing any location. + * @param replyParameters Optional reply parameters to use when sending the location. */ suspend fun sendLocation( body: String, @@ -139,8 +139,26 @@ interface Timeline : AutoCloseable { description: String? = null, zoomLevel: Int? = null, assetType: AssetType? = null, + replyParameters: ReplyParameters?, ): Result + suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, + ): Result + + suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result + + suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result + + suspend fun forwardEvent(eventId: EventId, roomIds: List): Result + + suspend fun cancelSend(transactionId: TransactionId): Result = + redactEvent(transactionId.toEventOrTransactionId(), reason = null) + /** * Create a poll in the room. * @@ -189,13 +207,6 @@ interface Timeline : AutoCloseable { */ suspend fun endPoll(pollStartId: EventId, text: String): Result - suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback? - ): Result - suspend fun loadReplyDetails(eventId: EventId): InReplyTo /** 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 c0f191a85b5..bf062b76e0d 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 @@ -70,7 +70,8 @@ data class UnableToDecryptContent( data class RoomMembershipContent( val userId: UserId, val userDisplayName: String?, - val change: MembershipChange? + val change: MembershipChange?, + val reason: String?, ) : EventContent data class ProfileChangeContent( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt index 1630f49301f..2fd9ec699a0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt @@ -28,6 +28,9 @@ sealed interface MessageShield { /** The sender was previously verified but is not anymore. */ data class VerificationViolation(val isCritical: Boolean) : MessageShield + + /** The sender of the event does not match the owner of the device that created the Megolm session. */ + data class MismatchedSender(val isCritical: Boolean) : MessageShield } val MessageShield.isCritical: Boolean @@ -38,4 +41,5 @@ val MessageShield.isCritical: Boolean is MessageShield.UnverifiedIdentity -> isCritical is MessageShield.SentInClear -> isCritical is MessageShield.VerificationViolation -> isCritical + is MessageShield.MismatchedSender -> isCritical } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt index ff1f9351f91..46ab482ad5b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt @@ -44,6 +44,13 @@ fun ProfileTimelineDetails.getDisambiguatedDisplayName(userId: UserId): String { } } +fun ProfileTimelineDetails.getDisplayName(): String? { + return when (this) { + is ProfileTimelineDetails.Ready -> displayName + else -> null + } +} + fun ProfileTimelineDetails.getAvatarUrl(): String? { return when (this) { is ProfileTimelineDetails.Ready -> avatarUrl diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt index 10835f04482..528e28d5cf4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt @@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.timeline.item.event enum class TimelineItemEventOrigin { LOCAL, SYNC, - PAGINATION + PAGINATION, + CACHE, } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TraceLogPack.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TraceLogPack.kt new file mode 100644 index 00000000000..cdb9c07996e --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TraceLogPack.kt @@ -0,0 +1,25 @@ +/* + * 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.matrix.api.tracing + +enum class TraceLogPack(val key: String) { + EVENT_CACHE("event_cache") { + override val title: String = "Event Cache" + }, + SEND_QUEUE("send_queue") { + override val title: String = "Send Queue" + }, + TIMELINE("timeline") { + override val title: String = "Timeline" + }, + NOTIFICATION_CLIENT("notification_client") { + override val title: String = "Notification Client" + }; + + abstract val title: String +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingConfiguration.kt index db2c946bf50..f559176fa6f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingConfiguration.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingConfiguration.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.tracing data class TracingConfiguration( val logLevel: LogLevel, val extraTargets: List, + val traceLogPacks: Set, val writesToLogcat: Boolean, val writesToFilesConfiguration: WriteToFilesConfiguration, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt index 53ee37ca78e..5cef6cde02b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt @@ -10,6 +10,5 @@ package io.element.android.libraries.matrix.api.tracing import timber.log.Timber interface TracingService { - fun setupTracing(tracingConfiguration: TracingConfiguration) fun createTimberTree(target: String): Timber.Tree } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationData.kt index 7f48977ae9d..7d460dc042d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationData.kt @@ -25,6 +25,4 @@ sealed interface SessionVerificationData { // https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji data class VerificationEmoji( val number: Int, - val emoji: String, - val description: String, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt index aa4a7a4b8af..897e17c6114 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt @@ -15,9 +15,15 @@ import kotlinx.parcelize.Parcelize @Parcelize data class SessionVerificationRequestDetails( - val senderId: UserId, + val senderProfile: SenderProfile, val flowId: FlowId, val deviceId: DeviceId, - val displayName: String?, val firstSeenTimestamp: Long, -) : Parcelable +) : Parcelable { + @Parcelize + data class SenderProfile( + val userId: UserId, + val displayName: String?, + val avatarUrl: String?, + ) : Parcelable +} 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 cf9468b4938..6e34c3de325 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 @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.api.verification import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.UserId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -31,7 +32,12 @@ interface SessionVerificationService { /** * Request verification of the current session. */ - suspend fun requestVerification() + suspend fun requestCurrentSessionVerification() + + /** + * Request verification of the user with the given [userId]. + */ + suspend fun requestUserVerification(userId: UserId) /** * Cancels the current verification attempt. @@ -67,7 +73,7 @@ interface SessionVerificationService { * Set this particular request as the currently active one and register for * events pertaining it. */ - suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) + suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) /** * Accept the previously acknowledged verification request. @@ -76,7 +82,7 @@ interface SessionVerificationService { } interface SessionVerificationServiceListener { - fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails) + fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) } /** Verification status of the current session. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/VerificationRequest.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/VerificationRequest.kt new file mode 100644 index 00000000000..f26afe50f61 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/VerificationRequest.kt @@ -0,0 +1,30 @@ +/* + * 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.matrix.api.verification + +import android.os.Parcelable +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.parcelize.Parcelize + +sealed interface VerificationRequest : Parcelable { + sealed interface Outgoing : VerificationRequest { + @Parcelize + data object CurrentSession : Outgoing + + @Parcelize + data class User(val userId: UserId) : Outgoing + } + + sealed class Incoming(open val details: SessionVerificationRequestDetails) : VerificationRequest { + @Parcelize + data class OtherSession(override val details: SessionVerificationRequestDetails) : Incoming(details) + + @Parcelize + data class User(override val details: SessionVerificationRequestDetails) : Incoming(details) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt new file mode 100644 index 00000000000..ee57c6edbc7 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt @@ -0,0 +1,16 @@ +/* + * 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.matrix.api.widget + +interface CallAnalyticCredentialsProvider { + val posthogUserId: String? + val posthogApiHost: String? + val posthogApiKey: String? + val rageshakeSubmitUrl: String? + val sentryDsn: String? +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt index e5bdeb9a8ec..c87316a1dc1 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.api.widget import java.util.UUID interface CallWidgetSettingsProvider { - fun provide( + suspend fun provide( baseUrl: String, widgetId: String = UUID.randomUUID().toString(), encrypted: Boolean, diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 90a76932c91..790d23ec075 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -37,7 +37,7 @@ dependencies { api(projects.libraries.matrix.api) implementation(libs.dagger) implementation(projects.libraries.core) - implementation("net.java.dev.jna:jna:5.16.0@aar") + implementation("net.java.dev.jna:jna:5.17.0@aar") implementation(libs.androidx.datastore.preferences) implementation(libs.serialization.json) implementation(libs.kotlinx.collections.immutable) 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 cb6d5aa24ee..7a3bfb3e6b7 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,19 +8,22 @@ package io.element.android.libraries.matrix.impl import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.paths.getSessionPaths import io.element.android.libraries.matrix.impl.util.anonymizedTokens import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.ClientSessionDelegate import org.matrix.rustcomponents.sdk.Session import timber.log.Timber +import java.lang.ref.WeakReference import java.util.concurrent.atomic.AtomicBoolean +private val loggerTag = LoggerTag("RustClientSessionDelegate") + /** * This class is responsible for handling the session data for the Rust SDK. * @@ -28,14 +31,11 @@ import java.util.concurrent.atomic.AtomicBoolean * * IMPORTANT: you must set the [client] property as soon as possible so [didReceiveAuthError] can work properly. */ -@OptIn(ExperimentalCoroutinesApi::class) class RustClientSessionDelegate( private val sessionStore: SessionStore, private val appCoroutineScope: CoroutineScope, coroutineDispatchers: CoroutineDispatchers, ) : ClientSessionDelegate, ClientDelegate { - private val clientLog = Timber.tag("$this") - // Used to ensure several calls to `didReceiveAuthError` don't trigger multiple logouts private val isLoggingOut = AtomicBoolean(false) @@ -43,20 +43,27 @@ class RustClientSessionDelegate( 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: RustMatrixClient? = null + private var client: WeakReference = WeakReference(null) /** * Sets the [ClientDelegate] for the [RustMatrixClient], and keeps a reference to the client so it can be used later. */ fun bindClient(client: RustMatrixClient) { - this.client = client + this.client = WeakReference(client) + } + + /** + * Clears the current client reference. + */ + fun clearCurrentClient() { + this.client.clear() } override fun saveSessionInKeychain(session: Session) { appCoroutineScope.launch(updateTokensDispatcher) { val existingData = sessionStore.getSession(session.userId) ?: return@launch val (anonymizedAccessToken, anonymizedRefreshToken) = session.anonymizedTokens() - clientLog.d( + Timber.tag(loggerTag.value).d( "Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " + "Was token valid: ${existingData.isTokenValid}" ) @@ -67,29 +74,29 @@ class RustClientSessionDelegate( sessionPaths = existingData.getSessionPaths(), ) sessionStore.updateData(newData) - clientLog.d("Saved new session data with access token: '$anonymizedAccessToken'.") + Timber.tag(loggerTag.value).d("Saved new session data with access token: '$anonymizedAccessToken'.") }.invokeOnCompletion { if (it != null) { - clientLog.e(it, "Failed to save new session data.") + Timber.tag(loggerTag.value).e(it, "Failed to save new session data.") } } } override fun didReceiveAuthError(isSoftLogout: Boolean) { - clientLog.w("didReceiveAuthError(isSoftLogout=$isSoftLogout)") + Timber.tag(loggerTag.value).w("didReceiveAuthError(isSoftLogout=$isSoftLogout)") if (isLoggingOut.getAndSet(true).not()) { - clientLog.v("didReceiveAuthError -> do the cleanup") + Timber.tag(loggerTag.value).v("didReceiveAuthError -> do the cleanup") // TODO handle isSoftLogout parameter. appCoroutineScope.launch(updateTokensDispatcher) { - val currentClient = client + val currentClient = client.get() if (currentClient == null) { - clientLog.w("didReceiveAuthError -> no client, exiting") + Timber.tag(loggerTag.value).w("didReceiveAuthError -> no client, exiting") isLoggingOut.set(false) return@launch } val existingData = sessionStore.getSession(currentClient.sessionId.value) val (anonymizedAccessToken, anonymizedRefreshToken) = existingData.anonymizedTokens() - clientLog.d( + Timber.tag(loggerTag.value).d( "Removing session data with access token '$anonymizedAccessToken' " + "and refresh token '$anonymizedRefreshToken'." ) @@ -97,25 +104,21 @@ class RustClientSessionDelegate( // Set isTokenValid to false val newData = existingData.copy(isTokenValid = false) sessionStore.updateData(newData) - clientLog.d("Invalidated session data with access token: '$anonymizedAccessToken'.") + Timber.tag(loggerTag.value).d("Invalidated session data with access token: '$anonymizedAccessToken'.") } else { - clientLog.d("No session data found.") + Timber.tag(loggerTag.value).d("No session data found.") } - client?.logout(userInitiated = false, ignoreSdkError = true) + currentClient.logout(userInitiated = false, ignoreSdkError = true) }.invokeOnCompletion { if (it != null) { - clientLog.e(it, "Failed to remove session data.") + Timber.tag(loggerTag.value).e(it, "Failed to remove session data.") } } } else { - clientLog.v("didReceiveAuthError -> already cleaning up") + Timber.tag(loggerTag.value).v("didReceiveAuthError -> already cleaning up") } } - override fun didRefreshTokens() { - // This is done in `saveSessionInKeychain(Session)` instead. - } - override fun retrieveSessionFromKeychain(userId: String): Session { // This should never be called, as it's only used for multi-process setups error("retrieveSessionFromKeychain should never be called for Android") 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 51cf67c2191..cf7d3595ad7 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 @@ -8,11 +8,12 @@ package io.element.android.libraries.matrix.impl import io.element.android.libraries.androidutils.file.getSizeOfFiles -import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.DeviceId @@ -21,27 +22,27 @@ 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 import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias 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.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService +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.MatrixRoom -import io.element.android.libraries.matrix.api.room.PendingRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom +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.RoomMember 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.join.JoinRule -import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.roomlist.RoomListService -import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState @@ -50,21 +51,28 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService +import io.element.android.libraries.matrix.impl.exception.mapClientException import io.element.android.libraries.matrix.impl.media.RustMediaLoader +import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.oidc.toRustAction import io.element.android.libraries.matrix.impl.pushers.RustPushersService +import io.element.android.libraries.matrix.impl.room.GetRoomResult +import io.element.android.libraries.matrix.impl.room.NotJoinedRustRoom import io.element.android.libraries.matrix.impl.room.RoomContentForwarder +import io.element.android.libraries.matrix.impl.room.RoomInfoMapper import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber import io.element.android.libraries.matrix.impl.room.RustRoomFactory import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory +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.preview.RoomPreviewInfoMapper import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService import io.element.android.libraries.matrix.impl.roomdirectory.map import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService +import io.element.android.libraries.matrix.impl.roomlist.roomOrNull import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.sync.map import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper @@ -88,7 +96,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -104,6 +111,7 @@ import org.matrix.rustcomponents.sdk.ClientException import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.PowerLevels +import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.use @@ -123,7 +131,7 @@ class RustMatrixClient( private val sessionStore: SessionStore, private val appCoroutineScope: CoroutineScope, private val sessionDelegate: RustClientSessionDelegate, - innerSyncService: ClientSyncService, + private val innerSyncService: ClientSyncService, dispatchers: CoroutineDispatchers, baseCacheDirectory: File, clock: SystemClock, @@ -148,9 +156,8 @@ class RustMatrixClient( ) private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService) private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) } - private val notificationService = RustNotificationService(innerNotificationClient, dispatchers, clock) - private val notificationSettingsService = RustNotificationSettingsService(innerClient, dispatchers) - .apply { start() } + private val notificationService = RustNotificationService(sessionId, innerNotificationClient, dispatchers, clock) + private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers) private val encryptionService = RustEncryptionService( client = innerClient, syncService = rustSyncService, @@ -184,6 +191,7 @@ class RustMatrixClient( sessionCoroutineScope = sessionCoroutineScope, ) + private val roomInfoMapper = RoomInfoMapper() private val roomMembershipObserver = RoomMembershipObserver() private val roomFactory = RustRoomFactory( roomListService = roomListService, @@ -199,6 +207,7 @@ class RustMatrixClient( timelineEventTypeFilterFactory = timelineEventTypeFilterFactory, featureFlagService = featureFlagService, roomMembershipObserver = roomMembershipObserver, + roomInfoMapper = roomInfoMapper, ) override val mediaLoader: MatrixMediaLoader = RustMediaLoader( @@ -207,6 +216,12 @@ class RustMatrixClient( innerClient = innerClient, ) + private val mediaPreviewService = RustMediaPreviewService( + sessionCoroutineScope = sessionCoroutineScope, + innerClient = innerClient, + sessionDispatcher = sessionDispatcher, + ) + private var clientDelegateTaskHandle: TaskHandle? = innerClient.setDelegate(sessionDelegate) private val _userProfile: MutableStateFlow = MutableStateFlow( @@ -214,13 +229,16 @@ class RustMatrixClient( userId = sessionId, // TODO cache for displayName? displayName = null, - avatarUrl = innerClient.cachedAvatarUrl(), + avatarUrl = null, ) ) override val userProfile: StateFlow = _userProfile override val ignoredUsersFlow = mxCallbackFlow> { + // Fetch the initial value manually, the SDK won't return it automatically + channel.trySend(innerClient.ignoredUsers().map(::UserId).toPersistentList()) + innerClient.subscribeToIgnoredUsers(object : IgnoredUsersListener { override fun call(ignoredUserIds: List) { channel.trySend(ignoredUserIds.map(::UserId).toPersistentList()) @@ -235,13 +253,16 @@ class RustMatrixClient( sessionDelegate.bindClient(this) sessionCoroutineScope.launch { + // Start notification settings + notificationSettingsService.start() + // Force a refresh of the profile getUserProfile() } } override fun userIdServerName(): String { - return runCatching { + return runCatchingExceptions { innerClient.userIdServerName() } .onFailure { @@ -252,59 +273,60 @@ class RustMatrixClient( } override suspend fun getUrl(url: String): Result = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.getUrl(url) } } - override suspend fun getRoom(roomId: RoomId): MatrixRoom? { - return roomFactory.create(roomId) + override suspend fun getRoom(roomId: RoomId): BaseRoom? = withContext(sessionDispatcher) { + roomFactory.getBaseRoom(roomId) } - override suspend fun getPendingRoom(roomId: RoomId): PendingRoom? { - return roomFactory.createPendingRoom(roomId) + override suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? = withContext(sessionDispatcher) { + (roomFactory.getJoinedRoomOrPreview(roomId) as? GetRoomResult.Joined)?.joinedRoom } /** - * Wait for the room to be available in the room list with the correct membership for the current user. - * @param roomIdOrAlias the room id or alias to wait for + * Wait for the room to be available in the client with the correct membership for the current user. + * @param roomId the room id to wait for * @param timeout the timeout to wait for the room to be available * @param currentUserMembership the membership to wait for * @throws TimeoutCancellationException if the room is not available after the timeout */ private suspend fun awaitRoom( - roomIdOrAlias: RoomIdOrAlias, + roomId: RoomId, timeout: Duration, currentUserMembership: CurrentUserMembership, - ): RoomSummary { + ): RoomInfo { return withTimeout(timeout) { - getRoomSummaryFlow(roomIdOrAlias) - .mapNotNull { optionalRoomSummary -> optionalRoomSummary.getOrNull() } - .filter { roomSummary -> roomSummary.info.currentUserMembership == currentUserMembership } - .first() + getRoomInfoFlow(roomId) + .mapNotNull { roomInfo -> roomInfo.getOrNull() } + .first { info -> info.currentUserMembership == currentUserMembership } // Ensure that the room is ready - .also { innerClient.awaitRoomRemoteEcho(it.roomId.value) } + .also { innerClient.awaitRoomRemoteEcho(roomId.value).destroy() } } } - override suspend fun findDM(userId: UserId): RoomId? = withContext(sessionDispatcher) { - innerClient.getDmRoom(userId.value)?.use { RoomId(it.id()) } + override suspend fun findDM(userId: UserId): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.getDmRoom(userId.value)?.use { RoomId(it.id()) } + } } override suspend fun ignoreUser(userId: UserId): Result = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.ignoreUser(userId.value) } } override suspend fun unignoreUser(userId: UserId): Result = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.unignoreUser(userId.value) } } override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { val rustParams = RustCreateRoomParameters( name = createRoomParams.name, topic = createRoomParams.topic, @@ -327,12 +349,13 @@ class RustMatrixClient( } ), joinRuleOverride = createRoomParams.joinRuleOverride?.map(), + historyVisibilityOverride = createRoomParams.historyVisibilityOverride?.map(), canonicalAlias = createRoomParams.roomAliasName.getOrNull(), ) val roomId = RoomId(innerClient.createRoom(rustParams)) // Wait to receive the room back from the sync but do not returns failure if it fails. try { - awaitRoom(roomId.toRoomIdOrAlias(), 30.seconds, CurrentUserMembership.JOINED) + awaitRoom(roomId, 30.seconds, CurrentUserMembership.JOINED) } catch (e: Exception) { Timber.e(e, "Timeout waiting for the room to be available in the room list") } @@ -353,7 +376,7 @@ class RustMatrixClient( } override suspend fun getProfile(userId: UserId): Result = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.getProfile(userId.value).let(UserProfileMapper::map) } } @@ -363,81 +386,85 @@ class RustMatrixClient( override suspend fun searchUsers(searchTerm: String, limit: Long): Result = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map) } } override suspend fun setDisplayName(displayName: String): Result = withContext(sessionDispatcher) { - runCatching { innerClient.setDisplayName(displayName) } + runCatchingExceptions { innerClient.setDisplayName(displayName) } } override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result = withContext(sessionDispatcher) { - runCatching { innerClient.uploadAvatar(mimeType, data) } + runCatchingExceptions { innerClient.uploadAvatar(mimeType, data) } } override suspend fun removeAvatar(): Result = withContext(sessionDispatcher) { - runCatching { innerClient.removeAvatar() } + runCatchingExceptions { innerClient.removeAvatar() } } - override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { - runCatching { + override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { + runCatchingExceptions { innerClient.joinRoomById(roomId.value).destroy() try { - awaitRoom(roomId.toRoomIdOrAlias(), 10.seconds, CurrentUserMembership.JOINED) + awaitRoom(roomId, 10.seconds, CurrentUserMembership.JOINED) } catch (e: Exception) { Timber.e(e, "Timeout waiting for the room to be available in the room list") null } } - } + }.mapFailure { it.mapClientException() } - override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result = withContext(sessionDispatcher) { - runCatching { - innerClient.joinRoomByIdOrAlias( + override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + val roomId = innerClient.joinRoomByIdOrAlias( roomIdOrAlias = roomIdOrAlias.identifier, serverNames = serverNames, - ).destroy() + ).use { + RoomId(it.id()) + } try { - awaitRoom(roomIdOrAlias, 10.seconds, CurrentUserMembership.JOINED) + awaitRoom(roomId, 10.seconds, CurrentUserMembership.JOINED) } catch (e: Exception) { Timber.e(e, "Timeout waiting for the room to be available in the room list") null } - } + }.mapFailure { it.mapClientException() } } - override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result = withContext( + override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result = withContext( sessionDispatcher ) { - runCatching { - innerClient.knock(roomIdOrAlias.identifier, message, serverNames).destroy() + runCatchingExceptions { + val roomId = innerClient.knock(roomIdOrAlias.identifier, message, serverNames).use { + RoomId(it.id()) + } try { - awaitRoom(roomIdOrAlias, 10.seconds, CurrentUserMembership.KNOCKED) + awaitRoom(roomId, 10.seconds, CurrentUserMembership.KNOCKED) } catch (e: Exception) { Timber.e(e, "Timeout waiting for the room to be available in the room list") null } - } + }.mapFailure { it.mapClientException() } } override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.trackRecentlyVisitedRoom(roomId.value) } } override suspend fun getRecentlyVisitedRooms(): Result> = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.getRecentlyVisitedRooms().map(::RoomId) } } override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result> = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { val result = innerClient.resolveRoomAlias(roomAlias.value)?.let { ResolvedRoomAlias( roomId = RoomId(it.roomId), @@ -448,15 +475,30 @@ class RustMatrixClient( } } - override suspend fun getRoomPreviewInfo(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result = withContext(sessionDispatcher) { - runCatching { + override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result = withContext(sessionDispatcher) { + runCatchingExceptions { when (roomIdOrAlias) { - is RoomIdOrAlias.Alias -> innerClient.getRoomPreviewFromRoomAlias(roomIdOrAlias.roomAlias.value) - is RoomIdOrAlias.Id -> innerClient.getRoomPreviewFromRoomId(roomIdOrAlias.roomId.value, serverNames) - }.use { roomPreview -> - RoomPreviewInfoMapper.map(roomPreview.info()) + is RoomIdOrAlias.Alias -> { + val roomId = innerClient.resolveRoomAlias(roomIdOrAlias.roomAlias.value)?.roomId?.let { RoomId(it) } + + var room = (roomId?.let { roomFactory.getJoinedRoomOrPreview(it) } as? GetRoomResult.NotJoined)?.notJoinedRoom + if (room == null) { + val preview = innerClient.getRoomPreviewFromRoomAlias(roomIdOrAlias.roomAlias.value) + room = NotJoinedRustRoom(sessionId, null, RoomPreviewInfoMapper.map(preview.info())) + } + room + } + is RoomIdOrAlias.Id -> { + var room = (roomFactory.getJoinedRoomOrPreview(roomIdOrAlias.roomId) as? GetRoomResult.NotJoined)?.notJoinedRoom + + if (room == null) { + val preview = innerClient.getRoomPreviewFromRoomId(roomIdOrAlias.roomId.value, serverNames) + room = NotJoinedRustRoom(sessionId, null, RoomPreviewInfoMapper.map(preview.info())) + } + room + } } - } + }.mapFailure { it.mapClientException() } } override fun syncService(): SyncService = rustSyncService @@ -473,15 +515,25 @@ class RustMatrixClient( override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService - override fun close() { - appCoroutineScope.launch { - roomFactory.destroy() - rustSyncService.destroy() - } + override fun mediaPreviewService(): MediaPreviewService = mediaPreviewService + + internal suspend fun destroy() { + innerNotificationClient.close() + + roomFactory.destroy() + rustSyncService.destroy() + notificationSettingsService.destroy() + notificationProcessSetup.destroy() + sessionCoroutineScope.cancel() clientDelegateTaskHandle?.cancelAndDestroy() - notificationSettingsService.destroy() verificationService.destroy() + + sessionDelegate.clearCurrentClient() + innerRoomListService.close() + notificationService.close() + encryptionService.close() + innerClient.close() } override suspend fun getCacheSize(): Long { @@ -489,19 +541,19 @@ class RustMatrixClient( } override suspend fun clearCache() { - close() - deleteSessionDirectory(deleteCryptoDb = false) + innerClient.clearCaches(innerSyncService) + destroy() } - override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? { - var result: String? = null + override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) { + sessionCoroutineScope.cancel() // Remove current delegate so we don't receive an auth error clientDelegateTaskHandle?.cancelAndDestroy() clientDelegateTaskHandle = null withContext(sessionDispatcher) { if (userInitiated) { try { - result = innerClient.logout() + innerClient.logout() } catch (failure: Throwable) { if (ignoreSdkError) { Timber.e(failure, "Fail to call logout on HS. Still delete local files.") @@ -513,18 +565,17 @@ class RustMatrixClient( } } } - close() + destroy() - deleteSessionDirectory(deleteCryptoDb = true) + deleteSessionDirectory() if (userInitiated) { sessionStore.removeSession(sessionId.value) } } - return result } override fun canDeactivateAccount(): Boolean { - return runCatching { + return runCatchingExceptions { innerClient.canDeactivateAccount() } .getOrNull() @@ -536,9 +587,9 @@ class RustMatrixClient( // Remove current delegate so we don't receive an auth error clientDelegateTaskHandle?.cancelAndDestroy() clientDelegateTaskHandle = null - runCatching { + runCatchingExceptions { // First call without AuthData, should fail - val firstAttempt = runCatching { + val firstAttempt = runCatchingExceptions { innerClient.deactivateAccount( authData = null, eraseData = eraseData, @@ -547,7 +598,7 @@ class RustMatrixClient( if (firstAttempt.isFailure) { Timber.w(firstAttempt.exceptionOrNull(), "Expected failure, try again") // This is expected, try again with the password - runCatching { + runCatchingExceptions { innerClient.deactivateAccount( authData = AuthData.Password( passwordDetails = AuthDataPasswordDetails( @@ -564,8 +615,8 @@ class RustMatrixClient( throw it } } - close() - deleteSessionDirectory(deleteCryptoDb = true) + destroy() + deleteSessionDirectory() sessionStore.removeSession(sessionId.value) }.onFailure { Timber.e(it, "Failed to deactivate account") @@ -574,34 +625,32 @@ class RustMatrixClient( override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result = withContext(sessionDispatcher) { val rustAction = action?.toRustAction() - runCatching { + runCatchingExceptions { innerClient.accountUrl(rustAction) } } override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher()) } } override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver - override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow> { - val predicate: (RoomSummary) -> Boolean = when (roomIdOrAlias) { - is RoomIdOrAlias.Alias -> { roomSummary -> - roomSummary.info.aliases.contains(roomIdOrAlias.roomAlias) + override fun getRoomInfoFlow(roomId: RoomId): Flow> { + return mxCallbackFlow { + val roomNotFound = innerRoomListService.roomOrNull(roomId.value).use { it == null } + if (roomNotFound) { + channel.send(Optional.empty()) } - is RoomIdOrAlias.Id -> { roomSummary -> - roomSummary.roomId == roomIdOrAlias.roomId - } - } - return roomListService.allRooms.summaries - .map { roomSummaries -> - val roomSummary = roomSummaries.firstOrNull(predicate) - Optional.ofNullable(roomSummary) - } - .distinctUntilChanged() + innerClient.subscribeToRoomInfo(roomId.value, object : RoomInfoListener { + override fun call(roomInfo: org.matrix.rustcomponents.sdk.RoomInfo) { + val mappedRoomInfo = roomInfoMapper.map(roomInfo) + channel.trySend(Optional.of(mappedRoomInfo)) + } + }) + }.distinctUntilChanged() } override suspend fun setAllSendQueuesEnabled(enabled: Boolean) { @@ -622,17 +671,27 @@ class RustMatrixClient( }.buffer(Channel.UNLIMITED) override suspend fun availableSlidingSyncVersions(): Result> = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.availableSlidingSyncVersions().map { it.map() } } } override suspend fun currentSlidingSyncVersion(): Result = withContext(sessionDispatcher) { - runCatching { + runCatchingExceptions { innerClient.session().slidingSyncVersion.map() } } + override suspend fun canReportRoom(): Boolean = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.isReportRoomApiSupported() + }.getOrDefault(false) + } + + override suspend fun isLivekitRtcSupported(): Boolean = withContext(sessionDispatcher) { + innerClient.isLivekitRtcSupported() + } + private suspend fun File.getCacheSize( includeCryptoDb: Boolean = false, ): Long = withContext(sessionDispatcher) { @@ -653,25 +712,9 @@ class RustMatrixClient( } } - private suspend fun deleteSessionDirectory( - deleteCryptoDb: Boolean = false, - ): Boolean = withContext(sessionDispatcher) { - val sessionPaths = sessionPathsProvider.provides(sessionId) ?: return@withContext false - // Always delete the cache directory - sessionPaths.cacheDirectory.deleteRecursively() - if (deleteCryptoDb) { - // Delete the folder and all its content - sessionPaths.fileDirectory.deleteRecursively() - } else { - // Do not delete the crypto database files. - sessionPaths.fileDirectory.listFiles().orEmpty() - .filterNot { it.name.contains("matrix-sdk-crypto") } - .forEach { file -> - Timber.w("Deleting file ${file.name}...") - file.safeDelete() - } - true - } + private suspend fun deleteSessionDirectory() = withContext(sessionDispatcher) { + // Delete all the files for this session + sessionPathsProvider.provides(sessionId)?.deleteRecursively() } } 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 48e12967929..c1dab6f404f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.impl.analytics.UtdTracker @@ -33,6 +34,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder import org.matrix.rustcomponents.sdk.use import timber.log.Timber import uniffi.matrix_sdk_crypto.CollectStrategy +import uniffi.matrix_sdk_crypto.DecryptionSettings import uniffi.matrix_sdk_crypto.TrustRequirement import java.io.File import javax.inject.Inject @@ -40,6 +42,7 @@ import javax.inject.Inject class RustMatrixClientFactory @Inject constructor( private val baseDirectory: File, @CacheDirectory private val cacheDirectory: File, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, private val sessionStore: SessionStore, @@ -72,8 +75,9 @@ class RustMatrixClientFactory @Inject constructor( suspend fun create(client: Client): RustMatrixClient { val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens() + client.setUtdDelegate(UtdTracker(analyticsService)) + val syncService = client.syncService() - .withUtdHook(UtdTracker(analyticsService)) .withOfflineMode() .finish() @@ -105,12 +109,11 @@ class RustMatrixClientFactory @Inject constructor( cachePath = sessionPaths.cacheDirectory.absolutePath, ) .setSessionDelegate(sessionDelegate) - .passphrase(passphrase) + .sessionPassphrase(passphrase) .userAgent(userAgentProvider.provide()) .addRootCertificates(userCertificatesProvider.provides()) .autoEnableBackups(true) .autoEnableCrossSigning(true) - .useEventCachePersistentStorage(featureFlagService.isFeatureEnabled(FeatureFlags.EventCache)) .roomKeyRecipientStrategy( strategy = if (featureFlagService.isFeatureEnabled(FeatureFlags.OnlySignedDeviceIsolationMode)) { CollectStrategy.IDENTITY_BASED_STRATEGY @@ -118,21 +121,22 @@ class RustMatrixClientFactory @Inject constructor( CollectStrategy.ERROR_ON_VERIFIED_USER_PROBLEM } ) - .roomDecryptionTrustRequirement( - trustRequirement = if (featureFlagService.isFeatureEnabled(FeatureFlags.OnlySignedDeviceIsolationMode)) { - TrustRequirement.CROSS_SIGNED_OR_LEGACY - } else { - TrustRequirement.UNTRUSTED - } + .decryptionSettings( + DecryptionSettings( + senderDeviceTrustRequirement = if (featureFlagService.isFeatureEnabled(FeatureFlags.OnlySignedDeviceIsolationMode)) { + TrustRequirement.CROSS_SIGNED_OR_LEGACY + } else { + TrustRequirement.UNTRUSTED + } + ) ) + .enableShareHistoryOnInvite(featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite)) .run { // Apply sliding sync version settings when (slidingSyncType) { ClientBuilderSlidingSync.Restored -> this - is ClientBuilderSlidingSync.CustomProxy -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Proxy(slidingSyncType.url)) - ClientBuilderSlidingSync.Discovered -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DiscoverProxy) - ClientBuilderSlidingSync.Simplified -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DiscoverNative) - ClientBuilderSlidingSync.ForcedSimplified -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Native) + ClientBuilderSlidingSync.Discovered -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DISCOVER_NATIVE) + ClientBuilderSlidingSync.Native -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.NATIVE) } } .run { @@ -143,21 +147,14 @@ class RustMatrixClientFactory @Inject constructor( } sealed interface ClientBuilderSlidingSync { - // The proxy is set by the user. - data class CustomProxy(val url: String) : ClientBuilderSlidingSync - // The proxy will be supplied when restoring the Session. data object Restored : ClientBuilderSlidingSync - // A proxy must be discovered whilst building the session. + // A Native Sliding Sync instance must be discovered whilst building the session. data object Discovered : ClientBuilderSlidingSync - // Use Simplified Sliding Sync. - data object Simplified : ClientBuilderSlidingSync - - // Force using Simplified Sliding Sync. - // TODO allow the user to select between proxy, simplified or force simplified in developer options. - data object ForcedSimplified : ClientBuilderSlidingSync + // Force using Native Sliding Sync. + data object Native : ClientBuilderSlidingSync } private fun SessionData.toSession() = Session( @@ -166,6 +163,6 @@ private fun SessionData.toSession() = Session( userId = userId, deviceId = deviceId, homeserverUrl = homeserverUrl, - slidingSyncVersion = slidingSyncProxy?.let(SlidingSyncVersion::Proxy) ?: SlidingSyncVersion.Native, + slidingSyncVersion = SlidingSyncVersion.NATIVE, 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 f98f9fedc82..66fcee1e48c 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 @@ -8,7 +8,10 @@ package io.element.android.libraries.matrix.impl.analytics import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.libraries.matrix.api.room.MatrixRoom +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 { return when (this) { @@ -22,11 +25,16 @@ private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize { } } -fun MatrixRoom.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom { +suspend fun BaseRoom.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom { + val roomInfo = roomInfoFlow.first() + return roomInfo.toAnalyticsJoinedRoom(trigger) +} + +fun RoomInfo.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom { return JoinedRoom( - isDM = isDirect, + isDM = isDm, isSpace = isSpace, - roomSize = joinedMemberCount.toAnalyticsRoomSize(), + roomSize = joinedMembersCount.toAnalyticsRoomSize(), trigger = trigger ) } 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 c7fa9598ffe..7175913dad4 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 @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.auth import io.element.android.libraries.matrix.api.auth.AuthenticationException import org.matrix.rustcomponents.sdk.ClientBuildException +import org.matrix.rustcomponents.sdk.OidcException fun Throwable.mapAuthenticationException(): AuthenticationException { val message = this.message ?: "Unknown error" @@ -24,6 +25,13 @@ fun Throwable.mapAuthenticationException(): AuthenticationException { is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message) is ClientBuildException.EventCache -> AuthenticationException.Generic(message) } + is OidcException -> when (this) { + is OidcException.Generic -> AuthenticationException.Oidc(message) + is OidcException.CallbackUrlInvalid -> AuthenticationException.Oidc(message) + is OidcException.Cancelled -> AuthenticationException.Oidc(message) + is OidcException.MetadataInvalid -> AuthenticationException.Oidc(message) + is OidcException.NotSupported -> AuthenticationException.Oidc(message) + } else -> AuthenticationException.Generic(message) } } 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 index b2539859425..ea398f935dd 100644 --- 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 @@ -7,28 +7,23 @@ package io.element.android.libraries.matrix.impl.auth +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 -import java.io.File import javax.inject.Inject class OidcConfigurationProvider @Inject constructor( - private val baseDirectory: File, + private val buildMeta: BuildMeta, + private val oidcRedirectUrlProvider: OidcRedirectUrlProvider, ) { fun get(): OidcConfiguration = OidcConfiguration( - clientName = "Element", - redirectUri = OidcConfig.REDIRECT_URI, - clientUri = "https://element.io", - logoUri = "https://element.io/mobile-icon.png", - tosUri = "https://element.io/acceptable-use-policy-terms", - policyUri = "https://element.io/privacy", - contacts = listOf( - "support@element.io", - ), - // Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually - staticRegistrations = mapOf( - "https://id.thirdroom.io/realms/thirdroom" to "elementx", - ), - dynamicRegistrationsFile = File(baseDirectory, "oidc/registrations.json").absolutePath, + 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 ea1eb93b293..0ba9eb4c498 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 @@ -12,10 +12,7 @@ import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt { return when (this) { - OidcPrompt.None -> RustOidcPrompt.None - OidcPrompt.Login -> RustOidcPrompt.Login - OidcPrompt.Consent -> RustOidcPrompt.Consent - OidcPrompt.SelectAccount -> RustOidcPrompt.SelectAccount + 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/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 54808fdb0e9..506abcf5da2 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 @@ -8,9 +8,9 @@ package io.element.android.libraries.matrix.impl.auth import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appconfig.AuthenticationConfig import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClient @@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.paths.SessionPaths import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory -import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionStore @@ -40,10 +39,8 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client -import org.matrix.rustcomponents.sdk.ClientBuildException import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.HumanQrLoginException import org.matrix.rustcomponents.sdk.OidcConfiguration @@ -52,7 +49,7 @@ import org.matrix.rustcomponents.sdk.QrCodeDecodeException import org.matrix.rustcomponents.sdk.QrLoginProgress import org.matrix.rustcomponents.sdk.QrLoginProgressListener import timber.log.Timber -import uniffi.matrix_sdk.OidcAuthorizationData +import uniffi.matrix_sdk.OAuthAuthorizationData import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -64,7 +61,6 @@ class RustMatrixAuthenticationService @Inject constructor( private val rustMatrixClientFactory: RustMatrixClientFactory, private val passphraseGenerator: PassphraseGenerator, private val oidcConfigurationProvider: OidcConfigurationProvider, - private val appPreferencesStore: AppPreferencesStore, ) : MatrixAuthenticationService { // Passphrase which will be used for new sessions. Existing sessions will use the passphrase // stored in the SessionData. @@ -96,7 +92,7 @@ class RustMatrixAuthenticationService @Inject constructor( } override suspend fun restoreSession(sessionId: SessionId): Result = withContext(coroutineDispatchers.io) { - runCatching { + runCatchingExceptions { val sessionData = sessionStore.getSession(sessionId.value) if (sessionData != null) { if (sessionData.isTokenValid) { @@ -131,7 +127,7 @@ class RustMatrixAuthenticationService @Inject constructor( override suspend fun setHomeserver(homeserver: String): Result = withContext(coroutineDispatchers.io) { val emptySessionPath = rotateSessionPath() - runCatching { + runCatchingExceptions { val client = makeClient(sessionPaths = emptySessionPath) { serverNameOrHomeserverUrl(homeserver) } @@ -142,13 +138,14 @@ class RustMatrixAuthenticationService @Inject constructor( }.onFailure { clear() }.mapFailure { failure -> + Timber.e(failure, "Failed to set homeserver to $homeserver") failure.mapAuthenticationException() } } override suspend fun login(username: String, password: String): Result = withContext(coroutineDispatchers.io) { - runCatching { + runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") client.login(username, password, "Element X Android", null) @@ -161,15 +158,20 @@ class RustMatrixAuthenticationService @Inject constructor( ) newMatrixClientObserver?.invoke(rustMatrixClientFactory.create(client)) sessionStore.storeData(sessionData) + + // Clean up the strong reference held here since it's no longer necessary + currentClient = null + SessionId(sessionData.userId) }.mapFailure { failure -> + Timber.e(failure, "Failed to login") failure.mapAuthenticationException() } } override suspend fun importCreatedSession(externalSession: ExternalSession): Result = withContext(coroutineDispatchers.io) { - runCatching { + runCatchingExceptions { currentClient ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") val sessionData = externalSession.toSessionData( @@ -184,20 +186,25 @@ class RustMatrixAuthenticationService @Inject constructor( } } - private var pendingOidcAuthorizationData: OidcAuthorizationData? = null + private var pendingOAuthAuthorizationData: OAuthAuthorizationData? = null - override suspend fun getOidcUrl(prompt: OidcPrompt): Result { + override suspend fun getOidcUrl( + prompt: OidcPrompt, + loginHint: String?, + ): Result { return withContext(coroutineDispatchers.io) { - runCatching { + runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") - val oidcAuthenticationData = client.urlForOidc( + val oAuthAuthorizationData = client.urlForOidc( oidcConfiguration = oidcConfigurationProvider.get(), prompt = prompt.toRustPrompt(), + loginHint = loginHint, ) - val url = oidcAuthenticationData.loginUrl() - pendingOidcAuthorizationData = oidcAuthenticationData + val url = oAuthAuthorizationData.loginUrl() + pendingOAuthAuthorizationData = oAuthAuthorizationData OidcDetails(url) }.mapFailure { failure -> + Timber.e(failure, "Failed to get OIDC URL") failure.mapAuthenticationException() } } @@ -205,10 +212,13 @@ class RustMatrixAuthenticationService @Inject constructor( override suspend fun cancelOidcLogin(): Result { return withContext(coroutineDispatchers.io) { - runCatching { - pendingOidcAuthorizationData?.close() - pendingOidcAuthorizationData = null + runCatchingExceptions { + pendingOAuthAuthorizationData?.use { + currentClient?.abortOidcAuth(it) + } + pendingOAuthAuthorizationData = null }.mapFailure { failure -> + Timber.e(failure, "Failed to cancel OIDC login") failure.mapAuthenticationException() } } @@ -219,23 +229,30 @@ class RustMatrixAuthenticationService @Inject constructor( */ override suspend fun loginWithOidc(callbackUrl: String): Result { return withContext(coroutineDispatchers.io) { - runCatching { + runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") - val urlForOidcLogin = pendingOidcAuthorizationData ?: error("You need to call `getOidcUrl()` first") - client.loginWithOidcCallback(urlForOidcLogin, callbackUrl) + client.loginWithOidcCallback(callbackUrl) val sessionData = client.session().toSessionData( isTokenValid = true, loginType = LoginType.OIDC, passphrase = pendingPassphrase, sessionPaths = currentSessionPaths, ) - pendingOidcAuthorizationData?.close() - pendingOidcAuthorizationData = null + + // Free the pending data since we won't use it to abort the flow anymore + pendingOAuthAuthorizationData?.close() + pendingOAuthAuthorizationData = null + newMatrixClientObserver?.invoke(rustMatrixClientFactory.create(client)) sessionStore.storeData(sessionData) + + // Clean up the strong reference held here since it's no longer necessary + currentClient = null + SessionId(sessionData.userId) }.mapFailure { failure -> + Timber.e(failure, "Failed to login with OIDC") failure.mapAuthenticationException() } } @@ -252,7 +269,7 @@ class RustMatrixAuthenticationService @Inject constructor( progress(state.toStep()) } } - runCatching { + runCatchingExceptions { val client = makeQrCodeLoginClient( sessionPaths = emptySessionPaths, passphrase = pendingPassphrase, @@ -269,6 +286,10 @@ class RustMatrixAuthenticationService @Inject constructor( ) newMatrixClientObserver?.invoke(rustMatrixClientFactory.create(client)) sessionStore.storeData(sessionData) + + // Clean up the strong reference held here since it's no longer necessary + currentClient = null + SessionId(sessionData.userId) }.mapFailure { when (it) { @@ -288,28 +309,12 @@ class RustMatrixAuthenticationService @Inject constructor( sessionPaths: SessionPaths, config: suspend ClientBuilder.() -> ClientBuilder, ): Client { - val slidingSyncType = getSlidingSyncType() - if (slidingSyncType is ClientBuilderSlidingSync.Simplified) { - Timber.d("Creating client with simplified sliding sync") - try { - return rustMatrixClientFactory - .getBaseClientBuilder( - sessionPaths = sessionPaths, - passphrase = pendingPassphrase, - slidingSyncType = slidingSyncType, - ) - .config() - .build() - } catch (e: ClientBuildException.SlidingSyncVersion) { - Timber.e(e, "Failed to create client with simplified sliding sync, trying with Proxy now") - } - } - Timber.d("Creating client with Proxy sliding sync") + Timber.d("Creating client with simplified sliding sync") return rustMatrixClientFactory .getBaseClientBuilder( sessionPaths = sessionPaths, passphrase = pendingPassphrase, - slidingSyncType = getSlidingSyncProxy(), + slidingSyncType = ClientBuilderSlidingSync.Discovered, ) .config() .build() @@ -322,43 +327,17 @@ class RustMatrixAuthenticationService @Inject constructor( oidcConfiguration: OidcConfiguration, progressListener: QrLoginProgressListener, ): Client { - val slidingSyncType = getSlidingSyncType() - if (slidingSyncType is ClientBuilderSlidingSync.Simplified) { - Timber.d("Creating client for QR Code login with simplified sliding sync") - try { - return rustMatrixClientFactory - .getBaseClientBuilder( - sessionPaths = sessionPaths, - passphrase = pendingPassphrase, - slidingSyncType = slidingSyncType, - ) - .passphrase(passphrase) - .buildWithQrCode(qrCodeData, oidcConfiguration, progressListener) - } catch (e: HumanQrLoginException.SlidingSyncNotAvailable) { - Timber.e(e, "Failed to create client with simplified sliding sync, trying with Proxy now") - } - } - Timber.d("Creating client for QR Code login with Proxy sliding sync") + Timber.d("Creating client for QR Code login with simplified sliding sync") return rustMatrixClientFactory .getBaseClientBuilder( sessionPaths = sessionPaths, passphrase = pendingPassphrase, - slidingSyncType = getSlidingSyncProxy(), + slidingSyncType = ClientBuilderSlidingSync.Discovered, ) - .passphrase(passphrase) + .sessionPassphrase(passphrase) .buildWithQrCode(qrCodeData, oidcConfiguration, progressListener) } - private suspend fun getSlidingSyncType(nativeSlidingSyncFailed: Boolean = false) = when { - appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first() && !nativeSlidingSyncFailed -> ClientBuilderSlidingSync.Simplified - else -> getSlidingSyncProxy() - } - - private fun getSlidingSyncProxy() = when { - AuthenticationConfig.SLIDING_SYNC_PROXY_URL != null -> ClientBuilderSlidingSync.CustomProxy(AuthenticationConfig.SLIDING_SYNC_PROXY_URL!!) - else -> ClientBuilderSlidingSync.Discovered - } - private fun clear() { currentClient?.close() currentClient = null diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt index 7efb918bfae..367a272a75c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.auth.qrlogin import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory @@ -17,6 +18,6 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class RustQrCodeLoginDataFactory @Inject constructor() : MatrixQrCodeLoginDataFactory { override fun parseQrCodeData(data: ByteArray): Result { - return runCatching { SdkQrCodeLoginData(QrCodeData.fromBytes(data)) } + return runCatchingExceptions { SdkQrCodeLoginData(QrCodeData.fromBytes(data)) } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginData.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginData.kt index 04a338f17eb..485e9bfaec1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginData.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginData.kt @@ -12,4 +12,8 @@ import org.matrix.rustcomponents.sdk.QrCodeData as RustQrCodeData class SdkQrCodeLoginData( internal val rustQrCodeData: RustQrCodeData, -) : MatrixQrCodeLoginData +) : MatrixQrCodeLoginData { + override fun serverName(): String? { + return rustQrCodeData.serverName() + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt deleted file mode 100644 index 6cf607a3737..00000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.call - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider -import timber.log.Timber -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultElementCallBaseUrlProvider @Inject constructor( - private val elementWellKnownParser: ElementWellKnownParser, -) : ElementCallBaseUrlProvider { - override suspend fun provides(matrixClient: MatrixClient): String? { - val url = buildString { - append("https://") - append(matrixClient.userIdServerName()) - append("/.well-known/element/element.json") - } - return matrixClient.getUrl(url) - .onFailure { failure -> - Timber.w(failure, "Failed to fetch well-known element.json") - } - .getOrNull() - ?.let { wellKnownStr -> - elementWellKnownParser.parse(wellKnownStr) - .onFailure { failure -> - // Can be a HTML 404. - Timber.w(failure, "Failed to parse content") - } - .getOrNull() - } - ?.call - ?.widgetUrl - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt deleted file mode 100644 index 46d601f5230..00000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.call - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import org.matrix.rustcomponents.sdk.ElementWellKnown -import org.matrix.rustcomponents.sdk.makeElementWellKnown -import javax.inject.Inject - -interface ElementWellKnownParser { - fun parse(str: String): Result -} - -@ContributesBinding(AppScope::class) -class RustElementWellKnownParser @Inject constructor() : ElementWellKnownParser { - override fun parse(str: String): Result { - return runCatching { - makeElementWellKnown(str) - } - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/RustSendHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/RustSendHandle.kt index 41a6edf2405..9d5e1137038 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/RustSendHandle.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/RustSendHandle.kt @@ -7,13 +7,14 @@ package io.element.android.libraries.matrix.impl.core +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.SendHandle class RustSendHandle( val inner: org.matrix.rustcomponents.sdk.SendHandle, ) : SendHandle { override suspend fun retry(): Result { - return runCatching { + return runCatchingExceptions { inner.tryResend() } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 361d1efd97c..6a04079a7e1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.EncryptionService 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.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService @@ -71,4 +72,9 @@ object SessionMatrixModule { fun providesRoomDirectoryService(matrixClient: MatrixClient): RoomDirectoryService { return matrixClient.roomDirectoryService() } + + @Provides + fun providesMediaPreviewService(matrixClient: MatrixClient): MediaPreviewService { + return matrixClient.mediaPreviewService() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index 3e0e6571d4c..7c87666fe7f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.encryption import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.core.extensions.runCatchingExceptions 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.encryption.BackupState @@ -18,6 +19,7 @@ import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.impl.sync.RustSyncService import kotlinx.coroutines.CoroutineScope @@ -95,7 +97,7 @@ internal class RustEncryptionService( .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false) override suspend fun enableBackups(): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { service.enableBackups() }.mapFailure { it.mapRecoveryException() @@ -105,7 +107,7 @@ internal class RustEncryptionService( override suspend fun enableRecovery( waitForBackupsToUpload: Boolean, ): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { service.enableRecovery( waitForBackupsToUpload = waitForBackupsToUpload, progressListener = object : EnableRecoveryProgressListener { @@ -123,14 +125,14 @@ internal class RustEncryptionService( } override suspend fun doesBackupExistOnServer(): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { service.backupExistsOnServer() } } override fun waitForBackupUploadSteadyState(): Flow { return callbackFlow { - runCatching { + runCatchingExceptions { service.waitForBackupUploadSteadyState( progressListener = object : BackupSteadyStateListener { override fun onUpdate(status: RustBackupUploadState) { @@ -154,7 +156,7 @@ internal class RustEncryptionService( } override suspend fun disableRecovery(): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { service.disableRecovery() }.mapFailure { it.mapRecoveryException() @@ -162,7 +164,7 @@ internal class RustEncryptionService( } private suspend fun isLastDevice(): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { service.isLastDevice() }.mapFailure { it.mapRecoveryException() @@ -170,7 +172,7 @@ internal class RustEncryptionService( } override suspend fun resetRecoveryKey(): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { service.resetRecoveryKey() }.mapFailure { it.mapRecoveryException() @@ -178,7 +180,7 @@ internal class RustEncryptionService( } override suspend fun recover(recoveryKey: String): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { service.recover(recoveryKey) }.mapFailure { it.mapRecoveryException() @@ -186,33 +188,52 @@ internal class RustEncryptionService( } override suspend fun deviceCurve25519(): String? { - return service.curve25519Key() + return runCatchingExceptions { service.curve25519Key() }.getOrNull() } override suspend fun deviceEd25519(): String? { - return service.ed25519Key() + return runCatchingExceptions { service.ed25519Key() }.getOrNull() } override suspend fun startIdentityReset(): Result { - return runCatching { + return runCatchingExceptions { service.resetIdentity() }.flatMap { handle -> RustIdentityResetHandleFactory.create(sessionId, handle) } } - override suspend fun isUserVerified(userId: UserId): Result = runCatching { - getUserIdentity(userId).isVerified() + override suspend fun isUserVerified(userId: UserId): Result = runCatchingExceptions { + getUserIdentityInternal(userId).isVerified() } - override suspend fun pinUserIdentity(userId: UserId): Result = runCatching { - getUserIdentity(userId).pin() + override suspend fun pinUserIdentity(userId: UserId): Result = runCatchingExceptions { + getUserIdentityInternal(userId).pin() } - private suspend fun getUserIdentity(userId: UserId): UserIdentity { + override suspend fun withdrawVerification(userId: UserId): Result = runCatchingExceptions { + getUserIdentityInternal(userId).withdrawVerification() + } + + override suspend fun getUserIdentity(userId: UserId): Result = runCatchingExceptions { + val identity = getUserIdentityInternal(userId) + val isVerified = identity.isVerified() + when { + identity.hasVerificationViolation() -> IdentityState.VerificationViolation + isVerified -> IdentityState.Verified + !isVerified -> IdentityState.Pinned + else -> null + } + } + + suspend fun getUserIdentityInternal(userId: UserId): UserIdentity { return service.userIdentity( userId = userId.value, // requestFromHomeserverIfNeeded = true, ) ?: error("User identity not found") } + + fun close() { + service.close() + } } 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 e16c094569c..3651cc52ac2 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 @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.impl.encryption +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle @@ -20,7 +21,7 @@ object RustIdentityResetHandleFactory { userId: UserId, identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle? ): Result { - return runCatching { + return runCatchingExceptions { identityResetHandle?.let { when (val authType = identityResetHandle.authType()) { is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl) @@ -37,7 +38,7 @@ class RustPasswordIdentityResetHandle( private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, ) : IdentityPasswordResetHandle { override suspend fun resetPassword(password: String): Result { - return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } + return runCatchingExceptions { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } } override suspend fun cancel() { @@ -50,7 +51,7 @@ class RustOidcIdentityResetHandle( override val url: String, ) : IdentityOidcResetHandle { override suspend fun resetOidc(): Result { - return runCatching { identityResetHandle.reset(null) } + return runCatchingExceptions { identityResetHandle.reset(null) } } override suspend fun cancel() { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt index d8bc9ccff7e..21b7f393a1e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt @@ -14,7 +14,13 @@ fun Throwable.mapClientException(): ClientException { return when (this) { is RustClientException -> { when (this) { - is RustClientException.Generic -> ClientException.Generic(msg) + is RustClientException.Generic -> ClientException.Generic(msg, details) + is RustClientException.MatrixApi -> ClientException.MatrixApi( + kind = kind.map(), + code = code, + message = msg, + details = details, + ) } } else -> ClientException.Other(message ?: "Unknown error") diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ErrorKind.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ErrorKind.kt new file mode 100644 index 00000000000..12b15b64651 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ErrorKind.kt @@ -0,0 +1,63 @@ +/* + * 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.matrix.impl.exception +import io.element.android.libraries.matrix.api.exception.ErrorKind +import org.matrix.rustcomponents.sdk.ErrorKind as RustErrorKind + +fun RustErrorKind.map(): ErrorKind { + return when (this) { + RustErrorKind.BadAlias -> ErrorKind.BadAlias + RustErrorKind.BadJson -> ErrorKind.BadJson + RustErrorKind.BadState -> ErrorKind.BadState + is RustErrorKind.BadStatus -> ErrorKind.BadStatus(status?.toInt(), body) + RustErrorKind.CannotLeaveServerNoticeRoom -> ErrorKind.CannotLeaveServerNoticeRoom + RustErrorKind.CannotOverwriteMedia -> ErrorKind.CannotOverwriteMedia + RustErrorKind.CaptchaInvalid -> ErrorKind.CaptchaInvalid + RustErrorKind.CaptchaNeeded -> ErrorKind.CaptchaNeeded + RustErrorKind.ConnectionFailed -> ErrorKind.ConnectionFailed + RustErrorKind.ConnectionTimeout -> ErrorKind.ConnectionTimeout + is RustErrorKind.Custom -> ErrorKind.Custom(errcode) + RustErrorKind.DuplicateAnnotation -> ErrorKind.DuplicateAnnotation + RustErrorKind.Exclusive -> ErrorKind.Exclusive + RustErrorKind.Forbidden -> ErrorKind.Forbidden + RustErrorKind.GuestAccessForbidden -> ErrorKind.GuestAccessForbidden + is RustErrorKind.IncompatibleRoomVersion -> ErrorKind.IncompatibleRoomVersion(roomVersion) + RustErrorKind.InvalidParam -> ErrorKind.InvalidParam + RustErrorKind.InvalidRoomState -> ErrorKind.InvalidRoomState + RustErrorKind.InvalidUsername -> ErrorKind.InvalidUsername + is RustErrorKind.LimitExceeded -> ErrorKind.LimitExceeded(retryAfterMs?.toLong()) + RustErrorKind.MissingParam -> ErrorKind.MissingParam + RustErrorKind.MissingToken -> ErrorKind.MissingToken + RustErrorKind.NotFound -> ErrorKind.NotFound + RustErrorKind.NotJson -> ErrorKind.NotJson + RustErrorKind.NotYetUploaded -> ErrorKind.NotYetUploaded + is RustErrorKind.ResourceLimitExceeded -> ErrorKind.ResourceLimitExceeded(adminContact) + RustErrorKind.RoomInUse -> ErrorKind.RoomInUse + RustErrorKind.ServerNotTrusted -> ErrorKind.ServerNotTrusted + RustErrorKind.ThreepidAuthFailed -> ErrorKind.ThreepidAuthFailed + RustErrorKind.ThreepidDenied -> ErrorKind.ThreepidDenied + RustErrorKind.ThreepidInUse -> ErrorKind.ThreepidInUse + RustErrorKind.ThreepidMediumNotSupported -> ErrorKind.ThreepidMediumNotSupported + RustErrorKind.ThreepidNotFound -> ErrorKind.ThreepidNotFound + RustErrorKind.TooLarge -> ErrorKind.TooLarge + RustErrorKind.UnableToAuthorizeJoin -> ErrorKind.UnableToAuthorizeJoin + RustErrorKind.UnableToGrantJoin -> ErrorKind.UnableToGrantJoin + RustErrorKind.Unauthorized -> ErrorKind.Unauthorized + RustErrorKind.Unknown -> ErrorKind.Unknown + is RustErrorKind.UnknownToken -> ErrorKind.UnknownToken(softLogout) + RustErrorKind.Unrecognized -> ErrorKind.Unrecognized + RustErrorKind.UnsupportedRoomVersion -> ErrorKind.UnsupportedRoomVersion + RustErrorKind.UrlNotSet -> ErrorKind.UrlNotSet + RustErrorKind.UserDeactivated -> ErrorKind.UserDeactivated + RustErrorKind.UserInUse -> ErrorKind.UserInUse + RustErrorKind.UserLocked -> ErrorKind.UserLocked + RustErrorKind.UserSuspended -> ErrorKind.UserSuspended + RustErrorKind.WeakPassword -> ErrorKind.WeakPassword + is RustErrorKind.WrongRoomKeysVersion -> ErrorKind.WrongRoomKeysVersion(currentVersion) + } +} 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 f90b334f058..1d45c474706 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 @@ -12,7 +12,6 @@ import io.element.android.libraries.matrix.impl.paths.SessionPaths import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData import org.matrix.rustcomponents.sdk.Session -import org.matrix.rustcomponents.sdk.SlidingSyncVersion import java.util.Date internal fun Session.toSessionData( @@ -28,7 +27,7 @@ internal fun Session.toSessionData( refreshToken = refreshToken, homeserverUrl = homeserverUrl ?: this.homeserverUrl, oidcData = oidcData, - slidingSyncProxy = (slidingSyncVersion as? SlidingSyncVersion.Proxy)?.url, + slidingSyncProxy = null, loginTimestamp = Date(), isTokenValid = isTokenValid, loginType = loginType, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt index 185bdf42c71..dbfa67f374a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.media import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.media.MediaUploadHandler import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import java.io.File @@ -17,7 +18,7 @@ class MediaUploadHandlerImpl( private val sendAttachmentJoinHandle: SendAttachmentJoinHandle, ) : MediaUploadHandler { override suspend fun await(): Result = - runCatching { + runCatchingExceptions { sendAttachmentJoinHandle.join() } .also { cleanUpFiles() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index a7a926239b8..d7cd9c4092b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -8,11 +8,11 @@ package io.element.android.libraries.matrix.impl.media 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.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaSource -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.use @@ -24,31 +24,28 @@ class RustMediaLoader( dispatchers: CoroutineDispatchers, private val innerClient: Client, ) : MatrixMediaLoader { - @OptIn(ExperimentalCoroutinesApi::class) private val mediaDispatcher = dispatchers.io.limitedParallelism(32) private val cacheDirectory get() = File(baseCacheDirectory, "temp/media").apply { if (!exists()) mkdirs() // Must always ensure that this directory exists because "Clear cache" does not restart an app's process. } - @OptIn(ExperimentalUnsignedTypes::class) override suspend fun loadMediaContent(source: MediaSource): Result = withContext(mediaDispatcher) { - runCatching { + runCatchingExceptions { source.toRustMediaSource().use { source -> innerClient.getMediaContent(source) } } } - @OptIn(ExperimentalUnsignedTypes::class) override suspend fun loadMediaThumbnail( source: MediaSource, width: Long, height: Long ): Result = withContext(mediaDispatcher) { - runCatching { + runCatchingExceptions { source.toRustMediaSource().use { mediaSource -> innerClient.getMediaThumbnail( mediaSource = mediaSource, @@ -66,12 +63,20 @@ class RustMediaLoader( useCache: Boolean, ): Result = withContext(mediaDispatcher) { - runCatching { + runCatchingExceptions { source.toRustMediaSource().use { mediaSource -> val mediaFile = innerClient.getMediaFile( mediaSource = mediaSource, filename = filename, - mimeType = mimeType?.takeIf { MimeTypes.hasSubtype(it) } ?: MimeTypes.OctetStream, + mimeType = when { + mimeType == null -> MimeTypes.OctetStream + MimeTypes.hasSubtype(mimeType) -> mimeType + // Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension. + mimeType == MimeTypes.Images -> MimeTypes.Jpeg + mimeType == MimeTypes.Videos -> MimeTypes.Mp4 + mimeType == MimeTypes.Audio -> MimeTypes.Mp3 + else -> MimeTypes.OctetStream + }, useCache = useCache, tempDir = cacheDirectory.path, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt new file mode 100644 index 00000000000..e4e9bc07bdf --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt @@ -0,0 +1,88 @@ +/* + * 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.matrix.impl.media + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewService +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.impl.util.mxCallbackFlow +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.InviteAvatars +import org.matrix.rustcomponents.sdk.MediaPreviewConfigListener +import org.matrix.rustcomponents.sdk.MediaPreviews +import org.matrix.rustcomponents.sdk.MediaPreviewConfig as RustMediaPreviewConfig + +class RustMediaPreviewService( + sessionCoroutineScope: CoroutineScope, + private val sessionDispatcher: CoroutineDispatcher, + private val innerClient: Client, +) : MediaPreviewService { + override val mediaPreviewConfigFlow: StateFlow = + innerClient + .getMediaPreviewConfigFlow() + .stateIn(sessionCoroutineScope, started = SharingStarted.Lazily, initialValue = MediaPreviewConfig.DEFAULT) + + override suspend fun fetchMediaPreviewConfig(): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.fetchMediaPreviewConfig()?.into() + } + } + + override suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.setMediaPreviewDisplayPolicy(mediaPreviewValue.into()) + } + } + + override suspend fun setHideInviteAvatars(hide: Boolean): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + val inviteAvatars = if (hide) InviteAvatars.OFF else InviteAvatars.ON + innerClient.setInviteAvatarsDisplayPolicy(inviteAvatars) + } + } +} + +private fun RustMediaPreviewConfig.into(): MediaPreviewConfig { + return MediaPreviewConfig( + mediaPreviewValue = mediaPreviews.into(), + hideInviteAvatar = inviteAvatars == InviteAvatars.OFF + ) +} + +private fun Client.getMediaPreviewConfigFlow() = mxCallbackFlow { + subscribeToMediaPreviewConfig(object : MediaPreviewConfigListener { + override fun onChange(mediaPreviewConfig: RustMediaPreviewConfig?) { + if (mediaPreviewConfig != null) { + trySend(mediaPreviewConfig.into()) + } + } + }) +} + +private fun MediaPreviewValue.into(): MediaPreviews { + return when (this) { + MediaPreviewValue.On -> MediaPreviews.ON + MediaPreviewValue.Off -> MediaPreviews.OFF + MediaPreviewValue.Private -> MediaPreviews.PRIVATE + } +} + +private fun MediaPreviews.into(): MediaPreviewValue { + return when (this) { + MediaPreviews.ON -> MediaPreviewValue.On + MediaPreviews.OFF -> MediaPreviewValue.Off + MediaPreviews.PRIVATE -> MediaPreviewValue.Private + } +} 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 8b21648071f..9130df5c7a0 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 @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.notification import io.element.android.libraries.core.bool.orFalse 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.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData @@ -25,6 +26,7 @@ class NotificationMapper( private val notificationContentMapper = NotificationContentMapper() fun map( + sessionId: SessionId, eventId: EventId, roomId: RoomId, notificationItem: NotificationItem @@ -35,7 +37,10 @@ class NotificationMapper( activeMembersCount = item.roomInfo.joinedMembersCount.toInt(), ) NotificationData( + sessionId = sessionId, eventId = eventId, + // FIXME once the `NotificationItem` in the SDK returns the thread id + threadId = null, roomId = roomId, senderAvatarUrl = item.senderInfo.avatarUrl, senderDisplayName = item.senderInfo.displayName, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt index 2e817900bdd..b88b53454a2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt @@ -8,31 +8,75 @@ package io.element.android.libraries.matrix.impl.notification import io.element.android.libraries.core.coroutine.CoroutineDispatchers +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.SessionId +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.notification.NotificationService import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.NotificationClient -import org.matrix.rustcomponents.sdk.use +import org.matrix.rustcomponents.sdk.NotificationItemsRequest +import timber.log.Timber class RustNotificationService( + private val sessionId: SessionId, private val notificationClient: NotificationClient, private val dispatchers: CoroutineDispatchers, - clock: SystemClock, + private val clock: SystemClock, ) : NotificationService { private val notificationMapper: NotificationMapper = NotificationMapper(clock) - override suspend fun getNotification( - roomId: RoomId, - eventId: EventId, - ): Result = withContext(dispatchers.io) { - runCatching { - val item = notificationClient.getNotification(roomId.value, eventId.value) - item?.use { - notificationMapper.map(eventId, roomId, it) + override suspend fun getNotifications( + ids: Map> + ): Result> = withContext(dispatchers.io) { + runCatchingExceptions { + val requests = ids.map { (roomId, eventIds) -> + NotificationItemsRequest( + roomId = roomId.value, + eventIds = eventIds.map { it.value } + ) + } + val items = notificationClient.getNotifications(requests) + buildMap { + val eventIds = requests.flatMap { it.eventIds } + for (eventId in eventIds) { + val item = items[eventId] + val roomId = RoomId(requests.find { it.eventIds.contains(eventId) }?.roomId!!) + if (item != null) { + put(EventId(eventId), notificationMapper.map(sessionId, EventId(eventId), roomId, item)) + } else { + Timber.e("Could not retrieve event for notification with $eventId") + put( + EventId(eventId), + NotificationData( + sessionId = sessionId, + eventId = EventId(eventId), + threadId = null, + roomId = roomId, + senderAvatarUrl = null, + senderDisplayName = null, + senderIsNameAmbiguous = false, + roomAvatarUrl = null, + roomDisplayName = null, + isDirect = false, + isDm = false, + isEncrypted = false, + isNoisy = false, + timestamp = clock.epochMillis(), + content = NotificationContent.MessageLike.UnableToResolve, + hasMention = false + ) + ) + } + } } } } + + fun close() { + notificationClient.close() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt index e05a4d7e67b..cf914abe8da 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt @@ -62,7 +62,7 @@ private fun StateEventContent.toContent(): NotificationContent.StateEvent { StateEventContent.RoomServerAcl -> NotificationContent.StateEvent.RoomServerAcl StateEventContent.RoomThirdPartyInvite -> NotificationContent.StateEvent.RoomThirdPartyInvite StateEventContent.RoomTombstone -> NotificationContent.StateEvent.RoomTombstone - StateEventContent.RoomTopic -> NotificationContent.StateEvent.RoomTopic + is StateEventContent.RoomTopic -> NotificationContent.StateEvent.RoomTopic(topic) StateEventContent.SpaceChild -> NotificationContent.StateEvent.SpaceChild StateEventContent.SpaceParent -> NotificationContent.StateEvent.SpaceParent } 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 b08a6675a45..b4f3e70754e 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 @@ -8,10 +8,13 @@ package io.element.android.libraries.matrix.impl.notificationsettings import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.coroutine.suspendLazy +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -24,9 +27,10 @@ import timber.log.Timber class RustNotificationSettingsService( client: Client, + sessionCoroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, ) : NotificationSettingsService { - private val notificationSettings = client.getNotificationSettings() + private val notificationSettings by suspendLazy(sessionCoroutineScope.coroutineContext + dispatchers.io) { client.getNotificationSettings() } private val _notificationSettingsChangeFlow = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) override val notificationSettingsChangeFlow: SharedFlow = _notificationSettingsChangeFlow.asSharedFlow() @@ -36,22 +40,22 @@ class RustNotificationSettingsService( } } - fun start() { - notificationSettings.setDelegate(notificationSettingsDelegate) + suspend fun start() { + notificationSettings.await().setDelegate(notificationSettingsDelegate) } - fun destroy() { - notificationSettings.setDelegate(null) + suspend fun destroy() { + notificationSettings.await().setDelegate(null) } override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result = - runCatching { - notificationSettings.getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map) + runCatchingExceptions { + notificationSettings.await().getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map) } override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result = - runCatching { - notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) + runCatchingExceptions { + notificationSettings.await().getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) } override suspend fun setDefaultRoomNotificationMode( @@ -59,9 +63,9 @@ class RustNotificationSettingsService( mode: RoomNotificationMode, isOneToOne: Boolean ): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { try { - notificationSettings.setDefaultRoomNotificationMode(isEncrypted, isOneToOne, 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 @@ -71,68 +75,68 @@ class RustNotificationSettingsService( } override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result = withContext(dispatchers.io) { - runCatching { - notificationSettings.setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode)) + runCatchingExceptions { + notificationSettings.await().setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode)) } } override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result = withContext(dispatchers.io) { - runCatching { - notificationSettings.restoreDefaultRoomNotificationMode(roomId.value) + runCatchingExceptions { + notificationSettings.await().restoreDefaultRoomNotificationMode(roomId.value) } } override suspend fun muteRoom(roomId: RoomId): Result = setRoomNotificationMode(roomId, RoomNotificationMode.MUTE) override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean) = withContext(dispatchers.io) { - runCatching { - notificationSettings.unmuteRoom(roomId.value, isEncrypted, isOneToOne) + runCatchingExceptions { + notificationSettings.await().unmuteRoom(roomId.value, isEncrypted, isOneToOne) } } override suspend fun isRoomMentionEnabled(): Result = withContext(dispatchers.io) { - runCatching { - notificationSettings.isRoomMentionEnabled() + runCatchingExceptions { + notificationSettings.await().isRoomMentionEnabled() } } override suspend fun setRoomMentionEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { - runCatching { - notificationSettings.setRoomMentionEnabled(enabled) + runCatchingExceptions { + notificationSettings.await().setRoomMentionEnabled(enabled) } } override suspend fun isCallEnabled(): Result = withContext(dispatchers.io) { - runCatching { - notificationSettings.isCallEnabled() + runCatchingExceptions { + notificationSettings.await().isCallEnabled() } } override suspend fun setCallEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { - runCatching { - notificationSettings.setCallEnabled(enabled) + runCatchingExceptions { + notificationSettings.await().setCallEnabled(enabled) } } override suspend fun isInviteForMeEnabled(): Result = withContext(dispatchers.io) { - runCatching { - notificationSettings.isInviteForMeEnabled() + runCatchingExceptions { + notificationSettings.await().isInviteForMeEnabled() } } override suspend fun setInviteForMeEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { - runCatching { - notificationSettings.setInviteForMeEnabled(enabled) + runCatchingExceptions { + notificationSettings.await().setInviteForMeEnabled(enabled) } } override suspend fun getRoomsWithUserDefinedRules(): Result> = - runCatching { - notificationSettings.getRoomsWithUserDefinedRules(enabled = true) + runCatchingExceptions { + notificationSettings.await().getRoomsWithUserDefinedRules(enabled = true) } override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result = - runCatching { - notificationSettings.canPushEncryptedEventToDevice() + runCatchingExceptions { + notificationSettings.await().canPushEncryptedEventToDevice() } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt index c578ca5cc8f..9e881f18aa9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.permalink import android.net.Uri +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.MatrixConfiguration import io.element.android.libraries.core.extensions.replacePrefix @@ -43,7 +44,7 @@ class DefaultMatrixToConverter @Inject constructor() : MatrixToConverter { // Web or client url SUPPORTED_PATHS.any { it in uriString } -> { val path = SUPPORTED_PATHS.first { it in uriString } - Uri.parse(baseUrl + uriString.substringAfter(path)) + (baseUrl + uriString.substringAfter(path)).toUri() } // URL is not supported else -> null diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt index 50b3eb1d3e0..eb0524886f1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.permalink import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.RoomAlias @@ -24,7 +25,7 @@ class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder { if (!MatrixPatterns.isUserId(userId.value)) { return Result.failure(PermalinkBuilderError.InvalidData) } - return runCatching { + return runCatchingExceptions { matrixToUserPermalink(userId.value) } } @@ -33,7 +34,7 @@ class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder { if (!MatrixPatterns.isRoomAlias(roomAlias.value)) { return Result.failure(PermalinkBuilderError.InvalidData) } - return runCatching { + return runCatchingExceptions { matrixToRoomAliasPermalink(roomAlias.value) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt index d27a9a79df5..974c88825d9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt @@ -7,8 +7,9 @@ package io.element.android.libraries.matrix.impl.permalink -import android.net.Uri +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomAlias @@ -28,6 +29,7 @@ import javax.inject.Inject * element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks * or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org) * or client permalinks (e.g. user/@chagai95:matrix.org) + * or matrix: permalinks (e.g. matrix:u/chagai95:matrix.org) */ @ContributesBinding(AppScope::class) class DefaultPermalinkParser @Inject constructor( @@ -38,13 +40,18 @@ class DefaultPermalinkParser @Inject constructor( * https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md */ override fun parse(uriString: String): PermalinkData { - val uri = Uri.parse(uriString) - // the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the - // mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid - // so convert URI to matrix.to to simplify parsing process - val matrixToUri = matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) + val uri = uriString.toUri() + val matrixToUri = if (uri.scheme == "matrix") { + // take matrix: URI as is to [parseMatrixEntityFrom] + uri + } else { + // the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the + // mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid + // so convert URI to matrix.to to simplify parsing process + matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) + } - val result = runCatching { + val result = runCatchingExceptions { parseMatrixEntityFrom(matrixToUri.toString()) }.getOrNull() return if (result == null) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/platform/RustInitPlatformService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/platform/RustInitPlatformService.kt new file mode 100644 index 00000000000..1324ebe7965 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/platform/RustInitPlatformService.kt @@ -0,0 +1,26 @@ +/* + * 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.matrix.impl.platform + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.platform.InitPlatformService +import io.element.android.libraries.matrix.api.tracing.TracingConfiguration +import io.element.android.libraries.matrix.impl.tracing.map +import org.matrix.rustcomponents.sdk.initPlatform +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class RustInitPlatformService @Inject constructor() : InitPlatformService { + override fun init(tracingConfiguration: TracingConfiguration) { + initPlatform( + config = tracingConfiguration.map(), + useLightweightTokioRuntime = false + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/DefaultProxyProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/DefaultProxyProvider.kt index 57f7cd8e6b8..08809be5f7f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/DefaultProxyProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/proxy/DefaultProxyProvider.kt @@ -8,7 +8,9 @@ package io.element.android.libraries.matrix.impl.proxy import android.content.Context +import android.net.ConnectivityManager import android.provider.Settings +import androidx.core.content.getSystemService import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -32,6 +34,13 @@ class DefaultProxyProvider @Inject constructor( private val context: Context ) : ProxyProvider { override fun provides(): String? { + val defaultProxy = context.getSystemService()?.defaultProxy + if (defaultProxy == null) { + // Note: can be tested by running: + // adb shell settings put global http_proxy :0 + Timber.d("No default proxy") + return null + } return Settings.Global.getString(context.contentResolver, Settings.Global.HTTP_PROXY) ?.also { Timber.d("Using global proxy") diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt index 3ff24e85d8d..0fed390b143 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.pushers import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData @@ -26,7 +27,7 @@ class RustPushersService( ) : PushersService { override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result { return withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { client.setPusher( identifiers = PusherIdentifiers( pushkey = setHttpPusherData.pushKey, @@ -51,7 +52,7 @@ class RustPushersService( override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result { return withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { client.deletePusher( identifiers = PusherIdentifiers( pushkey = unsetHttpPusherData.pushKey, 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 new file mode 100644 index 00000000000..a7dc2523ad3 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -0,0 +1,490 @@ +/* + * 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.matrix.impl.room + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.coroutine.childScope +import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.matrix.api.core.DeviceId +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.SendHandle +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.CreateTimelineParams +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.RoomNotificationSettingsState +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.knock.KnockRequest +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import io.element.android.libraries.matrix.api.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.impl.core.RustSendHandle +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.member.RoomMemberListFetcher +import io.element.android.libraries.matrix.impl.roomdirectory.map +import io.element.android.libraries.matrix.impl.timeline.RustTimeline +import io.element.android.libraries.matrix.impl.util.MessageEventContent +import io.element.android.libraries.matrix.impl.util.mxCallbackFlow +import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver +import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl +import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +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.RoomMessageEventMessageType +import org.matrix.rustcomponents.sdk.TimelineConfiguration +import org.matrix.rustcomponents.sdk.TimelineFilter +import org.matrix.rustcomponents.sdk.TimelineFocus +import org.matrix.rustcomponents.sdk.TypingNotificationsListener +import org.matrix.rustcomponents.sdk.UserPowerLevelUpdate +import org.matrix.rustcomponents.sdk.WidgetCapabilities +import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider +import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions +import org.matrix.rustcomponents.sdk.use +import timber.log.Timber +import uniffi.matrix_sdk.RoomPowerLevelChanges +import kotlin.coroutines.cancellation.CancellationException +import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange +import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest +import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline + +class JoinedRustRoom( + private val baseRoom: RustBaseRoom, + private val liveInnerTimeline: InnerTimeline, + private val notificationSettingsService: NotificationSettingsService, + private val coroutineDispatchers: CoroutineDispatchers, + private val systemClock: SystemClock, + private val roomContentForwarder: RoomContentForwarder, + private val featureFlagService: FeatureFlagService, +) : JoinedRoom, BaseRoom by baseRoom { + // Create a dispatcher for all room methods... + private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) + private val innerRoom = baseRoom.innerRoom + + override val syncUpdateFlow = MutableStateFlow(0L) + + override val roomTypingMembersFlow: Flow> = mxCallbackFlow { + val initial = emptyList() + channel.trySend(initial) + innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener { + override fun call(typingUserIds: List) { + channel.trySend( + typingUserIds + .filter { it != sessionId.value } + .map(::UserId) + ) + } + }) + } + + override val identityStateChangesFlow: Flow> = mxCallbackFlow { + val initial = emptyList() + channel.trySend(initial) + innerRoom.subscribeToIdentityStatusChanges(object : IdentityStatusChangeListener { + override fun call(identityStatusChange: List) { + channel.trySend( + identityStatusChange.map { + IdentityStateChange( + userId = UserId(it.userId), + identityState = it.changedTo.map(), + ) + } + ) + } + }) + } + + override val knockRequestsFlow: Flow> = mxCallbackFlow { + innerRoom.subscribeToKnockRequests(object : KnockRequestsListener { + override fun call(joinRequests: List) { + val knockRequests = joinRequests.map { RustKnockRequest(it) } + channel.trySend(knockRequests) + } + }) + } + + override val roomNotificationSettingsStateFlow = MutableStateFlow(RoomNotificationSettingsState.Unknown) + + override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.LIVE) { + syncUpdateFlow.value = systemClock.epochMillis() + } + + init { + val powerLevelChanges = roomInfoFlow.map { it.roomPowerLevels }.distinctUntilChanged() + val membershipChanges = liveTimeline.membershipChangeEventReceived.onStart { emit(Unit) } + combine(membershipChanges, powerLevelChanges) { _, _ -> } + // Skip initial one + .drop(1) + // The new events should already be in the SDK cache, no need to fetch them from the server + .onEach { baseRoom.roomMemberListFetcher.fetchRoomMembers(source = RoomMemberListFetcher.Source.CACHE) } + .launchIn(roomCoroutineScope) + .invokeOnCompletion { + Timber.d("Observing membership changes for room $roomId stopped, reason: $it") + } + } + + override suspend fun createTimeline( + createTimelineParams: CreateTimelineParams, + ): Result = withContext(roomDispatcher) { + val focus = when (createTimelineParams) { + is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents( + maxEventsToLoad = 100u, + maxConcurrentRequests = 10u, + ) + is CreateTimelineParams.MediaOnly -> TimelineFocus.Live(hideThreadedEvents = false) + is CreateTimelineParams.Focused -> TimelineFocus.Event( + eventId = createTimelineParams.focusedEventId.value, + numContextEvents = 50u, + hideThreadedEvents = false, + ) + is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event( + eventId = createTimelineParams.focusedEventId.value, + numContextEvents = 50u, + hideThreadedEvents = false, + ) + } + + val filter = when (createTimelineParams) { + is CreateTimelineParams.MediaOnly, + is CreateTimelineParams.MediaOnlyFocused -> TimelineFilter.OnlyMessage( + types = listOf( + RoomMessageEventMessageType.FILE, + RoomMessageEventMessageType.IMAGE, + RoomMessageEventMessageType.VIDEO, + RoomMessageEventMessageType.AUDIO, + ) + ) + is CreateTimelineParams.Focused, + CreateTimelineParams.PinnedOnly -> TimelineFilter.All + } + + val internalIdPrefix = when (createTimelineParams) { + is CreateTimelineParams.PinnedOnly -> "pinned_events" + is CreateTimelineParams.Focused -> "focus_${createTimelineParams.focusedEventId}" + is CreateTimelineParams.MediaOnly -> "MediaGallery_" + is CreateTimelineParams.MediaOnlyFocused -> "MediaGallery_${createTimelineParams.focusedEventId}" + } + + // Note that for TimelineFilter.MediaOnlyFocused, the date separator will be filtered out, + // but there is no way to exclude data separator at the moment. + val dateDividerMode = when (createTimelineParams) { + is CreateTimelineParams.MediaOnly, + is CreateTimelineParams.MediaOnlyFocused -> DateDividerMode.MONTHLY + is CreateTimelineParams.Focused, + CreateTimelineParams.PinnedOnly -> DateDividerMode.DAILY + } + + // Track read receipts only for focused timeline for performance optimization + val trackReadReceipts = createTimelineParams is CreateTimelineParams.Focused + + runCatchingExceptions { + innerRoom.timelineWithConfiguration( + configuration = TimelineConfiguration( + focus = focus, + filter = filter, + internalIdPrefix = internalIdPrefix, + dateDividerMode = dateDividerMode, + trackReadReceipts = trackReadReceipts, + reportUtds = true, + ) + ).let { innerTimeline -> + val mode = when (createTimelineParams) { + is CreateTimelineParams.Focused -> Timeline.Mode.FOCUSED_ON_EVENT + is CreateTimelineParams.MediaOnly -> Timeline.Mode.MEDIA + is CreateTimelineParams.MediaOnlyFocused -> Timeline.Mode.FOCUSED_ON_EVENT + CreateTimelineParams.PinnedOnly -> Timeline.Mode.PINNED_EVENTS + } + innerTimeline.map(mode = mode) + } + }.mapFailure { + when (createTimelineParams) { + is CreateTimelineParams.Focused, + is CreateTimelineParams.MediaOnlyFocused -> it.toFocusEventException() + CreateTimelineParams.MediaOnly, + CreateTimelineParams.PinnedOnly -> it + } + }.onFailure { + if (it is CancellationException) { + throw it + } + } + } + + override suspend fun editMessage( + eventId: EventId, + body: String, + htmlBody: String?, + intentionalMentions: List + ): Result = withContext(roomDispatcher) { + runCatchingExceptions { + MessageEventContent.from(body, htmlBody, intentionalMentions).use { newContent -> + innerRoom.edit(eventId.value, newContent) + } + } + } + + override suspend fun typingNotice(isTyping: Boolean) = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.typingNotice(isTyping) + } + } + + override suspend fun inviteUserById(id: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.inviteUserById(id.value) + } + } + + override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.uploadAvatar(mimeType, data, null) + } + } + + override suspend fun removeAvatar(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.removeAvatar() + } + } + + override suspend fun setName(name: String): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.setName(name) + } + } + + override suspend fun setTopic(topic: String): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.setTopic(topic) + } + } + + override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason) + if (blockUserId != null) { + innerRoom.ignoreUser(blockUserId.value) + } + } + } + + override suspend fun updateRoomNotificationSettings(): Result = withContext(roomDispatcher) { + val currentState = roomNotificationSettingsStateFlow.value + val currentRoomNotificationSettings = currentState.roomNotificationSettings() + roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) + runCatchingExceptions { + val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow() + notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() + }.map { + roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Ready(it) + }.onFailure { + roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Error( + prevRoomNotificationSettings = currentRoomNotificationSettings, + failure = it + ) + } + } + + override suspend fun updateCanonicalAlias(canonicalAlias: RoomAlias?, alternativeAliases: List): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.updateCanonicalAlias(canonicalAlias?.value, alternativeAliases.map { it.value }) + } + } + + override suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.publishRoomAliasInRoomDirectory(roomAlias.value) + } + } + + override suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.removeRoomAliasFromRoomDirectory(roomAlias.value) + } + } + + override suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.updateRoomVisibility(roomVisibility.map()) + } + } + + override suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.updateHistoryVisibility(historyVisibility.map()) + } + } + + override suspend fun enableEncryption(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.enableEncryption() + } + } + + override suspend fun updateJoinRule(joinRule: JoinRule): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.updateJoinRules(joinRule.map()) + } + } + + override suspend fun updateUsersRoles(changes: List): Result { + return runCatchingExceptions { + val powerLevelChanges = changes.map { UserPowerLevelUpdate(it.userId.value, it.powerLevel) } + innerRoom.updatePowerLevelsForUsers(powerLevelChanges) + } + } + + override suspend fun updatePowerLevels(roomPowerLevelsValues: RoomPowerLevelsValues): Result = withContext(roomDispatcher) { + runCatchingExceptions { + val changes = RoomPowerLevelChanges( + ban = roomPowerLevelsValues.ban, + invite = roomPowerLevelsValues.invite, + kick = roomPowerLevelsValues.kick, + redact = roomPowerLevelsValues.redactEvents, + eventsDefault = roomPowerLevelsValues.sendEvents, + roomName = roomPowerLevelsValues.roomName, + roomAvatar = roomPowerLevelsValues.roomAvatar, + roomTopic = roomPowerLevelsValues.roomTopic, + ) + innerRoom.applyPowerLevelChanges(changes) + } + } + + override suspend fun resetPowerLevels(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.resetPowerLevels().let {} + } + } + + override suspend fun kickUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.kickUser(userId.value, reason) + } + } + + override suspend fun banUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.banUser(userId.value, reason) + } + } + + override suspend fun unbanUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.unbanUser(userId.value, reason) + } + } + + override suspend fun generateWidgetWebViewUrl( + widgetSettings: MatrixWidgetSettings, + clientId: String, + languageTag: String?, + theme: String?, + ) = withContext(roomDispatcher) { + runCatchingExceptions { + widgetSettings.generateWidgetWebViewUrl(innerRoom, clientId, languageTag, theme) + } + } + + override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result { + return runCatchingExceptions { + RustWidgetDriver( + widgetSettings = widgetSettings, + room = innerRoom, + widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider { + override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities { + return getElementCallRequiredPermissions(sessionId.value, baseRoom.deviceId.value) + } + }, + ) + } + } + + override suspend fun sendCallNotificationIfNeeded(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.sendCallNotificationIfNeeded() + } + } + + override suspend fun setSendQueueEnabled(enabled: Boolean) { + withContext(roomDispatcher) { + Timber.d("setSendQueuesEnabled: $enabled") + runCatchingExceptions { + innerRoom.enableSendQueue(enabled) + } + } + } + + override suspend fun ignoreDeviceTrustAndResend(devices: Map>, sendHandle: SendHandle) = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.ignoreDeviceTrustAndResend( + devices = devices.entries.associate { entry -> + entry.key.value to entry.value.map { it.value } + }, + sendHandle = (sendHandle as RustSendHandle).inner, + ) + } + } + + override suspend fun withdrawVerificationAndResend(userIds: List, sendHandle: SendHandle) = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.withdrawVerificationAndResend( + userIds = userIds.map { it.value }, + sendHandle = (sendHandle as RustSendHandle).inner, + ) + } + } + + override fun close() = destroy() + + override fun destroy() { + baseRoom.destroy() + liveInnerTimeline.destroy() + } + + private fun InnerTimeline.map( + mode: Timeline.Mode, + onNewSyncedEvent: () -> Unit = {}, + ): Timeline { + val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$this") + return RustTimeline( + mode = mode, + joinedRoom = this@JoinedRustRoom, + inner = this@map, + systemClock = systemClock, + coroutineScope = timelineCoroutineScope, + dispatcher = roomDispatcher, + roomContentForwarder = roomContentForwarder, + onNewSyncedEvent = onNewSyncedEvent, + featureFlagsService = featureFlagService, + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt deleted file mode 100644 index 0be4ccbc67a..00000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.matrix.impl.room - -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.CurrentUserMembership -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo -import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.api.user.MatrixUser -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.member.RoomMemberMapper -import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentMap -import org.matrix.rustcomponents.sdk.Membership -import org.matrix.rustcomponents.sdk.RoomHero -import org.matrix.rustcomponents.sdk.Membership as RustMembership -import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo -import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode - -class MatrixRoomInfoMapper { - fun map(rustRoomInfo: RustRoomInfo): MatrixRoomInfo = rustRoomInfo.let { - return MatrixRoomInfo( - id = RoomId(it.id), - creator = it.creator?.let(::UserId), - name = it.displayName, - rawName = it.rawName, - topic = it.topic, - avatarUrl = it.avatarUrl, - isDirect = it.isDirect, - isPublic = it.isPublic, - joinRule = it.joinRule?.map(), - isSpace = it.isSpace, - isTombstoned = it.isTombstoned, - isFavorite = it.isFavourite, - canonicalAlias = it.canonicalAlias?.let(::RoomAlias), - alternativeAliases = it.alternativeAliases.map(::RoomAlias).toImmutableList(), - currentUserMembership = it.membership.map(), - inviter = it.inviter?.let(RoomMemberMapper::map), - activeMembersCount = it.activeMembersCount.toLong(), - invitedMembersCount = it.invitedMembersCount.toLong(), - joinedMembersCount = it.joinedMembersCount.toLong(), - userPowerLevels = mapPowerLevels(it.userPowerLevels), - highlightCount = it.highlightCount.toLong(), - notificationCount = it.notificationCount.toLong(), - userDefinedNotificationMode = it.cachedUserDefinedNotificationMode?.map(), - hasRoomCall = it.hasRoomCall, - activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(), - heroes = it.elementHeroes().toImmutableList(), - pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(), - isMarkedUnread = it.isMarkedUnread, - numUnreadMessages = it.numUnreadMessages.toLong(), - numUnreadMentions = it.numUnreadMentions.toLong(), - numUnreadNotifications = it.numUnreadNotifications.toLong(), - historyVisibility = it.historyVisibility.map(), - ) - } -} - -fun RustMembership.map(): CurrentUserMembership = when (this) { - RustMembership.INVITED -> CurrentUserMembership.INVITED - RustMembership.JOINED -> CurrentUserMembership.JOINED - RustMembership.LEFT -> CurrentUserMembership.LEFT - Membership.KNOCKED -> CurrentUserMembership.KNOCKED - RustMembership.BANNED -> CurrentUserMembership.BANNED -} - -fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) { - RustRoomNotificationMode.ALL_MESSAGES -> RoomNotificationMode.ALL_MESSAGES - RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY - RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE -} - -/** - * Map a RoomHero to a MatrixUser. There is not need to create a RoomHero type on the application side. - */ -fun RoomHero.map(): MatrixUser = MatrixUser( - userId = UserId(userId), - displayName = displayName, - avatarUrl = avatarUrl -) - -fun mapPowerLevels(powerLevels: Map): ImmutableMap { - return powerLevels.mapKeys { (key, _) -> UserId(key) }.toPersistentMap() -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/NotJoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/NotJoinedRustRoom.kt new file mode 100644 index 00000000000..fb88cf625bf --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/NotJoinedRustRoom.kt @@ -0,0 +1,36 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.room + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.room.NotJoinedRoom +import io.element.android.libraries.matrix.api.room.RoomMembershipDetails +import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo +import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper + +@Immutable +class NotJoinedRustRoom( + private val sessionId: SessionId, + override val localRoom: RustBaseRoom?, + override val previewInfo: RoomPreviewInfo, +) : NotJoinedRoom { + override suspend fun membershipDetails(): Result = runCatchingExceptions { + val room = localRoom?.innerRoom ?: return@runCatchingExceptions null + val (ownMember, senderInfo) = room.memberWithSenderInfo(sessionId.value) + RoomMembershipDetails( + currentUserMember = RoomMemberMapper.map(ownMember), + senderMember = senderInfo?.let { RoomMemberMapper.map(it) }, + ) + } + + override fun close() { + localRoom?.close() + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt index c4d2d3bc98f..9d7495691da 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt @@ -8,14 +8,15 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.core.coroutine.parallelMap +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.room.ForwardEventException -import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline import io.element.android.libraries.matrix.impl.roomlist.roomOrNull import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered import kotlinx.coroutines.CancellationException import kotlinx.coroutines.withTimeout +import org.matrix.rustcomponents.sdk.MsgLikeKind import org.matrix.rustcomponents.sdk.RoomListService import org.matrix.rustcomponents.sdk.Timeline import org.matrix.rustcomponents.sdk.TimelineItemContent @@ -42,17 +43,17 @@ class RoomContentForwarder( toRoomIds: List, timeoutMs: Long = 5000L ) { - val content = (fromTimeline.getEventTimelineItemByEventId(eventId.value).content as? TimelineItemContent.Message)?.content + val messageLikeContent = (fromTimeline.getEventTimelineItemByEventId(eventId.value).content as? TimelineItemContent.MsgLike)?.content ?: throw ForwardEventException(toRoomIds) - val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) } - val targetRooms = targetSlidingSyncRooms.map { slidingSyncRoom -> - slidingSyncRoom.use { it.fullRoomWithTimeline(null) } - } + val content = (messageLikeContent.kind as? MsgLikeKind.Message)?.content + ?: throw ForwardEventException(toRoomIds) + + val targetRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) } val failedForwardingTo = mutableSetOf() targetRooms.parallelMap { room -> room.use { targetRoom -> - runCatching { + runCatchingExceptions { // Sending a message requires a registered timeline listener targetRoom.timeline().runWithTimelineListenerRegistered { withTimeout(timeoutMs.milliseconds) { 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 new file mode 100644 index 00000000000..5b5d1f69f0e --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt @@ -0,0 +1,106 @@ +/* + * 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. + */ + +package io.element.android.libraries.matrix.impl.room + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.RoomInfo +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels +import io.element.android.libraries.matrix.api.user.MatrixUser +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.member.RoomMemberMapper +import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsValuesMapper +import io.element.android.libraries.matrix.impl.room.tombstone.map +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentMap +import org.matrix.rustcomponents.sdk.Membership +import org.matrix.rustcomponents.sdk.RoomHero +import uniffi.matrix_sdk_base.EncryptionState +import org.matrix.rustcomponents.sdk.Membership as RustMembership +import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo +import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode +import org.matrix.rustcomponents.sdk.RoomPowerLevels as RustRoomPowerLevels + +class RoomInfoMapper { + fun map(rustRoomInfo: RustRoomInfo): RoomInfo = rustRoomInfo.let { + return RoomInfo( + id = RoomId(it.id), + creator = it.creator?.let(::UserId), + name = it.displayName, + rawName = it.rawName, + topic = it.topic, + avatarUrl = it.avatarUrl, + isPublic = it.isPublic, + isDirect = it.isDirect, + isEncrypted = when (it.encryptionState) { + EncryptionState.ENCRYPTED -> true + EncryptionState.NOT_ENCRYPTED -> false + EncryptionState.UNKNOWN -> null + }, + joinRule = it.joinRule?.map(), + isSpace = it.isSpace, + isFavorite = it.isFavourite, + canonicalAlias = it.canonicalAlias?.let(::RoomAlias), + alternativeAliases = it.alternativeAliases.map(::RoomAlias).toImmutableList(), + currentUserMembership = it.membership.map(), + inviter = it.inviter?.let(RoomMemberMapper::map), + activeMembersCount = it.activeMembersCount.toLong(), + invitedMembersCount = it.invitedMembersCount.toLong(), + joinedMembersCount = it.joinedMembersCount.toLong(), + roomPowerLevels = it.powerLevels?.let(::mapPowerLevels), + highlightCount = it.highlightCount.toLong(), + notificationCount = it.notificationCount.toLong(), + userDefinedNotificationMode = it.cachedUserDefinedNotificationMode?.map(), + hasRoomCall = it.hasRoomCall, + activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(), + heroes = it.elementHeroes().toImmutableList(), + pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(), + isMarkedUnread = it.isMarkedUnread, + numUnreadMessages = it.numUnreadMessages.toLong(), + numUnreadMentions = it.numUnreadMentions.toLong(), + numUnreadNotifications = it.numUnreadNotifications.toLong(), + historyVisibility = it.historyVisibility.map(), + successorRoom = it.successorRoom?.map(), + ) + } +} + +fun RustMembership.map(): CurrentUserMembership = when (this) { + RustMembership.INVITED -> CurrentUserMembership.INVITED + RustMembership.JOINED -> CurrentUserMembership.JOINED + RustMembership.LEFT -> CurrentUserMembership.LEFT + Membership.KNOCKED -> CurrentUserMembership.KNOCKED + RustMembership.BANNED -> CurrentUserMembership.BANNED +} + +fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) { + RustRoomNotificationMode.ALL_MESSAGES -> RoomNotificationMode.ALL_MESSAGES + RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE +} + +/** + * Map a RoomHero to a MatrixUser. There is not need to create a RoomHero type on the application side. + */ +fun RoomHero.map(): MatrixUser = MatrixUser( + userId = UserId(userId), + displayName = displayName, + avatarUrl = avatarUrl +) + +fun mapPowerLevels(roomPowerLevels: RustRoomPowerLevels): RoomPowerLevels { + return RoomPowerLevels( + values = RoomPowerLevelsValuesMapper.map(roomPowerLevels.values()), + users = roomPowerLevels.userPowerLevels().mapKeys { (key, _) -> UserId(key) }.toPersistentMap() + ) +} 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 new file mode 100644 index 00000000000..734531a7ccc --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -0,0 +1,299 @@ +/* + * 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. + */ + +package io.element.android.libraries.matrix.impl.room + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.coroutine.childScope +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.core.DeviceId +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.core.ThreadId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.RoomInfo +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.StateEventType +import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.matrix.impl.room.draft.into +import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher +import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper +import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsValuesMapper +import io.element.android.libraries.matrix.impl.room.tombstone.map +import io.element.android.libraries.matrix.impl.roomdirectory.map +import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType +import io.element.android.libraries.matrix.impl.util.mxCallbackFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.RoomInfoListener +import org.matrix.rustcomponents.sdk.use +import timber.log.Timber +import uniffi.matrix_sdk_base.EncryptionState +import org.matrix.rustcomponents.sdk.Room as InnerRoom + +class RustBaseRoom( + override val sessionId: SessionId, + internal val deviceId: DeviceId, + internal val innerRoom: InnerRoom, + coroutineDispatchers: CoroutineDispatchers, + private val roomSyncSubscriber: RoomSyncSubscriber, + private val roomMembershipObserver: RoomMembershipObserver, + sessionCoroutineScope: CoroutineScope, + roomInfoMapper: RoomInfoMapper, + initialRoomInfo: RoomInfo, +) : BaseRoom { + override val roomId = RoomId(innerRoom.id()) + + // Create a dispatcher for all room methods... + private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) + + // ...except getMember methods as it could quickly fill the roomDispatcher... + private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8) + + internal val roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher) + + override val membersStateFlow: StateFlow = roomMemberListFetcher.membersFlow + + override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId") + + override val roomInfoFlow: StateFlow = mxCallbackFlow { + innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener { + override fun call(roomInfo: org.matrix.rustcomponents.sdk.RoomInfo) { + channel.trySend(roomInfoMapper.map(roomInfo)) + } + }) + }.stateIn(roomCoroutineScope, started = SharingStarted.Lazily, initialValue = initialRoomInfo) + + override fun predecessorRoom(): PredecessorRoom? { + return innerRoom.predecessorRoom()?.map() + } + + override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId) + + override suspend fun updateMembers() { + val useCache = membersStateFlow.value is RoomMembersState.Unknown + val source = if (useCache) { + RoomMemberListFetcher.Source.CACHE_AND_SERVER + } else { + RoomMemberListFetcher.Source.SERVER + } + roomMemberListFetcher.fetchRoomMembers(source = source) + } + + override suspend fun getMembers(limit: Int) = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.members().use { + it.nextChunk(limit.toUInt()).orEmpty().map { roomMember -> + RoomMemberMapper.map(roomMember) + } + } + } + } + + override suspend fun getUpdatedMember(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + RoomMemberMapper.map(innerRoom.member(userId.value)) + } + } + + override fun close() = destroy() + + override fun destroy() { + innerRoom.destroy() + roomCoroutineScope.cancel() + } + + override suspend fun userDisplayName(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.memberDisplayName(userId.value) + } + } + + override suspend fun userRole(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + RoomMemberMapper.mapRole(innerRoom.suggestedRoleForUser(userId.value)) + } + } + + override suspend fun powerLevels(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { + RoomPowerLevelsValuesMapper.map(it.values()) + } + } + } + + override suspend fun userAvatarUrl(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.memberAvatarUrl(userId.value) + } + } + + override suspend fun leave(): Result = withContext(roomDispatcher) { + val membershipBeforeLeft = roomInfoFlow.value.currentUserMembership + runCatchingExceptions { + innerRoom.leave() + }.onSuccess { + roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft) + } + } + + override suspend fun join(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.join() + } + } + + override suspend fun forget(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.forget() + } + } + + override suspend fun canUserInvite(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { it.canUserInvite(userId.value) } + } + } + + override suspend fun canUserKick(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { it.canUserKick(userId.value) } + } + } + + override suspend fun canUserBan(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { it.canUserBan(userId.value) } + } + } + + override suspend fun canUserRedactOwn(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { it.canUserRedactOwn(userId.value) } + } + } + + override suspend fun canUserRedactOther(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { it.canUserRedactOther(userId.value) } + } + } + + override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { it.canUserSendState(userId.value, type.map()) } + } + } + + override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { it.canUserSendMessage(userId.value, type.map()) } + } + } + + override suspend fun canUserTriggerRoomNotification(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { it.canUserTriggerRoomNotification(userId.value) } + } + } + + override suspend fun canUserPinUnpin(userId: UserId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getPowerLevels().use { it.canUserPinUnpin(userId.value) } + } + } + + override suspend fun clearEventCacheStorage(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.clearEventCacheStorage() + } + } + + override suspend fun setIsFavorite(isFavorite: Boolean): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.setIsFavourite(isFavorite, null) + } + } + + override suspend fun markAsRead(receiptType: ReceiptType): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.markAsRead(receiptType.toRustReceiptType()) + } + } + + override suspend fun setUnreadFlag(isUnread: Boolean): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.setUnreadFlag(isUnread) + } + } + + override suspend fun getPermalink(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.matrixToPermalink() + } + } + + override suspend fun getPermalinkFor(eventId: EventId): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.matrixToEventPermalink(eventId.value) + } + } + + override suspend fun getRoomVisibility(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.getRoomVisibility().map() + } + } + + override suspend fun getUpdatedIsEncrypted(): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.latestEncryptionState() == EncryptionState.ENCRYPTED + } + } + + override suspend fun saveComposerDraft(composerDraft: ComposerDraft, threadRoot: ThreadId?): Result = withContext(roomDispatcher) { + runCatchingExceptions { + Timber.d("saveComposerDraft: $composerDraft into $roomId for thread root: $threadRoot") + innerRoom.saveComposerDraft(composerDraft.into(), threadRoot = threadRoot?.value) + } + } + + override suspend fun loadComposerDraft(threadRoot: ThreadId?): Result = withContext(roomDispatcher) { + runCatchingExceptions { + Timber.d("loadComposerDraft for $roomId with thread root: $threadRoot") + innerRoom.loadComposerDraft(threadRoot?.value)?.into() + } + } + + override suspend fun clearComposerDraft(threadRoot: ThreadId?): Result = withContext(roomDispatcher) { + runCatchingExceptions { + Timber.d("clearComposerDraft for $roomId with thread root: $threadRoot") + innerRoom.clearComposerDraft(threadRoot = threadRoot?.value) + } + } + + override suspend fun reportRoom(reason: String?): Result = withContext(roomDispatcher) { + runCatchingExceptions { + Timber.d("reportRoom $roomId") + innerRoom.reportRoom(reason) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt deleted file mode 100644 index ef570f5ae32..00000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ /dev/null @@ -1,881 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.matrix.impl.room - -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.core.coroutine.childScope -import io.element.android.libraries.core.extensions.mapFailure -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.matrix.api.core.DeviceId -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.ProgressCallback -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.SendHandle -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.TransactionId -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange -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 -import io.element.android.libraries.matrix.api.media.MediaUploadHandler -import io.element.android.libraries.matrix.api.media.VideoInfo -import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService -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.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState -import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState -import io.element.android.libraries.matrix.api.room.MessageEventType -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import io.element.android.libraries.matrix.api.room.StateEventType -import io.element.android.libraries.matrix.api.room.draft.ComposerDraft -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.knock.KnockRequest -import io.element.android.libraries.matrix.api.room.location.AssetType -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels -import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange -import io.element.android.libraries.matrix.api.room.roomNotificationSettings -import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility -import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId -import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver -import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings -import io.element.android.libraries.matrix.impl.core.RustSendHandle -import io.element.android.libraries.matrix.impl.mapper.map -import io.element.android.libraries.matrix.impl.room.draft.into -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.member.RoomMemberListFetcher -import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper -import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper -import io.element.android.libraries.matrix.impl.roomdirectory.map -import io.element.android.libraries.matrix.impl.timeline.RustTimeline -import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType -import io.element.android.libraries.matrix.impl.util.MessageEventContent -import io.element.android.libraries.matrix.impl.util.mxCallbackFlow -import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver -import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl -import io.element.android.services.toolbox.api.systemclock.SystemClock -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.AllowedMessageTypes -import org.matrix.rustcomponents.sdk.DateDividerMode -import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener -import org.matrix.rustcomponents.sdk.KnockRequestsListener -import org.matrix.rustcomponents.sdk.RoomInfo -import org.matrix.rustcomponents.sdk.RoomInfoListener -import org.matrix.rustcomponents.sdk.RoomListItem -import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType -import org.matrix.rustcomponents.sdk.TimelineConfiguration -import org.matrix.rustcomponents.sdk.TimelineFocus -import org.matrix.rustcomponents.sdk.TypingNotificationsListener -import org.matrix.rustcomponents.sdk.UserPowerLevelUpdate -import org.matrix.rustcomponents.sdk.WidgetCapabilities -import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider -import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions -import org.matrix.rustcomponents.sdk.use -import timber.log.Timber -import uniffi.matrix_sdk.RoomPowerLevelChanges -import java.io.File -import kotlin.coroutines.cancellation.CancellationException -import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange -import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest -import org.matrix.rustcomponents.sdk.Room as InnerRoom -import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline - -@Suppress("LargeClass") -class RustMatrixRoom( - override val sessionId: SessionId, - private val deviceId: DeviceId, - private val roomListItem: RoomListItem, - private val innerRoom: InnerRoom, - innerTimeline: InnerTimeline, - private val notificationSettingsService: NotificationSettingsService, - sessionCoroutineScope: CoroutineScope, - private val coroutineDispatchers: CoroutineDispatchers, - private val systemClock: SystemClock, - private val roomContentForwarder: RoomContentForwarder, - private val roomSyncSubscriber: RoomSyncSubscriber, - private val matrixRoomInfoMapper: MatrixRoomInfoMapper, - private val featureFlagService: FeatureFlagService, - private val roomMembershipObserver: RoomMembershipObserver, -) : MatrixRoom { - override val roomId = RoomId(innerRoom.id()) - - override val roomInfoFlow: Flow = mxCallbackFlow { - runCatching { innerRoom.roomInfo() } - .getOrNull() - ?.let(matrixRoomInfoMapper::map) - ?.let { initial -> - channel.trySend(initial) - } - innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener { - override fun call(roomInfo: RoomInfo) { - channel.trySend(matrixRoomInfoMapper.map(roomInfo)) - } - }) - } - - override val roomTypingMembersFlow: Flow> = mxCallbackFlow { - val initial = emptyList() - channel.trySend(initial) - innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener { - override fun call(typingUserIds: List) { - channel.trySend( - typingUserIds - .filter { it != sessionId.value } - .map(::UserId) - ) - } - }) - } - - override val identityStateChangesFlow: Flow> = mxCallbackFlow { - val initial = emptyList() - channel.trySend(initial) - innerRoom.subscribeToIdentityStatusChanges(object : IdentityStatusChangeListener { - override fun call(identityStatusChange: List) { - channel.trySend( - identityStatusChange.map { - IdentityStateChange( - userId = UserId(it.userId), - identityState = it.changedTo.map(), - ) - } - ) - } - }) - } - - override val knockRequestsFlow: Flow> = mxCallbackFlow { - innerRoom.subscribeToKnockRequests(object : KnockRequestsListener { - override fun call(joinRequests: List) { - val knockRequests = joinRequests.map { RustKnockRequest(it) } - channel.trySend(knockRequests) - } - }) - } - - // Create a dispatcher for all room methods... - private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) - - // ...except getMember methods as it could quickly fill the roomDispatcher... - private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8) - - override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId") - private val _syncUpdateFlow = MutableStateFlow(0L) - private val roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher) - - private val _roomNotificationSettingsStateFlow = MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) - override val roomNotificationSettingsStateFlow: StateFlow = _roomNotificationSettingsStateFlow - - override val liveTimeline = createTimeline(innerTimeline, mode = Timeline.Mode.LIVE) { - _syncUpdateFlow.value = systemClock.epochMillis() - } - - override val membersStateFlow: StateFlow = roomMemberListFetcher.membersFlow - - override val syncUpdateFlow: StateFlow = _syncUpdateFlow.asStateFlow() - - init { - val powerLevelChanges = roomInfoFlow.map { it.userPowerLevels }.distinctUntilChanged() - val membershipChanges = liveTimeline.membershipChangeEventReceived.onStart { emit(Unit) } - combine(membershipChanges, powerLevelChanges) { _, _ -> } - // Skip initial one - .drop(1) - // The new events should already be in the SDK cache, no need to fetch them from the server - .onEach { roomMemberListFetcher.fetchRoomMembers(source = RoomMemberListFetcher.Source.CACHE) } - .launchIn(roomCoroutineScope) - } - - override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId) - - override suspend fun timelineFocusedOnEvent(eventId: EventId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.timelineWithConfiguration( - configuration = TimelineConfiguration( - focus = TimelineFocus.Event( - eventId = eventId.value, - numContextEvents = 50u, - ), - allowedMessageTypes = AllowedMessageTypes.All, - internalIdPrefix = "focus_$eventId", - dateDividerMode = DateDividerMode.DAILY, - ) - ).let { inner -> - createTimeline(inner, mode = Timeline.Mode.FOCUSED_ON_EVENT) - } - }.mapFailure { - it.toFocusEventException() - }.onFailure { - if (it is CancellationException) { - throw it - } - } - } - - override suspend fun pinnedEventsTimeline(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.timelineWithConfiguration( - configuration = TimelineConfiguration( - focus = TimelineFocus.PinnedEvents( - maxEventsToLoad = 100u, - maxConcurrentRequests = 10u, - ), - allowedMessageTypes = AllowedMessageTypes.All, - internalIdPrefix = "pinned_events", - dateDividerMode = DateDividerMode.DAILY, - ) - ).let { inner -> - createTimeline(inner, mode = Timeline.Mode.PINNED_EVENTS) - } - }.onFailure { - if (it is CancellationException) { - throw it - } - } - } - - override suspend fun mediaTimeline( - eventId: EventId?, - ): Result = withContext(roomDispatcher) { - val focus = if (eventId != null) { - TimelineFocus.Event( - eventId = eventId.value, - numContextEvents = 50u, - ) - } else { - TimelineFocus.Live - } - runCatching { - innerRoom.timelineWithConfiguration( - configuration = TimelineConfiguration( - focus = focus, - allowedMessageTypes = AllowedMessageTypes.Only( - types = listOf( - RoomMessageEventMessageType.FILE, - RoomMessageEventMessageType.IMAGE, - RoomMessageEventMessageType.VIDEO, - RoomMessageEventMessageType.AUDIO, - ) - ), - internalIdPrefix = "MediaGallery_", - dateDividerMode = DateDividerMode.MONTHLY, - ) - ).let { inner -> - createTimeline(inner, mode = if (eventId != null) Timeline.Mode.FOCUSED_ON_EVENT else Timeline.Mode.MEDIA) - } - }.onFailure { - if (it is CancellationException) { - throw it - } - } - } - - override fun destroy() { - roomCoroutineScope.cancel() - liveTimeline.close() - } - - override val displayName: String - get() = runCatching { innerRoom.displayName() ?: "" }.getOrDefault("") - - override val topic: String? - get() = runCatching { innerRoom.topic() }.getOrDefault(null) - - override val avatarUrl: String? - get() = runCatching { roomListItem.avatarUrl() ?: innerRoom.avatarUrl() }.getOrDefault(null) - - override val isEncrypted: Boolean - get() = runCatching { innerRoom.isEncrypted() }.getOrDefault(false) - - override val canonicalAlias: RoomAlias? - get() = runCatching { innerRoom.canonicalAlias()?.let(::RoomAlias) }.getOrDefault(null) - - override val alternativeAliases: List - get() = runCatching { innerRoom.alternativeAliases().map { RoomAlias(it) } }.getOrDefault(emptyList()) - - override val isPublic: Boolean - get() = runCatching { innerRoom.isPublic() }.getOrDefault(false) - - override val isSpace: Boolean - get() = runCatching { innerRoom.isSpace() }.getOrDefault(false) - - override val isDirect: Boolean - get() = runCatching { innerRoom.isDirect() }.getOrDefault(false) - - override val joinedMemberCount: Long - get() = runCatching { innerRoom.joinedMembersCount().toLong() }.getOrDefault(0) - - override val activeMemberCount: Long - get() = runCatching { innerRoom.activeMembersCount().toLong() }.getOrDefault(0) - - override suspend fun updateMembers() { - val useCache = membersStateFlow.value is MatrixRoomMembersState.Unknown - val source = if (useCache) { - RoomMemberListFetcher.Source.CACHE_AND_SERVER - } else { - RoomMemberListFetcher.Source.SERVER - } - roomMemberListFetcher.fetchRoomMembers(source = source) - } - - override suspend fun getMembers(limit: Int) = withContext(roomDispatcher) { - runCatching { - innerRoom.members().use { - it.nextChunk(limit.toUInt()).orEmpty().map { roomMember -> - RoomMemberMapper.map(roomMember) - } - } - } - } - - override suspend fun getUpdatedMember(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - RoomMemberMapper.map(innerRoom.member(userId.value)) - } - } - - override suspend fun userDisplayName(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.memberDisplayName(userId.value) - } - } - - override suspend fun updateRoomNotificationSettings(): Result = withContext(roomDispatcher) { - val currentState = _roomNotificationSettingsStateFlow.value - val currentRoomNotificationSettings = currentState.roomNotificationSettings() - _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) - runCatching { - notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() - }.map { - _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(it) - }.onFailure { - _roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Error( - prevRoomNotificationSettings = currentRoomNotificationSettings, - failure = it - ) - } - } - - override suspend fun userRole(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - RoomMemberMapper.mapRole(innerRoom.suggestedRoleForUser(userId.value)) - } - } - - override suspend fun updateUsersRoles(changes: List): Result { - return runCatching { - val powerLevelChanges = changes.map { UserPowerLevelUpdate(it.userId.value, it.powerLevel) } - innerRoom.updatePowerLevelsForUsers(powerLevelChanges) - } - } - - override suspend fun powerLevels(): Result = withContext(roomDispatcher) { - runCatching { - RoomPowerLevelsMapper.map(innerRoom.getPowerLevels()) - } - } - - override suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result = withContext(roomDispatcher) { - runCatching { - val changes = RoomPowerLevelChanges( - ban = matrixRoomPowerLevels.ban, - invite = matrixRoomPowerLevels.invite, - kick = matrixRoomPowerLevels.kick, - redact = matrixRoomPowerLevels.redactEvents, - eventsDefault = matrixRoomPowerLevels.sendEvents, - roomName = matrixRoomPowerLevels.roomName, - roomAvatar = matrixRoomPowerLevels.roomAvatar, - roomTopic = matrixRoomPowerLevels.roomTopic, - ) - innerRoom.applyPowerLevelChanges(changes) - } - } - - override suspend fun resetPowerLevels(): Result = withContext(roomDispatcher) { - runCatching { - RoomPowerLevelsMapper.map(innerRoom.resetPowerLevels()) - } - } - - override suspend fun userAvatarUrl(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.memberAvatarUrl(userId.value) - } - } - - override suspend fun editMessage( - eventId: EventId, - body: String, - htmlBody: String?, - intentionalMentions: List - ): Result = withContext(roomDispatcher) { - runCatching { - MessageEventContent.from(body, htmlBody, intentionalMentions).use { newContent -> - innerRoom.edit(eventId.value, newContent) - } - } - } - - override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List): Result { - return liveTimeline.sendMessage(body, htmlBody, intentionalMentions) - } - - override suspend fun leave(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.leave() - }.onSuccess { - roomMembershipObserver.notifyUserLeftRoom(roomId) - } - } - - override suspend fun join(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.join() - } - } - - override suspend fun inviteUserById(id: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.inviteUserById(id.value) - } - } - - override suspend fun canUserInvite(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.canUserInvite(userId.value) - } - } - - override suspend fun canUserKick(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.canUserKick(userId.value) - } - } - - override suspend fun canUserBan(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.canUserBan(userId.value) - } - } - - override suspend fun canUserRedactOwn(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.canUserRedactOwn(userId.value) - } - } - - override suspend fun canUserRedactOther(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.canUserRedactOther(userId.value) - } - } - - override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.canUserSendState(userId.value, type.map()) - } - } - - override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.canUserSendMessage(userId.value, type.map()) - } - } - - override suspend fun canUserTriggerRoomNotification(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.canUserTriggerRoomNotification(userId.value) - } - } - - override suspend fun canUserPinUnpin(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.canUserPinUnpin(userId.value) - } - } - - override suspend fun sendImage( - file: File, - thumbnailFile: File?, - imageInfo: ImageInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - ): Result { - return liveTimeline.sendImage(file, thumbnailFile, imageInfo, caption, formattedCaption, progressCallback) - } - - override suspend fun sendVideo( - file: File, - thumbnailFile: File?, - videoInfo: VideoInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - ): Result { - return liveTimeline.sendVideo(file, thumbnailFile, videoInfo, caption, formattedCaption, progressCallback) - } - - override suspend fun sendAudio( - file: File, - audioInfo: AudioInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - ): Result { - return liveTimeline.sendAudio( - file = file, - audioInfo = audioInfo, - caption = caption, - formattedCaption = formattedCaption, - progressCallback = progressCallback, - ) - } - - override suspend fun sendFile( - file: File, - fileInfo: FileInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback?, - ): Result { - return liveTimeline.sendFile( - file, - fileInfo, - caption, - formattedCaption, - progressCallback, - ) - } - - override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result { - return liveTimeline.toggleReaction(emoji, eventOrTransactionId) - } - - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result { - return liveTimeline.forwardEvent(eventId, roomIds) - } - - override suspend fun cancelSend(transactionId: TransactionId): Result { - return liveTimeline.cancelSend(transactionId) - } - - override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.uploadAvatar(mimeType, data, null) - } - } - - override suspend fun removeAvatar(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.removeAvatar() - } - } - - override suspend fun setName(name: String): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.setName(name) - } - } - - override suspend fun setTopic(topic: String): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.setTopic(topic) - } - } - - override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason) - if (blockUserId != null) { - innerRoom.ignoreUser(blockUserId.value) - } - } - } - - override suspend fun clearEventCacheStorage(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.clearEventCacheStorage() - } - } - - override suspend fun kickUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.kickUser(userId.value, reason) - } - } - - override suspend fun banUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.banUser(userId.value, reason) - } - } - - override suspend fun unbanUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.unbanUser(userId.value, reason) - } - } - - override suspend fun setIsFavorite(isFavorite: Boolean): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.setIsFavourite(isFavorite, null) - } - } - - override suspend fun markAsRead(receiptType: ReceiptType): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.markAsRead(receiptType.toRustReceiptType()) - } - } - - override suspend fun setUnreadFlag(isUnread: Boolean): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.setUnreadFlag(isUnread) - } - } - - override suspend fun sendLocation( - body: String, - geoUri: String, - description: String?, - zoomLevel: Int?, - assetType: AssetType?, - ): Result { - return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType) - } - - override suspend fun createPoll( - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result { - return liveTimeline.createPoll(question, answers, maxSelections, pollKind) - } - - override suspend fun editPoll( - pollStartId: EventId, - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result { - return liveTimeline.editPoll(pollStartId, question, answers, maxSelections, pollKind) - } - - override suspend fun sendPollResponse( - pollStartId: EventId, - answers: List - ): Result { - return liveTimeline.sendPollResponse(pollStartId, answers) - } - - override suspend fun endPoll( - pollStartId: EventId, - text: String - ): Result { - return liveTimeline.endPoll(pollStartId, text) - } - - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - ): Result { - return liveTimeline.sendVoiceMessage(file, audioInfo, waveform, progressCallback) - } - - override suspend fun typingNotice(isTyping: Boolean) = withContext(roomDispatcher) { - runCatching { - innerRoom.typingNotice(isTyping) - } - } - - override suspend fun generateWidgetWebViewUrl( - widgetSettings: MatrixWidgetSettings, - clientId: String, - languageTag: String?, - theme: String?, - ) = withContext(roomDispatcher) { - runCatching { - widgetSettings.generateWidgetWebViewUrl(innerRoom, clientId, languageTag, theme) - } - } - - override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result { - return runCatching { - RustWidgetDriver( - widgetSettings = widgetSettings, - room = innerRoom, - widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider { - override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities { - return getElementCallRequiredPermissions(sessionId.value, deviceId.value) - } - }, - ) - } - } - - override suspend fun getPermalink(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.matrixToPermalink() - } - } - - override suspend fun getPermalinkFor(eventId: EventId): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.matrixToEventPermalink(eventId.value) - } - } - - override suspend fun sendCallNotificationIfNeeded(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.sendCallNotificationIfNeeded() - } - } - - override suspend fun setSendQueueEnabled(enabled: Boolean) { - withContext(roomDispatcher) { - Timber.d("setSendQueuesEnabled: $enabled") - runCatching { - innerRoom.enableSendQueue(enabled) - } - } - } - - override suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result = withContext(roomDispatcher) { - runCatching { - Timber.d("saveComposerDraft: $composerDraft into $roomId") - innerRoom.saveComposerDraft(composerDraft.into()) - } - } - - override suspend fun loadComposerDraft(): Result = withContext(roomDispatcher) { - runCatching { - Timber.d("loadComposerDraft for $roomId") - innerRoom.loadComposerDraft()?.into() - } - } - - override suspend fun clearComposerDraft(): Result = withContext(roomDispatcher) { - runCatching { - Timber.d("clearComposerDraft for $roomId") - innerRoom.clearComposerDraft() - } - } - - override suspend fun ignoreDeviceTrustAndResend(devices: Map>, sendHandle: SendHandle) = withContext(roomDispatcher) { - runCatching { - innerRoom.ignoreDeviceTrustAndResend( - devices = devices.entries.associate { entry -> - entry.key.value to entry.value.map { it.value } - }, - sendHandle = (sendHandle as RustSendHandle).inner, - ) - } - } - - override suspend fun withdrawVerificationAndResend(userIds: List, sendHandle: SendHandle) = withContext(roomDispatcher) { - runCatching { - innerRoom.withdrawVerificationAndResend( - userIds = userIds.map { it.value }, - sendHandle = (sendHandle as RustSendHandle).inner, - ) - } - } - - override suspend fun updateCanonicalAlias(canonicalAlias: RoomAlias?, alternativeAliases: List): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.updateCanonicalAlias(canonicalAlias?.value, alternativeAliases.map { it.value }) - } - } - - override suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.publishRoomAliasInRoomDirectory(roomAlias.value) - } - } - - override suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.removeRoomAliasFromRoomDirectory(roomAlias.value) - } - } - - override suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.updateRoomVisibility(roomVisibility.map()) - } - } - - override suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.updateHistoryVisibility(historyVisibility.map()) - } - } - - override suspend fun getRoomVisibility(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.getRoomVisibility().map() - } - } - - override suspend fun enableEncryption(): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.enableEncryption() - } - } - - override suspend fun updateJoinRule(joinRule: JoinRule): Result = withContext(roomDispatcher) { - runCatching { - innerRoom.updateJoinRules(joinRule.map()) - } - } - - private fun createTimeline( - timeline: InnerTimeline, - mode: Timeline.Mode, - onNewSyncedEvent: () -> Unit = {}, - ): Timeline { - val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$timeline") - return RustTimeline( - mode = mode, - matrixRoom = this, - inner = timeline, - systemClock = systemClock, - coroutineScope = timelineCoroutineScope, - dispatcher = roomDispatcher, - roomContentForwarder = roomContentForwarder, - onNewSyncedEvent = onNewSyncedEvent, - featureFlagsService = featureFlagService, - ) - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustPendingRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustPendingRoom.kt deleted file mode 100644 index f53cf957ac7..00000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustPendingRoom.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.room - -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.PendingRoom -import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import org.matrix.rustcomponents.sdk.RoomPreview - -class RustPendingRoom( - override val sessionId: SessionId, - override val roomId: RoomId, - private val inner: RoomPreview, - private val roomMembershipObserver: RoomMembershipObserver, -) : PendingRoom { - override suspend fun leave(): Result = runCatching { - inner.leave() - }.onSuccess { - roomMembershipObserver.notifyUserLeftRoom(roomId) - } - - override fun close() { - inner.destroy() - } -} 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 f19d75e7522..c842c258763 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 @@ -7,7 +7,6 @@ package io.element.android.libraries.matrix.impl.room -import androidx.collection.lruCache import io.element.android.appconfig.TimelineConfig import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -15,12 +14,12 @@ import io.element.android.libraries.matrix.api.core.DeviceId 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.notificationsettings.NotificationSettingsService -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.PendingRoom +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.awaitLoaded -import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline +import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper import io.element.android.libraries.matrix.impl.roomlist.roomOrNull import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope @@ -28,16 +27,16 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.RoomListException -import org.matrix.rustcomponents.sdk.RoomListItem +import org.matrix.rustcomponents.sdk.TimelineConfiguration +import org.matrix.rustcomponents.sdk.TimelineFilter +import org.matrix.rustcomponents.sdk.TimelineFocus import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService -private const val CACHE_SIZE = 16 -private val PENDING_MEMBERSHIPS = setOf(Membership.INVITED, Membership.KNOCKED) - class RustRoomFactory( private val sessionId: SessionId, private val deviceId: DeviceId, @@ -52,26 +51,11 @@ class RustRoomFactory( private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory, private val featureFlagService: FeatureFlagService, private val roomMembershipObserver: RoomMembershipObserver, + private val roomInfoMapper: RoomInfoMapper, ) { private val dispatcher = dispatchers.io.limitedParallelism(1) private val mutex = Mutex() - private var isDestroyed: Boolean = false - - private data class RustRoomReferences( - val roomListItem: RoomListItem, - val fullRoom: Room, - ) - - private val cache = lruCache( - maxSize = CACHE_SIZE, - onEntryRemoved = { evicted, roomId, oldRoom, _ -> - Timber.d("On room removed from cache: $roomId, evicted: $evicted") - oldRoom.roomListItem.close() - oldRoom.fullRoom.close() - } - ) - - private val matrixRoomInfoMapper = MatrixRoomInfoMapper() + private val isDestroyed: AtomicBoolean = AtomicBoolean(false) private val eventFilters = TimelineConfig.excludedEvents .takeIf { it.isNotEmpty() } @@ -83,98 +67,115 @@ class RustRoomFactory( withContext(NonCancellable + dispatcher) { mutex.withLock { Timber.d("Destroying room factory") - cache.evictAll() - isDestroyed = true + isDestroyed.set(true) } } } - suspend fun create(roomId: RoomId): MatrixRoom? = withContext(dispatcher) { + suspend fun getBaseRoom(roomId: RoomId): RustBaseRoom? = withContext(dispatcher) { mutex.withLock { - if (isDestroyed) { + if (isDestroyed.get()) { Timber.d("Room factory is destroyed, returning null for $roomId") return@withContext null } - var roomReferences: RustRoomReferences? = getRoomReferences(roomId) - if (roomReferences == null) { - // ... otherwise, lets wait for the SS to load all rooms and check again. - roomListService.allRooms.awaitLoaded() - roomReferences = getRoomReferences(roomId) - } - if (roomReferences == null) { - Timber.d("No room found for $roomId, returning null") - return@withContext null - } - val liveTimeline = roomReferences.fullRoom.timeline() - RustMatrixRoom( - sessionId = sessionId, - deviceId = deviceId, - roomListItem = roomReferences.roomListItem, - innerRoom = roomReferences.fullRoom, - innerTimeline = liveTimeline, - sessionCoroutineScope = sessionCoroutineScope, - notificationSettingsService = notificationSettingsService, - coroutineDispatchers = dispatchers, - systemClock = systemClock, - roomContentForwarder = roomContentForwarder, - roomSyncSubscriber = roomSyncSubscriber, - matrixRoomInfoMapper = matrixRoomInfoMapper, - featureFlagService = featureFlagService, - roomMembershipObserver = roomMembershipObserver, - ) + val room = awaitRoomInRoomList(roomId) ?: return@withContext null + getBaseRoom(room) } } - suspend fun createPendingRoom(roomId: RoomId): PendingRoom? = withContext(dispatcher) { - if (isDestroyed) { - Timber.d("Room factory is destroyed, returning null for $roomId") - return@withContext null - } - val roomListItem = innerRoomListService.roomOrNull(roomId.value) - if (roomListItem == null) { - Timber.d("Room not found for $roomId") - return@withContext null - } - if (roomListItem.membership() !in PENDING_MEMBERSHIPS) { - Timber.d("Room $roomId is not in pending state") - return@withContext null - } - val innerRoom = try { - roomListItem.previewRoom(via = emptyList()) - } catch (e: Exception) { - Timber.e(e, "Failed to get pending room for $roomId") - return@withContext null - } - RustPendingRoom( + private suspend fun getBaseRoom(sdkRoom: Room): RustBaseRoom { + val initialRoomInfo = sdkRoom.roomInfo() + return RustBaseRoom( sessionId = sessionId, - roomId = roomId, - inner = innerRoom, + deviceId = deviceId, + innerRoom = sdkRoom, + coroutineDispatchers = dispatchers, + roomSyncSubscriber = roomSyncSubscriber, roomMembershipObserver = roomMembershipObserver, + roomInfoMapper = roomInfoMapper, + initialRoomInfo = roomInfoMapper.map(initialRoomInfo), + sessionCoroutineScope = sessionCoroutineScope, ) } - private suspend fun getRoomReferences(roomId: RoomId): RustRoomReferences? { - cache[roomId]?.let { - Timber.d("Room found in cache for $roomId") - return it + suspend fun getJoinedRoomOrPreview(roomId: RoomId): GetRoomResult? = withContext(dispatcher) { + mutex.withLock { + if (isDestroyed.get()) { + Timber.d("Room factory is destroyed, returning null for $roomId") + return@withContext null + } + val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withContext null + + if (sdkRoom.membership() == Membership.JOINED) { + // Init the live timeline in the SDK from the Room + val timeline = sdkRoom.timelineWithConfiguration( + TimelineConfiguration( + focus = TimelineFocus.Live(hideThreadedEvents = false), + filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All, + internalIdPrefix = "live", + dateDividerMode = DateDividerMode.DAILY, + trackReadReceipts = true, + reportUtds = true, + ) + ) + + GetRoomResult.Joined( + JoinedRustRoom( + baseRoom = getBaseRoom(sdkRoom), + notificationSettingsService = notificationSettingsService, + roomContentForwarder = roomContentForwarder, + liveInnerTimeline = timeline, + coroutineDispatchers = dispatchers, + systemClock = systemClock, + featureFlagService = featureFlagService, + ) + ) + } else { + val preview = try { + sdkRoom.previewRoom(via = emptyList()) + } catch (e: Exception) { + Timber.e(e, "Failed to get room preview for $roomId") + return@withContext null + } + + GetRoomResult.NotJoined( + NotJoinedRustRoom( + sessionId = sessionId, + localRoom = getBaseRoom(sdkRoom), + previewInfo = RoomPreviewInfoMapper.map(preview.info()), + ) + ) + } } - val roomListItem = innerRoomListService.roomOrNull(roomId.value) - if (roomListItem == null) { - Timber.d("Room not found for $roomId") - return null + } + + /** + * Get the Rust room for a room, retrying after the room list is loaded if necessary. + */ + private suspend fun awaitRoomInRoomList(roomId: RoomId): Room? { + var sdkRoom = innerRoomListService.roomOrNull(roomId.value) + if (sdkRoom == null) { + // ... otherwise, lets wait for the SS to load all rooms and check again. + roomListService.allRooms.awaitLoaded() + sdkRoom = innerRoomListService.roomOrNull(roomId.value) } - val fullRoom = try { - roomListItem.fullRoomWithTimeline(filter = eventFilters) - } catch (e: RoomListException) { - Timber.e(e, "Failed to get full room with timeline for $roomId") + + if (sdkRoom == null) { + Timber.d("Room not found for $roomId") return null } - Timber.d("Got full room with timeline for $roomId") - return RustRoomReferences( - roomListItem = roomListItem, - fullRoom = fullRoom, - ).also { - cache.put(roomId, it) - } + + return sdkRoom } } + +sealed interface GetRoomResult { + data class Joined(val joinedRoom: JoinedRoom) : GetRoomResult + data class NotJoined(val notJoinedRoom: NotJoinedRustRoom) : GetRoomResult + + val room: BaseRoom? + get() = when (this) { + is Joined -> joinedRoom + is NotJoined -> notJoinedRoom.localRoom + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt index a93ffad91ed..98a2175df99 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt @@ -9,11 +9,13 @@ package io.element.android.libraries.matrix.impl.room.join import com.squareup.anvil.annotations.ContributesBinding import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.exception.ClientException +import io.element.android.libraries.matrix.api.exception.ErrorKind import io.element.android.libraries.matrix.api.room.join.JoinRoom -import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.impl.analytics.toAnalyticsJoinedRoom import io.element.android.services.analytics.api.AnalyticsService import javax.inject.Inject @@ -39,15 +41,19 @@ class DefaultJoinRoom @Inject constructor( is RoomIdOrAlias.Alias -> { client.joinRoomByIdOrAlias(roomIdOrAlias, serverNames = emptyList()) } - }.onSuccess { roomSummary -> - client.captureJoinedRoomAnalytics(roomSummary, trigger) + }.onSuccess { roomInfo -> + if (roomInfo != null) { + analyticsService.capture(roomInfo.toAnalyticsJoinedRoom(trigger)) + } + }.mapFailure { + if (it is ClientException.MatrixApi) { + when (it.kind) { + ErrorKind.Forbidden -> JoinRoom.Failures.UnauthorizedJoin + else -> it + } + } else { + it + } }.map { } } - - private suspend fun MatrixClient.captureJoinedRoomAnalytics(roomSummary: RoomSummary?, trigger: JoinedRoom.Trigger) { - if (roomSummary == null) return - getRoom(roomSummary.roomId)?.use { room -> - analyticsService.capture(room.toAnalyticsJoinedRoom(trigger)) - } - } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt index 70167ec45c7..02e405969b6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.impl.room.knock +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.knock.KnockRequest @@ -23,19 +24,19 @@ class RustKnockRequest( override val timestamp: Long? = inner.timestamp?.toLong() override val isSeen: Boolean = inner.isSeen - override suspend fun accept(): Result = runCatching { + override suspend fun accept(): Result = runCatchingExceptions { inner.actions.accept() } - override suspend fun decline(reason: String?): Result = runCatching { + override suspend fun decline(reason: String?): Result = runCatchingExceptions { inner.actions.decline(reason) } - override suspend fun declineAndBan(reason: String?): Result = runCatching { + override suspend fun declineAndBan(reason: String?): Result = runCatchingExceptions { inner.actions.declineAndBan(reason) } - override suspend fun markAsSeen(): Result = runCatching { + override suspend fun markAsSeen(): Result = runCatchingExceptions { inner.actions.markAsSeen() } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcher.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcher.kt index b349d05e3a7..187bead11e2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcher.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcher.kt @@ -7,8 +7,8 @@ package io.element.android.libraries.matrix.impl.room.member -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState 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.roomMembers import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -42,8 +42,8 @@ internal class RoomMemberListFetcher( private val updatedRoomMemberMutex = Mutex() private val roomId = room.id() - private val _membersFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) - val membersFlow: StateFlow = _membersFlow + private val _membersFlow = MutableStateFlow(RoomMembersState.Unknown) + val membersFlow: StateFlow = _membersFlow /** * Fetches the room members for the given room. @@ -75,16 +75,16 @@ internal class RoomMemberListFetcher( } } - private suspend fun MutableStateFlow.fetchCachedRoomMembers(asPendingState: Boolean = true) { + private suspend fun MutableStateFlow.fetchCachedRoomMembers(asPendingState: Boolean = true) { Timber.i("Loading cached members for room $roomId") try { // Send current member list with pending state to notify the UI that we are loading new members emit(pendingWithCurrentMembers()) val members = parseAndEmitMembers(room.membersNoSync()) val newState = if (asPendingState) { - MatrixRoomMembersState.Pending(prevRoomMembers = members) + RoomMembersState.Pending(prevRoomMembers = members) } else { - MatrixRoomMembersState.Ready(members) + RoomMembersState.Ready(members) } emit(newState) } catch (exception: CancellationException) { @@ -92,22 +92,22 @@ internal class RoomMemberListFetcher( throw exception } catch (exception: Exception) { Timber.e(exception, "Failed to load cached members for room $roomId") - emit(MatrixRoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())) + emit(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())) } } - private suspend fun MutableStateFlow.fetchRemoteRoomMembers() { + private suspend fun MutableStateFlow.fetchRemoteRoomMembers() { try { // Send current member list with pending state to notify the UI that we are loading new members emit(pendingWithCurrentMembers()) // Start loading new members - emit(MatrixRoomMembersState.Ready(parseAndEmitMembers(room.members()))) + emit(RoomMembersState.Ready(parseAndEmitMembers(room.members()))) } catch (exception: CancellationException) { Timber.d("Cancelled loading updated members for room $roomId") throw exception } catch (exception: Exception) { Timber.e(exception, "Failed to load updated members for room $roomId") - emit(MatrixRoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())) + emit(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())) } } @@ -129,5 +129,5 @@ internal class RoomMemberListFetcher( } } - private fun pendingWithCurrentMembers() = MatrixRoomMembersState.Pending(_membersFlow.value.roomMembers().orEmpty().toImmutableList()) + private fun pendingWithCurrentMembers() = RoomMembersState.Pending(_membersFlow.value.roomMembers().orEmpty().toImmutableList()) } 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 5df7b6044b4..99fec55a02e 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 @@ -25,6 +25,7 @@ object RoomMemberMapper { normalizedPowerLevel = roomMember.normalizedPowerLevel, isIgnored = roomMember.isIgnored, role = mapRole(roomMember.suggestedRoleForPowerLevel), + membershipChangeReason = roomMember.membershipChangeReason ) fun mapRole(role: RoomMemberRole): RoomMember.Role = diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/message/ReplyParameters.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/message/ReplyParameters.kt new file mode 100644 index 00000000000..415d493e7d4 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/message/ReplyParameters.kt @@ -0,0 +1,16 @@ +/* + * 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.matrix.impl.room.message + +import io.element.android.libraries.matrix.api.room.message.ReplyParameters + +fun ReplyParameters.map() = org.matrix.rustcomponents.sdk.ReplyParameters( + eventId = inReplyToEventId.value, + enforceThread = enforceThreadReply, + replyWithinThread = replyWithinThread, +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapper.kt deleted file mode 100644 index d7babcfd2b4..00000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapper.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.room.powerlevels - -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels -import org.matrix.rustcomponents.sdk.RoomPowerLevels as RustRoomPowerLevels - -object RoomPowerLevelsMapper { - fun map(roomPowerLevels: RustRoomPowerLevels): MatrixRoomPowerLevels { - return MatrixRoomPowerLevels( - ban = roomPowerLevels.ban, - invite = roomPowerLevels.invite, - kick = roomPowerLevels.kick, - sendEvents = roomPowerLevels.eventsDefault, - redactEvents = roomPowerLevels.redact, - roomName = roomPowerLevels.roomName, - roomAvatar = roomPowerLevels.roomAvatar, - roomTopic = roomPowerLevels.roomTopic - ) - } -} 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 new file mode 100644 index 00000000000..840a5da4281 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapper.kt @@ -0,0 +1,26 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.room.powerlevels + +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import org.matrix.rustcomponents.sdk.RoomPowerLevelsValues as RustRoomPowerLevelsValues + +object RoomPowerLevelsValuesMapper { + fun map(values: RustRoomPowerLevelsValues): RoomPowerLevelsValues { + return RoomPowerLevelsValues( + ban = values.ban, + invite = values.invite, + kick = values.kick, + sendEvents = values.eventsDefault, + redactEvents = values.redact, + roomName = values.roomName, + roomAvatar = values.roomAvatar, + roomTopic = values.roomTopic, + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewInfoMapper.kt index c80ce425297..cb1867ec8e1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewInfoMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewInfoMapper.kt @@ -11,9 +11,8 @@ import io.element.android.libraries.core.bool.orFalse 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.preview.RoomPreviewInfo +import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.map -import org.matrix.rustcomponents.sdk.JoinRule -import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.RoomPreviewInfo as RustRoomPreviewInfo object RoomPreviewInfoMapper { @@ -27,10 +26,8 @@ object RoomPreviewInfoMapper { numberOfJoinedMembers = info.numJoinedMembers.toLong(), roomType = info.roomType.map(), isHistoryWorldReadable = info.isHistoryWorldReadable.orFalse(), - isJoined = info.membership == Membership.JOINED, - isInvited = info.membership == Membership.INVITED, - isPublic = info.joinRule == JoinRule.Public, - canKnock = info.joinRule == JoinRule.Knock + membership = info.membership?.map(), + joinRule = info.joinRule?.map(), ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/PredecessorRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/PredecessorRoom.kt new file mode 100644 index 00000000000..08daee4f19a --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/PredecessorRoom.kt @@ -0,0 +1,20 @@ +/* + * 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.matrix.impl.room.tombstone + +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.tombstone.PredecessorRoom +import org.matrix.rustcomponents.sdk.PredecessorRoom as RustPredecessorRoom + +fun RustPredecessorRoom.map(): PredecessorRoom { + return PredecessorRoom( + roomId = RoomId(roomId), + lastEventId = EventId(lastEventId), + ) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/SuccessorRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/SuccessorRoom.kt new file mode 100644 index 00000000000..afc0ee96afa --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tombstone/SuccessorRoom.kt @@ -0,0 +1,19 @@ +/* + * 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.matrix.impl.room.tombstone + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom +import org.matrix.rustcomponents.sdk.SuccessorRoom as RustSuccessorRoom + +fun RustSuccessorRoom.map(): SuccessorRoom { + return SuccessorRoom( + roomId = RoomId(roomId), + reason = reason + ) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index 9fddea7c4fe..8cdcd13f224 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind import org.matrix.rustcomponents.sdk.RoomListEntriesListener import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListInterface -import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener import org.matrix.rustcomponents.sdk.RoomListServiceInterface @@ -114,9 +114,9 @@ internal fun RoomListServiceInterface.syncIndicator(): Flow + innerRoomList.let { innerRoomList -> innerRoomList.entriesFlow( pageSize = pageSize, roomListDynamicEvents = dynamicEvents, - initialFilterKind = RoomListEntriesDynamicFilterKind.NonLeft + initialFilterKind = RoomListEntriesDynamicFilterKind.All(ROOM_LIST_RUST_FILTERS), ).onEach { update -> processor.postUpdate(update) }.launchIn(this) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListItemExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListItemExtensions.kt deleted file mode 100644 index ebb2d7e0c58..00000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListItemExtensions.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.roomlist - -import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.RoomListItem -import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter - -/** Returns a `Room` with an initialized timeline using the given [filter]. */ -suspend fun RoomListItem.fullRoomWithTimeline(filter: TimelineEventTypeFilter? = null): Room { - if (!isTimelineInitialized()) { - initTimeline(filter, "live") - } - return fullRoom() -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryFactory.kt index 8dd58878c3b..69f48a578e9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryFactory.kt @@ -8,18 +8,18 @@ package io.element.android.libraries.matrix.impl.roomlist import io.element.android.libraries.matrix.api.roomlist.RoomSummary -import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper +import io.element.android.libraries.matrix.impl.room.RoomInfoMapper import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory -import org.matrix.rustcomponents.sdk.RoomListItem +import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.use class RoomSummaryFactory( private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(), - private val roomInfoMapper: MatrixRoomInfoMapper = MatrixRoomInfoMapper(), + private val roomInfoMapper: RoomInfoMapper = RoomInfoMapper(), ) { - suspend fun create(roomListItem: RoomListItem): RoomSummary { - val roomInfo = roomListItem.roomInfo().let(roomInfoMapper::map) - val latestRoomMessage = roomListItem.latestEvent().use { event -> + suspend fun create(room: Room): RoomSummary { + val roomInfo = room.roomInfo().let(roomInfoMapper::map) + val latestRoomMessage = room.latestEvent().use { event -> roomMessageFactory.create(event) } return RoomSummary( 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 ac8992e23ff..66931a1f138 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 @@ -12,8 +12,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate -import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListServiceInterface import org.matrix.rustcomponents.sdk.use import timber.log.Timber @@ -95,20 +95,16 @@ class RoomSummaryListProcessor( } } - private suspend fun buildSummaryForRoomListEntry(entry: RoomListItem): RoomSummary { - return buildRoomSummaryForRoomListItem(entry) + private suspend fun buildSummaryForRoomListEntry(entry: Room): RoomSummary { + return entry.use { roomSummaryDetailsFactory.create(room = it) } } private suspend fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary? { - return roomListService.roomOrNull(identifier)?.use { roomListItem -> - buildRoomSummaryForRoomListItem(roomListItem) + return roomListService.roomOrNull(identifier)?.let { room -> + buildSummaryForRoomListEntry(room) } } - private suspend fun buildRoomSummaryForRoomListItem(roomListItem: RoomListItem): RoomSummary { - return roomSummaryDetailsFactory.create(roomListItem = roomListItem) - } - private suspend fun updateRoomSummaries(block: suspend MutableList.() -> Unit) = withContext(coroutineContext) { mutex.withLock { val current = roomSummaries.replayCache.lastOrNull() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt index dffe6f1b710..24baa79dfe1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt @@ -7,6 +7,8 @@ package io.element.android.libraries.matrix.impl.sync +import io.element.android.libraries.core.coroutine.mapState +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import kotlinx.coroutines.CoroutineDispatcher @@ -32,10 +34,10 @@ class RustSyncService( private val isServiceReady = AtomicBoolean(true) override suspend fun startSync() = withContext(dispatcher) { - runCatching { + runCatchingExceptions { if (!isServiceReady.get()) { Timber.d("Can't start sync: service is not ready") - return@runCatching + return@runCatchingExceptions } Timber.i("Start sync") inner.start() @@ -45,10 +47,10 @@ class RustSyncService( } override suspend fun stopSync() = withContext(dispatcher) { - runCatching { + runCatchingExceptions { if (!isServiceReady.get()) { Timber.d("Can't stop sync: service is not ready") - return@runCatching + return@runCatchingExceptions } Timber.i("Stop sync") inner.stop() @@ -62,6 +64,7 @@ class RustSyncService( stopSync() Timber.d("Destroying sync service") isServiceReady.set(false) + inner.destroy() } override val syncState: StateFlow = @@ -72,4 +75,6 @@ class RustSyncService( } .distinctUntilChanged() .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle) + + override val isOnline: StateFlow = syncState.mapState { it != SyncState.Offline } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SlidingSyncVersion.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SlidingSyncVersion.kt index ce1f1d025c1..6284e0028f3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SlidingSyncVersion.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SlidingSyncVersion.kt @@ -12,8 +12,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncVersion as RustSlidingSyncVersio internal fun RustSlidingSyncVersion.map(): SlidingSyncVersion { return when (this) { - RustSlidingSyncVersion.None -> SlidingSyncVersion.None - is RustSlidingSyncVersion.Proxy -> SlidingSyncVersion.Proxy - RustSlidingSyncVersion.Native -> SlidingSyncVersion.Native + RustSlidingSyncVersion.NONE -> SlidingSyncVersion.None + RustSlidingSyncVersion.NATIVE -> SlidingSyncVersion.Native } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt index 2ad364c1e16..2088b6bc1d0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt @@ -28,14 +28,6 @@ internal class MatrixTimelineDiffProcessor( private val _membershipChangeEventReceived = MutableSharedFlow(extraBufferCapacity = 1) val membershipChangeEventReceived: Flow = _membershipChangeEventReceived - suspend fun postItems(items: List) { - updateTimelineItems { - Timber.v("Update timeline items from postItems (with ${items.size} items) on ${Thread.currentThread()}") - val mappedItems = items.map { it.asMatrixTimelineItem() } - addAll(0, mappedItems) - } - } - suspend fun postDiffs(diffs: List) { updateTimelineItems { Timber.v("Update timeline items from postDiffs (with ${diffs.size} items) on ${Thread.currentThread()}") diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt index 0d79d0911e3..49fe7d90cca 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt @@ -20,11 +20,11 @@ import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineInterface import org.matrix.rustcomponents.sdk.TimelineListener import timber.log.Timber -import uniffi.matrix_sdk_ui.LiveBackPaginationStatus +import uniffi.matrix_sdk.RoomPaginationStatus -internal fun TimelineInterface.liveBackPaginationStatus(): Flow = callbackFlow { +internal fun TimelineInterface.liveBackPaginationStatus(): Flow = callbackFlow { val listener = object : PaginationStatusListener { - override fun onUpdate(status: LiveBackPaginationStatus) { + override fun onUpdate(status: RoomPaginationStatus) { trySend(status) } } 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 2cb1a4efa50..404de3a248f 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 @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.impl.timeline +import io.element.android.libraries.core.extensions.runCatchingExceptions 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 @@ -19,9 +20,10 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler 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.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline @@ -35,6 +37,7 @@ import io.element.android.libraries.matrix.impl.media.toMSC3246range import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.location.toInner +import io.element.android.libraries.matrix.impl.room.message.map 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 @@ -45,16 +48,16 @@ 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.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.launchIn @@ -70,9 +73,10 @@ import org.matrix.rustcomponents.sdk.MessageFormat import org.matrix.rustcomponents.sdk.PollData import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.UploadParameters +import org.matrix.rustcomponents.sdk.UploadSource import org.matrix.rustcomponents.sdk.use import timber.log.Timber -import uniffi.matrix_sdk_ui.LiveBackPaginationStatus +import uniffi.matrix_sdk.RoomPaginationStatus import java.io.File import org.matrix.rustcomponents.sdk.EventOrTransactionId as RustEventOrTransactionId import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline @@ -83,16 +87,13 @@ class RustTimeline( private val inner: InnerTimeline, mode: Timeline.Mode, systemClock: SystemClock, - private val matrixRoom: MatrixRoom, + private val joinedRoom: JoinedRoom, private val coroutineScope: CoroutineScope, private val dispatcher: CoroutineDispatcher, private val roomContentForwarder: RoomContentForwarder, private val featureFlagsService: FeatureFlagService, onNewSyncedEvent: () -> Unit, ) : Timeline { - private val initLatch = CompletableDeferred() - private val isTimelineInitialized = MutableStateFlow(false) - private val _timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) @@ -114,8 +115,6 @@ class RustTimeline( timeline = inner, timelineCoroutineScope = coroutineScope, timelineDiffProcessor = timelineDiffProcessor, - initLatch = initLatch, - isTimelineInitialized = isTimelineInitialized, dispatcher = dispatcher, onNewSyncedEvent = onNewSyncedEvent, ) @@ -125,16 +124,19 @@ class RustTimeline( private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode) private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode) - private val backPaginationStatus = MutableStateFlow( + override val backwardPaginationStatus = MutableStateFlow( Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS) ) - private val forwardPaginationStatus = MutableStateFlow( + override val forwardPaginationStatus = MutableStateFlow( Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode == Timeline.Mode.FOCUSED_ON_EVENT) ) init { - coroutineScope.fetchMembers() + if (mode != Timeline.Mode.PINNED_EVENTS) { + coroutineScope.fetchMembers() + } + if (mode == Timeline.Mode.LIVE) { // When timeline is live, we need to listen to the back pagination status as // sdk can automatically paginate backwards. @@ -147,8 +149,8 @@ class RustTimeline( .onEach { backPaginationStatus -> updatePaginationStatus(Timeline.PaginationDirection.BACKWARDS) { when (backPaginationStatus) { - is LiveBackPaginationStatus.Idle -> it.copy(isPaginating = false, hasMoreToLoad = !backPaginationStatus.hitStartOfTimeline) - is LiveBackPaginationStatus.Paginating -> it.copy(isPaginating = true, hasMoreToLoad = true) + is RoomPaginationStatus.Idle -> it.copy(isPaginating = false, hasMoreToLoad = !backPaginationStatus.hitTimelineStart) + is RoomPaginationStatus.Paginating -> it.copy(isPaginating = true, hasMoreToLoad = true) } } } @@ -158,14 +160,14 @@ class RustTimeline( override val membershipChangeEventReceived: Flow = timelineDiffProcessor.membershipChangeEventReceived override suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.sendReadReceipt(receiptType.toRustReceiptType(), eventId.value) } } private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus) -> Timeline.PaginationStatus) { when (direction) { - Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.getAndUpdate(update) + Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.getAndUpdate(update) Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update) } } @@ -173,8 +175,7 @@ class RustTimeline( // Use NonCancellable to avoid breaking the timeline when the coroutine is cancelled. override suspend fun paginate(direction: Timeline.PaginationDirection): Result = withContext(NonCancellable) { withContext(dispatcher) { - initLatch.await() - runCatching { + runCatchingExceptions { if (!canPaginate(direction)) throw TimelineException.CannotPaginate updatePaginationStatus(direction) { it.copy(isPaginating = true) } when (direction) { @@ -183,10 +184,10 @@ class RustTimeline( } }.onFailure { error -> if (error is TimelineException.CannotPaginate) { - Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}") + Timber.d("Can't paginate $direction on room ${joinedRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}") } else { updatePaginationStatus(direction) { it.copy(isPaginating = false) } - Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}") + Timber.e(error, "Error paginating $direction on room ${joinedRoom.roomId}") } }.onSuccess { hasReachedEnd -> updatePaginationStatus(direction) { it.copy(isPaginating = false, hasMoreToLoad = !hasReachedEnd) } @@ -195,37 +196,27 @@ class RustTimeline( } private fun canPaginate(direction: Timeline.PaginationDirection): Boolean { - if (!isTimelineInitialized.value) return false return when (direction) { - Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.value.canPaginate + Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.value.canPaginate Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.value.canPaginate } } - override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow { - return when (direction) { - Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus - Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus - } - } - override val timelineItems: Flow> = combine( _timelineItems, - backPaginationStatus, + backwardPaginationStatus, forwardPaginationStatus, - matrixRoom.roomInfoFlow.map { it.creator }, - isTimelineInitialized, + joinedRoom.roomInfoFlow.map { it.creator to it.isDm }.distinctUntilChanged(), ) { timelineItems, backwardPaginationStatus, forwardPaginationStatus, - roomCreator, - isTimelineInitialized -> + (roomCreator, isDm) -> withContext(dispatcher) { timelineItems .let { items -> roomBeginningPostProcessor.process( items = items, - isDm = matrixRoom.isDm, + isDm = isDm, roomCreator = roomCreator, hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad, ) @@ -233,7 +224,6 @@ class RustTimeline( .let { items -> loadingIndicatorsPostProcessor.process( items = items, - isTimelineInitialized = isTimelineInitialized, hasMoreToLoadBackward = backwardPaginationStatus.hasMoreToLoad, hasMoreToLoadForward = forwardPaginationStatus.hasMoreToLoad, ) @@ -243,10 +233,7 @@ class RustTimeline( } // Keep lastForwardIndicatorsPostProcessor last .let { items -> - lastForwardIndicatorsPostProcessor.process( - items = items, - isTimelineInitialized = isTimelineInitialized, - ) + lastForwardIndicatorsPostProcessor.process(items = items) } } }.onStart { @@ -261,11 +248,10 @@ class RustTimeline( } private fun CoroutineScope.fetchMembers() = launch(dispatcher) { - initLatch.await() try { inner.fetchMembers() } catch (exception: Exception) { - Timber.e(exception, "Error fetching members for room ${matrixRoom.roomId}") + Timber.e(exception, "Error fetching members for room ${joinedRoom.roomId}") } } @@ -275,14 +261,14 @@ class RustTimeline( intentionalMentions: List, ): Result = withContext(dispatcher) { MessageEventContent.from(body, htmlBody, intentionalMentions).use { content -> - runCatching { + runCatchingExceptions { inner.send(content) } } } override suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.redactEvent( eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(), reason = reason, @@ -296,7 +282,7 @@ class RustTimeline( htmlBody: String?, intentionalMentions: List, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { val editedContent = EditedContent.RoomMessage( content = MessageEventContent.from( body = body, @@ -316,7 +302,7 @@ class RustTimeline( caption: String?, formattedCaption: String?, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { val editedContent = EditedContent.MediaCaption( caption = caption, formattedCaption = formattedCaption?.let { @@ -324,23 +310,28 @@ class RustTimeline( }, mentions = null, ) - inner.edit( - newContent = editedContent, - eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(), - ) + withContext(Dispatchers.IO) { + inner.edit( + newContent = editedContent, + eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(), + ) + } } } override suspend fun replyMessage( - eventId: EventId, + replyParameters: ReplyParameters, body: String, htmlBody: String?, intentionalMentions: List, fromNotification: Boolean, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { val msg = MessageEventContent.from(body, htmlBody, intentionalMentions) - inner.sendReply(msg, eventId.value) + inner.sendReply( + msg = msg, + replyParams = replyParameters.map(), + ) } } @@ -351,18 +342,20 @@ class RustTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOfNotNull(file, thumbnailFile)) { inner.sendImage( params = UploadParameters( - filename = file.path, + source = UploadSource.File(file.path), caption = caption, formattedCaption = formattedCaption?.let { FormattedBody(body = it, format = MessageFormat.Html) }, useSendQueue = useSendQueue, mentions = null, + replyParams = replyParameters?.map(), ), thumbnailPath = thumbnailFile?.path, imageInfo = imageInfo.map(), @@ -378,18 +371,20 @@ class RustTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOfNotNull(file, thumbnailFile)) { inner.sendVideo( params = UploadParameters( - filename = file.path, + source = UploadSource.File(file.path), caption = caption, formattedCaption = formattedCaption?.let { FormattedBody(body = it, format = MessageFormat.Html) }, useSendQueue = useSendQueue, mentions = null, + replyParams = replyParameters?.map(), ), thumbnailPath = thumbnailFile?.path, videoInfo = videoInfo.map(), @@ -404,18 +399,20 @@ class RustTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOf(file)) { inner.sendAudio( params = UploadParameters( - filename = file.path, + source = UploadSource.File(file.path), caption = caption, formattedCaption = formattedCaption?.let { FormattedBody(body = it, format = MessageFormat.Html) }, useSendQueue = useSendQueue, mentions = null, + replyParams = replyParameters?.map(), ), audioInfo = audioInfo.map(), progressWatcher = progressCallback?.toProgressWatcher() @@ -429,18 +426,20 @@ class RustTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) return sendAttachment(listOf(file)) { inner.sendFile( params = UploadParameters( - filename = file.path, + source = UploadSource.File(file.path), caption = caption, formattedCaption = formattedCaption?.let { FormattedBody(body = it, format = MessageFormat.Html) }, useSendQueue = useSendQueue, mentions = null, + replyParams = replyParameters?.map(), ), fileInfo = fileInfo.map(), progressWatcher = progressCallback?.toProgressWatcher(), @@ -449,7 +448,7 @@ class RustTimeline( } override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.toggleReaction( key = emoji, itemId = eventOrTransactionId.toRustEventOrTransactionId(), @@ -458,7 +457,7 @@ class RustTimeline( } override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { roomContentForwarder.forward(fromTimeline = inner, eventId = eventId, toRoomIds = roomIds) }.onFailure { Timber.e(it) @@ -471,14 +470,42 @@ class RustTimeline( description: String?, zoomLevel: Int?, assetType: AssetType?, + replyParameters: ReplyParameters?, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.sendLocation( body = body, geoUri = geoUri, description = description, zoomLevel = zoomLevel?.toUByte(), assetType = assetType?.toInner(), + replyParams = replyParameters?.map(), + ) + } + } + + override suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, + ): Result { + val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) + return sendAttachment(listOf(file)) { + inner.sendVoiceMessage( + params = UploadParameters( + source = UploadSource.File(file.path), + // Maybe allow a caption in the future? + caption = null, + formattedCaption = null, + useSendQueue = useSendQueue, + mentions = null, + replyParams = replyParameters?.map(), + ), + audioInfo = audioInfo.map(), + waveform = waveform.toMSC3246range(), + progressWatcher = progressCallback?.toProgressWatcher(), ) } } @@ -489,7 +516,7 @@ class RustTimeline( maxSelections: Int, pollKind: PollKind, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.createPoll( question = question, answers = answers, @@ -506,7 +533,7 @@ class RustTimeline( maxSelections: Int, pollKind: PollKind, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { val editedContent = EditedContent.PollStart( pollData = PollData( question = question, @@ -519,14 +546,14 @@ class RustTimeline( newContent = editedContent, eventOrTransactionId = RustEventOrTransactionId.EventId(pollStartId.value), ) - }.map { } + } } override suspend fun sendPollResponse( pollStartId: EventId, answers: List ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.sendPollResponse( pollStartEventId = pollStartId.value, answers = answers, @@ -538,7 +565,7 @@ class RustTimeline( pollStartId: EventId, text: String ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.endPoll( pollStartEventId = pollStartId.value, text = text, @@ -546,32 +573,8 @@ class RustTimeline( } } - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - ): Result { - val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) - return sendAttachment(listOf(file)) { - inner.sendVoiceMessage( - params = UploadParameters( - filename = file.path, - // Maybe allow a caption in the future? - caption = null, - formattedCaption = null, - useSendQueue = useSendQueue, - mentions = null, - ), - audioInfo = audioInfo.map(), - waveform = waveform.toMSC3246range(), - progressWatcher = progressCallback?.toProgressWatcher(), - ) - } - } - private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { - return runCatching { + return runCatchingExceptions { MediaUploadHandlerImpl(files, handle()) } } @@ -594,19 +597,19 @@ class RustTimeline( } override suspend fun pinEvent(eventId: EventId): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.pinEvent(eventId = eventId.value) } } override suspend fun unpinEvent(eventId: EventId): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.unpinEvent(eventId = eventId.value) } } private suspend fun fetchDetailsForEvent(eventId: EventId): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.fetchDetailsForEvent(eventId.value) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt index c286a5fec3c..3d95d660ff0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt @@ -8,37 +8,26 @@ package io.element.android.libraries.matrix.impl.timeline import io.element.android.libraries.core.coroutine.childScope -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.matrix.rustcomponents.sdk.Timeline -import org.matrix.rustcomponents.sdk.TimelineChange -import org.matrix.rustcomponents.sdk.TimelineDiff -import org.matrix.rustcomponents.sdk.TimelineItem import uniffi.matrix_sdk_ui.EventItemOrigin -private const val INITIAL_MAX_SIZE = 50 - /** * This class is responsible for subscribing to a timeline and post the items/diffs to the timelineDiffProcessor. * It will also trigger a callback when a new synced event is received. - * It will also handle the initial items and make sure they are posted before any diff. */ internal class TimelineItemsSubscriber( timelineCoroutineScope: CoroutineScope, dispatcher: CoroutineDispatcher, private val timeline: Timeline, private val timelineDiffProcessor: MatrixTimelineDiffProcessor, - private val initLatch: CompletableDeferred, - private val isTimelineInitialized: MutableStateFlow, private val onNewSyncedEvent: () -> Unit, ) { private var subscriptionCount = 0 @@ -57,7 +46,7 @@ internal class TimelineItemsSubscriber( if (diffs.any { diff -> diff.eventOrigin() == EventItemOrigin.SYNC }) { onNewSyncedEvent() } - postDiffs(diffs) + timelineDiffProcessor.postDiffs(diffs) } .launchIn(coroutineScope) } @@ -78,35 +67,4 @@ internal class TimelineItemsSubscriber( } subscriptionCount-- } - - private suspend fun postItems(items: List) = coroutineScope { - if (items.isEmpty()) { - // Makes sure to post empty list if there is no item, so you can handle empty state. - timelineDiffProcessor.postItems(emptyList()) - } else { - // Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap. - items.chunked(INITIAL_MAX_SIZE).reversed().forEach { - ensureActive() - timelineDiffProcessor.postItems(it) - } - } - isTimelineInitialized.value = true - initLatch.complete(Unit) - } - - private suspend fun postDiffs(diffs: List) { - val diffsToProcess = diffs.toMutableList() - if (!isTimelineInitialized.value) { - val resetDiff = diffsToProcess.firstOrNull { it.change() == TimelineChange.RESET } - if (resetDiff != null) { - // Keep using the postItems logic so we can post the timelineItems asap. - postItems(resetDiff.reset() ?: emptyList()) - diffsToProcess.remove(resetDiff) - } - } - initLatch.await() - if (diffsToProcess.isNotEmpty()) { - timelineDiffProcessor.postDiffs(diffsToProcess) - } - } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 8c2f8645b29..d806c9492f5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -23,24 +23,28 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper +import org.matrix.rustcomponents.sdk.InReplyToDetails import org.matrix.rustcomponents.sdk.MessageType +import org.matrix.rustcomponents.sdk.MsgLikeKind import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody -import org.matrix.rustcomponents.sdk.MessageContent as Message import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat import org.matrix.rustcomponents.sdk.MessageType as RustMessageType +// https://github.com/Johennes/matrix-spec-proposals/blob/johannes/msgtype-galleries/proposals/4274-inline-media-galleries.md#unstable-prefix +private const val MSG_TYPE_GALLERY_UNSTABLE = "dm.filament.gallery" + class EventMessageMapper { private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) } - fun map(message: Message): MessageContent = message.use { - val type = it.msgType.use(this::mapMessageType) - val inReplyToEvent: InReplyTo? = it.inReplyTo?.use(inReplyToMapper::map) + fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, isThreaded: Boolean): MessageContent = message.use { + val type = it.content.msgType.use(this::mapMessageType) + val inReplyToEvent: InReplyTo? = inReplyTo?.use(inReplyToMapper::map) MessageContent( - body = it.body, + body = it.content.body, inReplyTo = inReplyToEvent, - isEdited = it.isEdited, - isThreaded = it.threadRoot != null, + isEdited = it.content.isEdited, + isThreaded = isThreaded, type = type ) } @@ -111,6 +115,10 @@ class EventMessageMapper { is MessageType.Other -> { OtherMessageType(type.msgtype, type.body) } + is MessageType.Gallery -> { + // TODO expose the GalleryType. + OtherMessageType(MSG_TYPE_GALLERY_UNSTABLE, type.content.body) + } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index 3e2817bf9ba..7cea0e29f4c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -28,6 +28,7 @@ import org.matrix.rustcomponents.sdk.EventOrTransactionId import org.matrix.rustcomponents.sdk.QueueWedgeError import org.matrix.rustcomponents.sdk.Reaction import org.matrix.rustcomponents.sdk.ShieldState +import org.matrix.rustcomponents.sdk.TimelineItemContent import uniffi.matrix_sdk_common.ShieldStateCode import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem @@ -48,7 +49,7 @@ class EventTimelineItemMapper( isOwn = isOwn, isRemote = isRemote, localSendState = localSendState?.map(), - reactions = reactions.map(), + reactions = (content as? TimelineItemContent.MsgLike)?.content?.reactions.map(), receipts = readReceipts.map(), sender = UserId(sender), senderProfile = senderProfile.map(), @@ -152,6 +153,7 @@ private fun RustEventItemOrigin.map(): TimelineItemEventOrigin { RustEventItemOrigin.LOCAL -> TimelineItemEventOrigin.LOCAL RustEventItemOrigin.SYNC -> TimelineItemEventOrigin.SYNC RustEventItemOrigin.PAGINATION -> TimelineItemEventOrigin.PAGINATION + RustEventItemOrigin.CACHE -> TimelineItemEventOrigin.CACHE } } @@ -174,6 +176,7 @@ private fun ShieldState?.map(): MessageShield? { ShieldStateCode.UNVERIFIED_IDENTITY -> MessageShield.UnverifiedIdentity(isCritical) ShieldStateCode.SENT_IN_CLEAR -> MessageShield.SentInClear(isCritical) ShieldStateCode.VERIFICATION_VIOLATION -> MessageShield.VerificationViolation(isCritical) + ShieldStateCode.MISMATCHED_SENDER -> MessageShield.MismatchedSender(isCritical) } } 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 19e4c36c6ff..a62f18ddd18 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 @@ -22,12 +22,12 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershi 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.UtdCause import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.poll.map import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap +import org.matrix.rustcomponents.sdk.MsgLikeKind import org.matrix.rustcomponents.sdk.TimelineItemContent import org.matrix.rustcomponents.sdk.use import uniffi.matrix_sdk_ui.RoomPinnedEventsChange @@ -55,8 +55,43 @@ class TimelineEventContentMapper( error = it.error ) } - is TimelineItemContent.Message -> { - eventMessageMapper.map(it.content) + is TimelineItemContent.MsgLike -> { + when (val kind = it.content.kind) { + is MsgLikeKind.Message -> { + val inReplyTo = it.content.inReplyTo + val isThreaded = it.content.threadRoot != null + eventMessageMapper.map(kind, inReplyTo, isThreaded) + } + is MsgLikeKind.Redacted -> { + RedactedContent + } + is MsgLikeKind.Poll -> { + PollContent( + question = kind.question, + kind = kind.kind.map(), + maxSelections = kind.maxSelections, + answers = kind.answers.map { answer -> answer.map() }.toImmutableList(), + votes = kind.votes.mapValues { vote -> + vote.value.map { userId -> UserId(userId) }.toImmutableList() + }.toImmutableMap(), + endTime = kind.endTime, + isEdited = kind.hasBeenEdited, + ) + } + is MsgLikeKind.UnableToDecrypt -> { + UnableToDecryptContent( + data = kind.msg.map() + ) + } + is MsgLikeKind.Sticker -> { + StickerContent( + filename = kind.body, + body = null, + info = kind.info.map(), + source = kind.source.map(), + ) + } + } } is TimelineItemContent.ProfileChange -> { ProfileChangeContent( @@ -66,14 +101,12 @@ class TimelineEventContentMapper( prevAvatarUrl = it.prevAvatarUrl ) } - TimelineItemContent.RedactedMessage -> { - RedactedContent - } is TimelineItemContent.RoomMembership -> { RoomMembershipContent( userId = UserId(it.userId), userDisplayName = it.userDisplayName, - change = it.change?.map() + change = it.change?.map(), + reason = it.reason, ) } is TimelineItemContent.State -> { @@ -82,35 +115,8 @@ class TimelineEventContentMapper( content = it.content.map() ) } - is TimelineItemContent.Sticker -> { - StickerContent( - filename = it.body, - body = null, - info = it.info.map(), - source = it.source.map(), - ) - } - is TimelineItemContent.Poll -> { - PollContent( - question = it.question, - kind = it.kind.map(), - maxSelections = it.maxSelections, - answers = it.answers.map { answer -> answer.map() }.toImmutableList(), - votes = it.votes.mapValues { vote -> - vote.value.map { userId -> UserId(userId) }.toImmutableList() - }.toImmutableMap(), - endTime = it.endTime, - isEdited = it.hasBeenEdited, - ) - } - is TimelineItemContent.UnableToDecrypt -> { - UnableToDecryptContent( - data = it.msg.map() - ) - } is TimelineItemContent.CallInvite -> LegacyCallInviteContent is TimelineItemContent.CallNotify -> CallNotifyContent - else -> UnknownContent } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt index 8df231bbab6..d60d88f1682 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt @@ -15,6 +15,7 @@ class VirtualTimelineItemMapper { return when (virtualTimelineItem) { is RustVirtualTimelineItem.DateDivider -> VirtualTimelineItem.DayDivider(virtualTimelineItem.ts.toLong()) RustVirtualTimelineItem.ReadMarker -> VirtualTimelineItem.ReadMarker + RustVirtualTimelineItem.TimelineStart -> VirtualTimelineItem.RoomBeginning } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt index 8e55b0aa765..9be7dddddbf 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt @@ -22,9 +22,7 @@ class LastForwardIndicatorsPostProcessor( fun process( items: List, - isTimelineInitialized: Boolean, ): List { - if (!isTimelineInitialized) return items // We don't need to add the last forward indicator if we are not in the FOCUSED_ON_EVENT mode if (mode != Timeline.Mode.FOCUSED_ON_EVENT) { return items diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt index 4261b78c936..6d42af54b5f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt @@ -16,11 +16,9 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) { fun process( items: List, - isTimelineInitialized: Boolean, hasMoreToLoadBackward: Boolean, hasMoreToLoadForward: Boolean, ): List { - if (!isTimelineInitialized) return items val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty() val currentTimestamp = systemClock.epochMillis() return buildList { 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 cfc03e50b9d..8403f631883 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 @@ -7,8 +7,6 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor -import androidx.annotation.VisibleForTesting -import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline @@ -16,11 +14,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent -import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem /** * 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 non DM room. + * or add the RoomBeginning item. */ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { fun process( @@ -39,8 +36,8 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { } private fun processForRoom(items: List): List { - val roomBeginningItem = createRoomBeginningItem() - return listOf(roomBeginningItem) + items + // No changes needed, timeline start item is already added by the SDK + return items } private fun processForDM(items: List, roomCreator: UserId?): List { @@ -49,38 +46,35 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { val roomCreationEventIndex = items.indexOfFirst { val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? StateContent stateEventContent?.content is OtherState.RoomCreate - } + }.takeIf { it >= 0 } // If the parameter roomCreator is null, the creator is the sender of the RoomCreate Event. - val roomCreatorUserId = roomCreator ?: (items.getOrNull(roomCreationEventIndex) as? MatrixTimelineItem.Event)?.event?.sender + val roomCreatorUserId = roomCreator ?: roomCreationEventIndex?.let { + (items.getOrNull(it) as? MatrixTimelineItem.Event)?.event?.sender + } // Find self-join event for the room creator. // This is usually the second MatrixTimelineItem.Event (so index 2) val selfUserJoinedEventIndex = roomCreatorUserId?.let { creatorUserId -> items.indexOfFirst { val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? RoomMembershipContent stateEventContent?.change == MembershipChange.JOINED && stateEventContent.userId == creatorUserId - } - } ?: -1 + }.takeIf { it >= 0 } + } - if (roomCreationEventIndex == -1 && selfUserJoinedEventIndex == -1) { + val indicesToRemove = listOfNotNull( + roomCreationEventIndex, + selfUserJoinedEventIndex, + ) + if (indicesToRemove.isEmpty()) { + // Nothing to do, return the list as is return items } + // Remove items at the indices we found val newItems = items.toMutableList() - if (selfUserJoinedEventIndex in newItems.indices) { - newItems.removeAt(selfUserJoinedEventIndex) - } - if (roomCreationEventIndex in newItems.indices) { - newItems.removeAt(roomCreationEventIndex) + indicesToRemove.sortedDescending().forEach { index -> + newItems.removeAt(index) } return newItems } - - @VisibleForTesting - fun createRoomBeginningItem(): MatrixTimelineItem.Virtual { - return MatrixTimelineItem.Virtual( - uniqueId = UniqueId("RoomBeginning"), - virtual = VirtualTimelineItem.RoomBeginning - ) - } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/reply/InReplyToMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/reply/InReplyToMapper.kt index 052385e5be5..1b329bfe0a0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/reply/InReplyToMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/reply/InReplyToMapper.kt @@ -12,8 +12,8 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.event.map +import org.matrix.rustcomponents.sdk.EmbeddedEventDetails import org.matrix.rustcomponents.sdk.InReplyToDetails -import org.matrix.rustcomponents.sdk.RepliedToEventDetails class InReplyToMapper( private val timelineEventContentMapper: TimelineEventContentMapper, @@ -21,7 +21,7 @@ class InReplyToMapper( fun map(inReplyToDetails: InReplyToDetails): InReplyTo { val inReplyToId = EventId(inReplyToDetails.eventId()) return when (val event = inReplyToDetails.event()) { - is RepliedToEventDetails.Ready -> { + is EmbeddedEventDetails.Ready -> { InReplyTo.Ready( eventId = inReplyToId, content = timelineEventContentMapper.map(event.content), @@ -29,14 +29,14 @@ class InReplyToMapper( senderProfile = event.senderProfile.map(), ) } - is RepliedToEventDetails.Error -> InReplyTo.Error( + is EmbeddedEventDetails.Error -> InReplyTo.Error( eventId = inReplyToId, message = event.message, ) - RepliedToEventDetails.Pending -> InReplyTo.Pending( + EmbeddedEventDetails.Pending -> InReplyTo.Pending( eventId = inReplyToId, ) - is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded( + is EmbeddedEventDetails.Unavailable -> InReplyTo.NotLoaded( eventId = inReplyToId ) } 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 74fb2e9db10..912561d843c 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 @@ -20,17 +20,6 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : TracingService { - override fun setupTracing(tracingConfiguration: TracingConfiguration) { - val rustTracingConfiguration = org.matrix.rustcomponents.sdk.TracingConfiguration( - writeToStdoutOrSystem = tracingConfiguration.writesToLogcat, - logLevel = tracingConfiguration.logLevel.toRustLogLevel(), - extraTargets = tracingConfiguration.extraTargets, - writeToFiles = tracingConfiguration.writesToFilesConfiguration.toTracingFileConfiguration(), - ) - org.matrix.rustcomponents.sdk.setupTracing(rustTracingConfiguration) - Timber.d("setupTracing: $rustTracingConfiguration") - } - override fun createTimberTree(target: String): Timber.Tree { return RustTracingTree(target = target, retrieveFromStackTrace = buildMeta.isDebuggable) } @@ -57,3 +46,12 @@ private fun WriteToFilesConfiguration.toTracingFileConfiguration(): TracingFileC ) } } + +fun TracingConfiguration.map(): org.matrix.rustcomponents.sdk.TracingConfiguration = org.matrix.rustcomponents.sdk.TracingConfiguration( + writeToStdoutOrSystem = writesToLogcat, + logLevel = logLevel.toRustLogLevel(), + extraTargets = extraTargets, + traceLogPacks = traceLogPacks.map(), + writeToFiles = writesToFilesConfiguration.toTracingFileConfiguration(), + sentryDsn = null, +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/TraceLogPacksMapping.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/TraceLogPacksMapping.kt new file mode 100644 index 00000000000..53876e9600d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/TraceLogPacksMapping.kt @@ -0,0 +1,22 @@ +/* + * 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.matrix.impl.tracing + +import io.element.android.libraries.matrix.api.tracing.TraceLogPack +import org.matrix.rustcomponents.sdk.TraceLogPacks as RustTraceLogPack + +fun TraceLogPack.map(): RustTraceLogPack = when (this) { + TraceLogPack.SEND_QUEUE -> RustTraceLogPack.SEND_QUEUE + TraceLogPack.EVENT_CACHE -> RustTraceLogPack.EVENT_CACHE + TraceLogPack.TIMELINE -> RustTraceLogPack.TIMELINE + TraceLogPack.NOTIFICATION_CLIENT -> RustTraceLogPack.NOTIFICATION_CLIENT +} + +fun Collection.map(): List { + return map { it.map() } +} 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 4349a12474f..12e72eac0b7 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 @@ -8,13 +8,15 @@ package io.element.android.libraries.matrix.impl.verification import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.verification.SessionVerificationData -import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.NonCancellable @@ -50,6 +52,8 @@ class RustSessionVerificationService( isSyncServiceReady: Flow, private val sessionCoroutineScope: CoroutineScope, ) : SessionVerificationService, SessionVerificationControllerDelegate { + private var currentVerificationRequest: VerificationRequest? = null + private val encryptionService: Encryption = client.encryption() private lateinit var verificationController: SessionVerificationController @@ -88,10 +92,8 @@ class RustSessionVerificationService( verificationStatus == SessionVerifiedStatus.NotVerified } - private var isOwnVerification = true - override fun didReceiveVerificationRequest(details: RustSessionVerificationRequestDetails) { - listener?.onIncomingSessionRequest(details.map()) + listener?.onIncomingSessionRequest(details.toVerificationRequest(UserId(client.userId()))) } private var listener: SessionVerificationServiceListener? = null @@ -99,7 +101,6 @@ class RustSessionVerificationService( init { // Instantiate the verification controller when possible, this is needed to get incoming verification requests sessionCoroutineScope.launch { - // Needed to avoid crashes on unit tests due to the Rust SDK not being available tryOrNull { encryptionService.waitForE2eeInitializationTasks() initVerificationControllerIfNeeded() @@ -111,9 +112,16 @@ class RustSessionVerificationService( this.listener = listener } - override suspend fun requestVerification() = tryOrFail { + override suspend fun requestCurrentSessionVerification() = tryOrFail { + initVerificationControllerIfNeeded() + verificationController.requestDeviceVerification() + currentVerificationRequest = VerificationRequest.Outgoing.CurrentSession + } + + override suspend fun requestUserVerification(userId: UserId) = tryOrFail { initVerificationControllerIfNeeded() - verificationController.requestVerification() + verificationController.requestUserVerification(userId.value) + currentVerificationRequest = VerificationRequest.Outgoing.User(userId) } override suspend fun cancelVerification() = tryOrFail { @@ -130,21 +138,21 @@ class RustSessionVerificationService( verificationController.startSasVerification() } - override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) = tryOrFail { - isOwnVerification = false + override suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) = tryOrFail { initVerificationControllerIfNeeded() verificationController.acknowledgeVerificationRequest( - senderId = details.senderId.value, - flowId = details.flowId.value, + senderId = verificationRequest.details.senderProfile.userId.value, + flowId = verificationRequest.details.flowId.value, ) } override suspend fun acceptVerificationRequest() = tryOrFail { + Timber.d("Accepting incoming verification request") verificationController.acceptVerificationRequest() } private suspend fun tryOrFail(block: suspend () -> Unit) { - runCatching { + runCatchingExceptions { // Ensure the block cannot be cancelled, else if the Rust SDK emit a new state during the API execution, // the state machine may cancel the api call. withContext(NonCancellable) { @@ -176,14 +184,14 @@ class RustSessionVerificationService( sessionCoroutineScope.launch { // Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it returns false if run immediately // It also sometimes unexpectedly fails to report the session as verified, so we have to handle that possibility and fail if needed - runCatching { + runCatchingExceptions { withTimeout(20.seconds) { // Wait until the SDK reports the state as verified sessionVerifiedStatus.first { it == SessionVerifiedStatus.Verified } } } .onSuccess { - if (isOwnVerification) { + if (currentVerificationRequest is VerificationRequest.Outgoing.CurrentSession) { // Try waiting for the final recovery state for better UX, but don't block the verification state on it tryOrNull { withTimeout(10.seconds) { @@ -215,7 +223,7 @@ class RustSessionVerificationService( // end-region override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) { - isOwnVerification = true + currentVerificationRequest = null if (isReady.value && cancelAnyPendingVerificationAttempt) { // Cancel any pending verification attempt tryOrNull { verificationController.cancelVerification() } @@ -244,7 +252,7 @@ class RustSessionVerificationService( } private fun updateVerificationStatus() { - runCatching { + runCatchingExceptions { _sessionVerifiedStatus.value = encryptionService.verificationState().map() Timber.d("New verification status: ${_sessionVerifiedStatus.value}") } @@ -266,8 +274,6 @@ private fun RustSessionVerificationData.map(): SessionVerificationData { emoji.use { sessionVerificationEmoji -> VerificationEmoji( number = sessionVerificationData.indices[index].toInt(), - emoji = sessionVerificationEmoji.symbol(), - description = sessionVerificationEmoji.description(), ) } }, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt index 028930371fc..d2e15a3fac0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt @@ -11,12 +11,28 @@ 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 import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.verification.VerificationRequest import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails +import org.matrix.rustcomponents.sdk.UserProfile as RustUserProfile fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails( - senderId = UserId(senderId), + senderProfile = senderProfile.map(), flowId = FlowId(flowId), deviceId = DeviceId(deviceId), - displayName = displayName, firstSeenTimestamp = firstSeenTimestamp.toLong(), ) + +fun RustUserProfile.map() = SessionVerificationRequestDetails.SenderProfile( + userId = UserId(userId), + displayName = displayName, + avatarUrl = avatarUrl, +) + +fun RustSessionVerificationRequestDetails.toVerificationRequest(currentUserId: UserId): VerificationRequest.Incoming { + val details = map() + return if (currentUserId == details.senderProfile.userId) { + VerificationRequest.Incoming.OtherSession(details) + } else { + VerificationRequest.Incoming.User(details) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt index 0b7b5f7a6b8..90b3c169064 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt @@ -8,30 +8,51 @@ package io.element.android.libraries.matrix.impl.widget import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.widget.CallAnalyticCredentialsProvider import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings -import org.matrix.rustcomponents.sdk.EncryptionSystem -import org.matrix.rustcomponents.sdk.VirtualElementCallWidgetOptions +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.coroutines.flow.first import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget +import uniffi.matrix_sdk.EncryptionSystem +import uniffi.matrix_sdk.HeaderStyle +import uniffi.matrix_sdk.VirtualElementCallWidgetOptions import javax.inject.Inject +import uniffi.matrix_sdk.Intent as CallIntent @ContributesBinding(AppScope::class) -class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettingsProvider { - override fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings { +class DefaultCallWidgetSettingsProvider @Inject constructor( + private val buildMeta: BuildMeta, + private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider, + private val analyticsService: AnalyticsService, +) : CallWidgetSettingsProvider { + override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings { + val isAnalyticsEnabled = analyticsService.userConsentFlow.first() val options = VirtualElementCallWidgetOptions( elementCallUrl = baseUrl, widgetId = widgetId, - parentUrl = null, - hideHeader = null, preload = null, fontScale = null, appPrompt = false, - skipLobby = true, confineToRoom = true, font = null, - analyticsId = null, encryption = if (encrypted) EncryptionSystem.PerParticipantKeys else EncryptionSystem.Unencrypted, + intent = CallIntent.START_CALL, + hideScreensharing = false, + posthogUserId = callAnalyticsCredentialsProvider.posthogUserId.takeIf { isAnalyticsEnabled }, + posthogApiHost = callAnalyticsCredentialsProvider.posthogApiHost.takeIf { isAnalyticsEnabled }, + posthogApiKey = callAnalyticsCredentialsProvider.posthogApiKey.takeIf { isAnalyticsEnabled }, + rageshakeSubmitUrl = callAnalyticsCredentialsProvider.rageshakeSubmitUrl, + sentryDsn = callAnalyticsCredentialsProvider.sentryDsn.takeIf { isAnalyticsEnabled }, + sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG", + parentUrl = null, + // For backwards compatibility, it'll be ignored in recent versions of Element Call + hideHeader = true, + controlledMediaDevices = true, + header = HeaderStyle.APP_BAR, ) val rustWidgetSettings = newVirtualElementCallWidget(options) return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/FakeClientBuilderProvider.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/FakeClientBuilderProvider.kt index 4da0122bc8b..3406b6686ad 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/FakeClientBuilderProvider.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/FakeClientBuilderProvider.kt @@ -7,11 +7,11 @@ package io.element.android.libraries.matrix.impl -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClientBuilder +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder import org.matrix.rustcomponents.sdk.ClientBuilder class FakeClientBuilderProvider : ClientBuilderProvider { override fun provide(): ClientBuilder { - return FakeRustClientBuilder() + return FakeFfiClientBuilder() } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index c6fc62348e2..c051aebbaca 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -31,7 +31,7 @@ class RustMatrixClientFactoryTest { val sut = createRustMatrixClientFactory() val result = sut.create(aSessionData()) assertThat(result.sessionId).isEqualTo(SessionId("@alice:server.org")) - result.close() + result.destroy() } } @@ -42,7 +42,7 @@ fun TestScope.createRustMatrixClientFactory( ) = RustMatrixClientFactory( baseDirectory = baseDirectory, cacheDirectory = cacheDirectory, - appCoroutineScope = this, + appCoroutineScope = backgroundScope, coroutineDispatchers = testCoroutineDispatchers(), sessionStore = sessionStore, userAgentProvider = SimpleUserAgentProvider(), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt index 0af84fc6e3b..91d0f06d1ca 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt @@ -9,40 +9,60 @@ package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustSyncService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test +import org.matrix.rustcomponents.sdk.Client import java.io.File class RustMatrixClientTest { @Test fun `ensure that sessionId and deviceId can be retrieved from the client`() = runTest { - createRustMatrixClient().use { sut -> - assertThat(sut.sessionId).isEqualTo(A_USER_ID) - assertThat(sut.deviceId).isEqualTo(A_DEVICE_ID) + createRustMatrixClient().run { + assertThat(sessionId).isEqualTo(A_USER_ID) + assertThat(deviceId).isEqualTo(A_DEVICE_ID) + destroy() } } + @Test + fun `clear cache invokes the method clearCaches from the client and close it`() = runTest { + val clearCachesResult = lambdaRecorder { } + val closeResult = lambdaRecorder { } + val client = createRustMatrixClient( + client = FakeFfiClient( + clearCachesResult = clearCachesResult, + closeResult = closeResult, + ) + ) + client.clearCache() + clearCachesResult.assertions().isCalledOnce() + closeResult.assertions().isCalledOnce() + client.destroy() + } + private fun TestScope.createRustMatrixClient( + client: Client = FakeFfiClient(), sessionStore: SessionStore = InMemorySessionStore(), ) = RustMatrixClient( - innerClient = FakeRustClient(), + innerClient = client, baseDirectory = File(""), sessionStore = sessionStore, - appCoroutineScope = this, + appCoroutineScope = backgroundScope, sessionDelegate = aRustClientSessionDelegate( sessionStore = sessionStore, ), - innerSyncService = FakeRustSyncService(), + innerSyncService = FakeFfiSyncService(), dispatchers = testCoroutineDispatchers(), baseCacheDirectory = File(""), clock = FakeSystemClock(), 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 new file mode 100644 index 00000000000..8bf86c5978a --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedExtKtTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.analytics + +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class JoinedExtKtTest { + @Test + fun `test room size mapping`() = runTest { + mapOf( + listOf(0L, 1L) to JoinedRoom.RoomSize.One, + listOf(2L, 2L) to JoinedRoom.RoomSize.Two, + listOf(3L, 10L) to JoinedRoom.RoomSize.ThreeToTen, + listOf(11L, 100L) to JoinedRoom.RoomSize.ElevenToOneHundred, + listOf(101L, 1000L) to JoinedRoom.RoomSize.OneHundredAndOneToAThousand, + listOf(1001L, 2000L) to JoinedRoom.RoomSize.MoreThanAThousand + ).forEach { (joinedMemberCounts, expectedRoomSize) -> + joinedMemberCounts.forEach { joinedMemberCount -> + assertThat(aRoom(joinedMemberCount = joinedMemberCount).toAnalyticsJoinedRoom(null)) + .isEqualTo( + JoinedRoom( + isDM = false, + isSpace = false, + roomSize = expectedRoomSize, + trigger = null + ) + ) + } + } + } + + @Test + fun `test isDirect parameter mapping`() = runTest { + assertThat(aRoom(isDirect = true).toAnalyticsJoinedRoom(null)) + .isEqualTo( + JoinedRoom( + isDM = true, + isSpace = false, + roomSize = JoinedRoom.RoomSize.One, + trigger = null + ) + ) + } + + @Test + fun `test isSpace parameter mapping`() = runTest { + assertThat(aRoom(isSpace = true).toAnalyticsJoinedRoom(null)) + .isEqualTo( + JoinedRoom( + isDM = false, + isSpace = true, + roomSize = JoinedRoom.RoomSize.One, + trigger = null + ) + ) + } + + @Test + fun `test trigger parameter mapping`() = runTest { + assertThat(aRoom(isDirect = false, isSpace = false, joinedMemberCount = 1).toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) + .isEqualTo( + JoinedRoom( + isDM = false, + isSpace = false, + roomSize = JoinedRoom.RoomSize.One, + trigger = JoinedRoom.Trigger.Invite + ) + ) + } + + private fun aRoom( + isDirect: Boolean = false, + isSpace: Boolean = false, + joinedMemberCount: Long = 0 + ): FakeBaseRoom { + return FakeBaseRoom().apply { + givenRoomInfo(aRoomInfo(isDirect = isDirect, isSpace = isSpace, joinedMembersCount = joinedMemberCount)) + } + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt deleted file mode 100644 index bce9097c5f4..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.analytics - -import com.google.common.truth.Truth.assertThat -import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import org.junit.Test - -class JoinedRoomExtKtTest { - @Test - fun `test room size mapping`() { - mapOf( - listOf(0L, 1L) to JoinedRoom.RoomSize.One, - listOf(2L, 2L) to JoinedRoom.RoomSize.Two, - listOf(3L, 10L) to JoinedRoom.RoomSize.ThreeToTen, - listOf(11L, 100L) to JoinedRoom.RoomSize.ElevenToOneHundred, - listOf(101L, 1000L) to JoinedRoom.RoomSize.OneHundredAndOneToAThousand, - listOf(1001L, 2000L) to JoinedRoom.RoomSize.MoreThanAThousand - ).forEach { (joinedMemberCounts, expectedRoomSize) -> - joinedMemberCounts.forEach { joinedMemberCount -> - assertThat(aMatrixRoom(joinedMemberCount = joinedMemberCount).toAnalyticsJoinedRoom(null)) - .isEqualTo( - JoinedRoom( - isDM = false, - isSpace = false, - roomSize = expectedRoomSize, - trigger = null - ) - ) - } - } - } - - @Test - fun `test isDirect parameter mapping`() { - assertThat(aMatrixRoom(isDirect = true).toAnalyticsJoinedRoom(null)) - .isEqualTo( - JoinedRoom( - isDM = true, - isSpace = false, - roomSize = JoinedRoom.RoomSize.One, - trigger = null - ) - ) - } - - @Test - fun `test isSpace parameter mapping`() { - assertThat(aMatrixRoom(isSpace = true).toAnalyticsJoinedRoom(null)) - .isEqualTo( - JoinedRoom( - isDM = false, - isSpace = true, - roomSize = JoinedRoom.RoomSize.One, - trigger = null - ) - ) - } - - @Test - fun `test trigger parameter mapping`() { - assertThat(aMatrixRoom().toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) - .isEqualTo( - JoinedRoom( - isDM = false, - isSpace = false, - roomSize = JoinedRoom.RoomSize.One, - trigger = JoinedRoom.Trigger.Invite - ) - ) - } - - private fun aMatrixRoom( - isDirect: Boolean = false, - isSpace: Boolean = false, - joinedMemberCount: Long = 0 - ): MatrixRoom { - return FakeMatrixRoom( - isDirect = isDirect, - isSpace = isSpace, - joinedMemberCount = 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 d6666afdc0f..c73ba1424b8 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 @@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.auth.AuthenticationException import org.junit.Test import org.matrix.rustcomponents.sdk.ClientBuildException +import org.matrix.rustcomponents.sdk.OidcException class AuthenticationExceptionMappingTest { @Test @@ -56,6 +57,20 @@ class AuthenticationExceptionMappingTest { .isException("EventCache error") } + @Test + fun `mapping Oidc exceptions map to the Oidc Kotlin`() { + assertThat(OidcException.Generic("Generic").mapAuthenticationException()) + .isException("Generic") + assertThat(OidcException.CallbackUrlInvalid("CallbackUrlInvalid").mapAuthenticationException()) + .isException("CallbackUrlInvalid") + assertThat(OidcException.Cancelled("Cancelled").mapAuthenticationException()) + .isException("Cancelled") + assertThat(OidcException.MetadataInvalid("MetadataInvalid").mapAuthenticationException()) + .isException("MetadataInvalid") + assertThat(OidcException.NotSupported("NotSupported").mapAuthenticationException()) + .isException("NotSupported") + } + private inline fun ThrowableSubject.isException(message: String) { isInstanceOf(T::class.java) hasMessageThat().isEqualTo(message) 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 69ec6e3f926..eb3ac75cf6d 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 @@ -9,14 +9,14 @@ package io.element.android.libraries.matrix.impl.auth import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustHomeserverLoginDetails +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails import org.junit.Test class HomeserverDetailsKtTest { @Test fun `map should be correct`() { // Given - val homeserverLoginDetails = FakeRustHomeserverLoginDetails( + val homeserverLoginDetails = FakeFfiHomeserverLoginDetails( url = "https://example.org", supportsPasswordLogin = true, supportsOidcLogin = false diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt index 0164cfeb201..96d1b46f931 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt @@ -8,14 +8,21 @@ package io.element.android.libraries.matrix.impl.auth import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.auth.OidcConfig +import io.element.android.libraries.matrix.test.auth.FAKE_REDIRECT_URL +import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider +import io.element.android.libraries.matrix.test.core.aBuildMeta import org.junit.Test -import java.io.File class OidcConfigurationProviderTest { @Test fun get() { - val result = OidcConfigurationProvider(File("/base")).get() - assertThat(result.redirectUri).isEqualTo(OidcConfig.REDIRECT_URI) + val result = OidcConfigurationProvider( + buildMeta = aBuildMeta( + applicationName = "myName", + ), + 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/RustMatrixAuthenticationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt index de6e25abd17..31453a7ca5e 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 @@ -11,7 +11,8 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +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.impl.memory.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData @@ -49,8 +50,10 @@ class RustMatrixAuthenticationServiceTest { sessionStore = sessionStore, rustMatrixClientFactory = rustMatrixClientFactory, passphraseGenerator = FakePassphraseGenerator(), - oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory), - appPreferencesStore = InMemoryAppPreferencesStore(), + oidcConfigurationProvider = OidcConfigurationProvider( + buildMeta = aBuildMeta(), + oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), + ), ) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginDataTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginDataTest.kt new file mode 100644 index 00000000000..1ba998fc419 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginDataTest.kt @@ -0,0 +1,35 @@ +/* + * 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.matrix.impl.auth.qrlogin + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import org.junit.Test + +class SdkQrCodeLoginDataTest { + @Test + fun `getServer reads the value from the Rust side, null case`() { + val sut = SdkQrCodeLoginData( + rustQrCodeData = FakeFfiQrCodeData( + serverNameResult = { null }, + ), + ) + assertThat(sut.serverName()).isNull() + } + + @Test + fun `getServer reads the value from the Rust side`() { + val sut = SdkQrCodeLoginData( + rustQrCodeData = FakeFfiQrCodeData( + serverNameResult = { A_HOMESERVER_URL }, + ), + ) + assertThat(sut.serverName()).isEqualTo(A_HOMESERVER_URL) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt deleted file mode 100644 index 7618a641b26..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.call - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.test.AN_EXCEPTION -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.matrix.rustcomponents.sdk.ElementCallWellKnown -import org.matrix.rustcomponents.sdk.ElementWellKnown - -class DefaultElementCallBaseUrlProviderTest { - @Test - fun `provides returns null when getUrl returns an error`() = runTest { - val userIdServerNameLambda = lambdaRecorder { "example.com" } - val getUrlLambda = lambdaRecorder> { _ -> - Result.failure(AN_EXCEPTION) - } - val sut = DefaultElementCallBaseUrlProvider( - FakeElementWellKnownParser( - Result.success(createElementWellKnown("")) - ) - ) - val matrixClient = FakeMatrixClient( - userIdServerNameLambda = userIdServerNameLambda, - getUrlLambda = getUrlLambda, - ) - val result = sut.provides(matrixClient) - assertThat(result).isNull() - userIdServerNameLambda.assertions().isCalledOnce() - getUrlLambda.assertions().isCalledOnce() - .with(value("https://example.com/.well-known/element/element.json")) - } - - @Test - fun `provides returns null when content parsing fails`() = runTest { - val userIdServerNameLambda = lambdaRecorder { "example.com" } - val getUrlLambda = lambdaRecorder> { _ -> - Result.success("""{"call":{"widget_url":"https://example.com/call"}}""") - } - val sut = DefaultElementCallBaseUrlProvider( - createFakeElementWellKnownParser( - Result.failure(AN_EXCEPTION) - ) - ) - val matrixClient = FakeMatrixClient( - userIdServerNameLambda = userIdServerNameLambda, - getUrlLambda = getUrlLambda, - ) - val result = sut.provides(matrixClient) - assertThat(result).isNull() - userIdServerNameLambda.assertions().isCalledOnce() - getUrlLambda.assertions().isCalledOnce() - .with(value("https://example.com/.well-known/element/element.json")) - } - - @Test - fun `provides returns value when getUrl returns correct content`() = runTest { - val userIdServerNameLambda = lambdaRecorder { "example.com" } - val getUrlLambda = lambdaRecorder> { _ -> - Result.success("""{"call":{"widget_url":"https://example.com/call"}}""") - } - val sut = DefaultElementCallBaseUrlProvider( - createFakeElementWellKnownParser( - Result.success(createElementWellKnown("aUrl")) - ) - ) - val matrixClient = FakeMatrixClient( - userIdServerNameLambda = userIdServerNameLambda, - getUrlLambda = getUrlLambda, - ) - val result = sut.provides(matrixClient) - assertThat(result).isEqualTo("aUrl") - userIdServerNameLambda.assertions().isCalledOnce() - getUrlLambda.assertions().isCalledOnce() - .with(value("https://example.com/.well-known/element/element.json")) - } - - private fun createFakeElementWellKnownParser(result: Result): FakeElementWellKnownParser { - return FakeElementWellKnownParser(result) - } - - private fun createElementWellKnown(widgetUrl: String): ElementWellKnown { - return ElementWellKnown( - call = ElementCallWellKnown( - widgetUrl = widgetUrl, - ), - registrationHelperUrl = null, - ) - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/FakeElementWellKnownParser.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/FakeElementWellKnownParser.kt deleted file mode 100644 index 5c37ca6a685..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/FakeElementWellKnownParser.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.call - -import org.matrix.rustcomponents.sdk.ElementWellKnown - -class FakeElementWellKnownParser( - private val result: Result -) : ElementWellKnownParser { - override fun parse(str: String): Result { - return result - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt index ff8fcaa78d5..fbbb6c3375a 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.matrix.impl.fixtures.factories -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustLazyTimelineItemProvider +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiLazyTimelineItemProvider import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID import org.matrix.rustcomponents.sdk.EventOrTransactionId @@ -15,7 +15,6 @@ import org.matrix.rustcomponents.sdk.EventSendState import org.matrix.rustcomponents.sdk.EventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo import org.matrix.rustcomponents.sdk.ProfileDetails -import org.matrix.rustcomponents.sdk.Reaction import org.matrix.rustcomponents.sdk.Receipt import org.matrix.rustcomponents.sdk.ShieldState import org.matrix.rustcomponents.sdk.TimelineItemContent @@ -30,7 +29,6 @@ fun aRustEventTimelineItem( isEditable: Boolean = true, content: TimelineItemContent = aRustTimelineItemMessageContent(), timestamp: ULong = 0uL, - reactions: List = emptyList(), debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(), localSendState: EventSendState? = null, readReceipts: Map = emptyMap(), @@ -49,11 +47,10 @@ fun aRustEventTimelineItem( canBeRepliedTo = canBeRepliedTo, content = content, localSendState = localSendState, - reactions = reactions, readReceipts = readReceipts, origin = origin, localCreatedAt = localCreatedAt, - lazyProvider = FakeRustLazyTimelineItemProvider( + lazyProvider = FakeFfiLazyTimelineItemProvider( debugInfo = debugInfo, shieldsState = shieldsState, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItemContent.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItemContent.kt index 9fe3a7569ac..b7ea8510c2a 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItemContent.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItemContent.kt @@ -9,16 +9,24 @@ package io.element.android.libraries.matrix.impl.fixtures.factories import org.matrix.rustcomponents.sdk.MessageContent import org.matrix.rustcomponents.sdk.MessageType +import org.matrix.rustcomponents.sdk.MsgLikeContent +import org.matrix.rustcomponents.sdk.MsgLikeKind import org.matrix.rustcomponents.sdk.TextMessageContent import org.matrix.rustcomponents.sdk.TimelineItemContent -fun aRustTimelineItemMessageContent(body: String = "Hello") = TimelineItemContent.Message( - content = MessageContent( - msgType = MessageType.Text(content = TextMessageContent(body = body, formatted = null)), - body = body, - inReplyTo = null, +fun aRustTimelineItemMessageContent(body: String = "Hello") = TimelineItemContent.MsgLike( + content = MsgLikeContent( + kind = MsgLikeKind.Message( + content = MessageContent( + msgType = MessageType.Text(content = TextMessageContent(body = body, formatted = null)), + body = body, + isEdited = false, + mentions = null, + ) + ), + reactions = emptyList(), threadRoot = null, - isEdited = false, - mentions = null, - ) + inReplyTo = null, + threadSummary = null, + ), ) 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 ce6b333680a..b13e48e6695 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 @@ -7,9 +7,11 @@ package io.element.android.libraries.matrix.impl.fixtures.factories -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineEvent +import io.element.android.libraries.matrix.api.core.ThreadId +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 +import org.matrix.rustcomponents.sdk.JoinRule import org.matrix.rustcomponents.sdk.NotificationEvent import org.matrix.rustcomponents.sdk.NotificationItem import org.matrix.rustcomponents.sdk.NotificationRoomInfo @@ -22,12 +24,14 @@ fun aRustNotificationItem( roomInfo: NotificationRoomInfo = aRustNotificationRoomInfo(), isNoisy: Boolean? = false, hasMention: Boolean? = false, + threadId: ThreadId? = null, ) = NotificationItem( event = event, senderInfo = senderInfo, roomInfo = roomInfo, isNoisy = isNoisy, hasMention = hasMention, + threadId = threadId?.value, ) fun aRustNotificationSenderInfo( @@ -44,20 +48,24 @@ fun aRustNotificationRoomInfo( displayName: String = A_ROOM_NAME, avatarUrl: String? = null, canonicalAlias: String? = null, + topic: String? = null, joinedMembersCount: ULong = 2u, isEncrypted: Boolean? = true, isDirect: Boolean = false, + joinRule: JoinRule? = null, ) = NotificationRoomInfo( displayName = displayName, avatarUrl = avatarUrl, canonicalAlias = canonicalAlias, + topic = topic, joinedMembersCount = joinedMembersCount, isEncrypted = isEncrypted, isDirect = isDirect, + joinRule = joinRule, ) fun aRustNotificationEventTimeline( - event: TimelineEvent = FakeRustTimelineEvent(), + event: TimelineEvent = FakeFfiTimelineEvent(), ) = NotificationEvent.Timeline( event = event, ) 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 f5341a1c5e8..9ba3c100a79 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 @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.fixtures.factories import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomPowerLevels import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import org.matrix.rustcomponents.sdk.JoinRule @@ -17,6 +18,9 @@ import org.matrix.rustcomponents.sdk.RoomHistoryVisibility import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomNotificationMode +import org.matrix.rustcomponents.sdk.RoomPowerLevels +import org.matrix.rustcomponents.sdk.SuccessorRoom +import uniffi.matrix_sdk_base.EncryptionState fun aRustRoomInfo( id: String = A_ROOM_ID.value, @@ -24,10 +28,10 @@ fun aRustRoomInfo( rawName: String? = A_ROOM_NAME, topic: String? = null, avatarUrl: String? = null, + encryptionState: EncryptionState = EncryptionState.UNKNOWN, isDirect: Boolean = false, isPublic: Boolean = false, isSpace: Boolean = false, - isTombstoned: Boolean = false, isFavourite: Boolean = false, canonicalAlias: String? = null, alternativeAliases: List = listOf(), @@ -37,7 +41,7 @@ fun aRustRoomInfo( activeMembersCount: ULong = 0uL, invitedMembersCount: ULong = 0uL, joinedMembersCount: ULong = 0uL, - userPowerLevels: Map = mapOf(), + roomPowerLevels: RoomPowerLevels = FakeFfiRoomPowerLevels(), highlightCount: ULong = 0uL, notificationCount: ULong = 0uL, userDefinedNotificationMode: RoomNotificationMode? = null, @@ -51,16 +55,17 @@ fun aRustRoomInfo( roomCreator: UserId? = null, joinRule: JoinRule? = null, historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined, + successorRoom: SuccessorRoom? = null, ) = RoomInfo( id = id, displayName = displayName, rawName = rawName, topic = topic, avatarUrl = avatarUrl, + encryptionState = encryptionState, isDirect = isDirect, isPublic = isPublic, isSpace = isSpace, - isTombstoned = isTombstoned, isFavourite = isFavourite, canonicalAlias = canonicalAlias, alternativeAliases = alternativeAliases, @@ -70,7 +75,7 @@ fun aRustRoomInfo( activeMembersCount = activeMembersCount, invitedMembersCount = invitedMembersCount, joinedMembersCount = joinedMembersCount, - userPowerLevels = userPowerLevels, + powerLevels = roomPowerLevels, highlightCount = highlightCount, notificationCount = notificationCount, cachedUserDefinedNotificationMode = userDefinedNotificationMode, @@ -83,5 +88,6 @@ fun aRustRoomInfo( pinnedEventIds = pinnedEventIds, creator = roomCreator?.value, joinRule = joinRule, - historyVisibility = historyVisibility + historyVisibility = historyVisibility, + successorRoom = successorRoom, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevels.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevels.kt index e7967a49d84..79d16069579 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevels.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevels.kt @@ -7,9 +7,9 @@ package io.element.android.libraries.matrix.impl.fixtures.factories -import org.matrix.rustcomponents.sdk.RoomPowerLevels +import org.matrix.rustcomponents.sdk.RoomPowerLevelsValues -internal fun aRustRoomPowerLevels( +internal fun aRustRoomPowerLevelsValues( ban: Long, invite: Long, kick: Long, @@ -20,7 +20,7 @@ internal fun aRustRoomPowerLevels( roomName: Long, roomAvatar: Long, roomTopic: Long, -) = RoomPowerLevels( +) = RoomPowerLevelsValues( ban = ban, invite = invite, kick = kick, 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 010da868879..b1301d0777c 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 @@ -14,7 +14,7 @@ import org.matrix.rustcomponents.sdk.Session import org.matrix.rustcomponents.sdk.SlidingSyncVersion internal fun aRustSession( - proxy: SlidingSyncVersion = SlidingSyncVersion.None, + proxy: SlidingSyncVersion = SlidingSyncVersion.NONE, accessToken: String = "accessToken", refreshToken: String = "refreshToken", ): Session { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt new file mode 100644 index 00000000000..8e88fa78fe1 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -0,0 +1,80 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession +import io.element.android.libraries.matrix.test.A_DEVICE_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.ClientDelegate +import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.IgnoredUsersListener +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.NotificationClient +import org.matrix.rustcomponents.sdk.NotificationProcessSetup +import org.matrix.rustcomponents.sdk.NotificationSettings +import org.matrix.rustcomponents.sdk.PusherIdentifiers +import org.matrix.rustcomponents.sdk.PusherKind +import org.matrix.rustcomponents.sdk.RoomDirectorySearch +import org.matrix.rustcomponents.sdk.Session +import org.matrix.rustcomponents.sdk.SessionVerificationController +import org.matrix.rustcomponents.sdk.SyncService +import org.matrix.rustcomponents.sdk.SyncServiceBuilder +import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate +import org.matrix.rustcomponents.sdk.UserProfile + +class FakeFfiClient( + private val userId: String = A_USER_ID.value, + private val deviceId: String = A_DEVICE_ID.value, + private val notificationClient: NotificationClient = FakeFfiNotificationClient(), + private val notificationSettings: NotificationSettings = FakeFfiNotificationSettings(), + private val encryption: Encryption = FakeFfiEncryption(), + private val session: Session = aRustSession(), + private val clearCachesResult: () -> Unit = { lambdaError() }, + private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() }, + private val closeResult: () -> Unit = {}, +) : Client(NoPointer) { + override fun userId(): String = userId + override fun deviceId(): String = deviceId + override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient + override suspend fun getNotificationSettings(): NotificationSettings = notificationSettings + override fun encryption(): Encryption = encryption + override fun session(): Session = session + override fun setDelegate(delegate: ClientDelegate?): TaskHandle = FakeFfiTaskHandle() + override suspend fun cachedAvatarUrl(): String? = null + override suspend fun restoreSession(session: Session) = Unit + override fun syncService(): SyncServiceBuilder = FakeFfiSyncServiceBuilder() + override fun roomDirectorySearch(): RoomDirectorySearch = FakeFfiRoomDirectorySearch() + override suspend fun setPusher( + identifiers: PusherIdentifiers, + kind: PusherKind, + appDisplayName: String, + deviceDisplayName: String, + profileTag: String?, + lang: String, + ) = Unit + + override suspend fun deletePusher(identifiers: PusherIdentifiers) = Unit + override suspend fun clearCaches(syncService: SyncService?) = simulateLongTask { clearCachesResult() } + override suspend fun setUtdDelegate(utdDelegate: UnableToDecryptDelegate) = withUtdHook(utdDelegate) + override suspend fun getSessionVerificationController(): SessionVerificationController = FakeFfiSessionVerificationController() + override suspend fun ignoredUsers(): List { + return emptyList() + } + override fun subscribeToIgnoredUsers(listener: IgnoredUsersListener): TaskHandle { + return FakeFfiTaskHandle() + } + + override suspend fun getProfile(userId: String): UserProfile { + return UserProfile(userId = userId, displayName = null, avatarUrl = null) + } + override fun close() = closeResult() +} 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 new file mode 100644 index 00000000000..3ddb8c2cee1 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt @@ -0,0 +1,53 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.ClientBuilder +import org.matrix.rustcomponents.sdk.ClientSessionDelegate +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.OidcConfiguration +import org.matrix.rustcomponents.sdk.QrCodeData +import org.matrix.rustcomponents.sdk.QrLoginProgressListener +import org.matrix.rustcomponents.sdk.RequestConfig +import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder +import uniffi.matrix_sdk.BackupDownloadStrategy +import uniffi.matrix_sdk_crypto.CollectStrategy +import uniffi.matrix_sdk_crypto.DecryptionSettings + +class FakeFfiClientBuilder : ClientBuilder(NoPointer) { + override fun addRootCertificates(certificates: List) = this + override fun autoEnableBackups(autoEnableBackups: Boolean) = this + override fun autoEnableCrossSigning(autoEnableCrossSigning: Boolean) = this + override fun backupDownloadStrategy(backupDownloadStrategy: BackupDownloadStrategy) = this + override fun disableAutomaticTokenRefresh() = this + override fun disableBuiltInRootCertificates() = this + override fun decryptionSettings(decryptionSettings: DecryptionSettings): ClientBuilder = this + override fun disableSslVerification() = this + override fun homeserverUrl(url: String) = this + override fun sessionPassphrase(passphrase: String?) = this + override fun proxy(url: String) = this + override fun requestConfig(config: RequestConfig) = this + override fun roomKeyRecipientStrategy(strategy: CollectStrategy) = this + override fun serverName(serverName: String) = this + override fun serverNameOrHomeserverUrl(serverNameOrUrl: String) = this + override fun sessionPaths(dataPath: String, cachePath: String) = this + override fun setSessionDelegate(sessionDelegate: ClientSessionDelegate) = this + override fun slidingSyncVersionBuilder(versionBuilder: SlidingSyncVersionBuilder) = this + override fun userAgent(userAgent: String) = this + override fun username(username: String) = this + override fun enableShareHistoryOnInvite(enableShareHistoryOnInvite: Boolean): ClientBuilder = this + + override suspend fun buildWithQrCode(qrCodeData: QrCodeData, oidcConfiguration: OidcConfiguration, progressListener: QrLoginProgressListener): Client { + return FakeFfiClient() + } + + override suspend fun build(): Client { + return FakeFfiClient(withUtdHook = {}) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiEncryption.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiEncryption.kt new file mode 100644 index 00000000000..536288caefd --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiEncryption.kt @@ -0,0 +1,46 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.tests.testutils.simulateLongTask +import org.matrix.rustcomponents.sdk.BackupState +import org.matrix.rustcomponents.sdk.BackupStateListener +import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RecoveryState +import org.matrix.rustcomponents.sdk.RecoveryStateListener +import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.VerificationStateListener + +class FakeFfiEncryption : Encryption(NoPointer) { + override fun verificationStateListener(listener: VerificationStateListener): TaskHandle { + return FakeFfiTaskHandle() + } + + override fun recoveryStateListener(listener: RecoveryStateListener): TaskHandle { + return FakeFfiTaskHandle() + } + + override suspend fun waitForE2eeInitializationTasks() = simulateLongTask {} + + override suspend fun isLastDevice(): Boolean { + return false + } + + override fun backupState(): BackupState { + return BackupState.ENABLED + } + + override fun recoveryState(): RecoveryState { + return RecoveryState.ENABLED + } + + override fun backupStateListener(listener: BackupStateListener): TaskHandle { + return FakeFfiTaskHandle() + } +} 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 new file mode 100644 index 00000000000..89774703654 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt @@ -0,0 +1,21 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.HomeserverLoginDetails +import org.matrix.rustcomponents.sdk.NoPointer + +class FakeFfiHomeserverLoginDetails( + private val url: String = "https://example.org", + private val supportsPasswordLogin: Boolean = true, + private val supportsOidcLogin: Boolean = false +) : HomeserverLoginDetails(NoPointer) { + override fun url(): String = url + override fun supportsOidcLogin(): Boolean = supportsOidcLogin + override fun supportsPasswordLogin(): Boolean = supportsPasswordLogin +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLazyTimelineItemProvider.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLazyTimelineItemProvider.kt new file mode 100644 index 00000000000..6149a9164de --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLazyTimelineItemProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.libraries.matrix.impl.fixtures.factories.anEventTimelineItemDebugInfo +import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo +import org.matrix.rustcomponents.sdk.LazyTimelineItemProvider +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.SendHandle +import org.matrix.rustcomponents.sdk.ShieldState + +class FakeFfiLazyTimelineItemProvider( + private val debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(), + private val shieldsState: ShieldState? = null, +) : LazyTimelineItemProvider(NoPointer) { + override fun getShields(strict: Boolean) = shieldsState + override fun debugInfo() = debugInfo + override fun getSendHandle(): SendHandle? = null +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt new file mode 100644 index 00000000000..180da9cc89b --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt @@ -0,0 +1,24 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.NotificationClient +import org.matrix.rustcomponents.sdk.NotificationItem +import org.matrix.rustcomponents.sdk.NotificationItemsRequest + +class FakeFfiNotificationClient( + var notificationItemResult: Map = emptyMap(), + val closeResult: () -> Unit = { } +) : NotificationClient(NoPointer) { + override suspend fun getNotifications(requests: List): Map { + return notificationItemResult + } + + override fun close() = closeResult() +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationSettings.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationSettings.kt new file mode 100644 index 00000000000..7a65b8d2cd8 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationSettings.kt @@ -0,0 +1,30 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomNotificationSettings +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.NotificationSettings +import org.matrix.rustcomponents.sdk.NotificationSettingsDelegate +import org.matrix.rustcomponents.sdk.RoomNotificationSettings + +class FakeFfiNotificationSettings( + private val roomNotificationSettings: RoomNotificationSettings = aRustRoomNotificationSettings(), +) : NotificationSettings(NoPointer) { + private var delegate: NotificationSettingsDelegate? = null + + override fun setDelegate(delegate: NotificationSettingsDelegate?) { + this.delegate = delegate + } + + override suspend fun getRoomNotificationSettings( + roomId: String, + isEncrypted: Boolean, + isOneToOne: Boolean, + ): RoomNotificationSettings = roomNotificationSettings +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt new file mode 100644 index 00000000000..1be8b87a660 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt @@ -0,0 +1,20 @@ +/* + * 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.matrix.impl.fixtures.fakes + +import io.element.android.tests.testutils.lambda.lambdaError +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.QrCodeData + +class FakeFfiQrCodeData( + private val serverNameResult: () -> String? = { lambdaError() }, +) : QrCodeData(NoPointer) { + override fun serverName(): String? { + return serverNameResult() + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoom.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoom.kt new file mode 100644 index 00000000000..45aa9706589 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoom.kt @@ -0,0 +1,55 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.tests.testutils.lambda.lambdaError +import org.matrix.rustcomponents.sdk.EventTimelineItem +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.RoomInfo +import org.matrix.rustcomponents.sdk.RoomMembersIterator + +class FakeFfiRoom( + private val roomId: RoomId = A_ROOM_ID, + private val getMembers: () -> RoomMembersIterator = { lambdaError() }, + private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() }, + private val leaveLambda: () -> Unit = { lambdaError() }, + private val latestEventLambda: () -> EventTimelineItem? = { lambdaError() }, + private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value), +) : Room(NoPointer) { + override fun id(): String { + return roomId.value + } + + override suspend fun members(): RoomMembersIterator { + return getMembers() + } + + override suspend fun membersNoSync(): RoomMembersIterator { + return getMembersNoSync() + } + + override suspend fun leave() { + leaveLambda() + } + + override suspend fun roomInfo(): RoomInfo { + return roomInfo + } + + override suspend fun latestEvent(): EventTimelineItem? { + return latestEventLambda() + } + + override fun close() { + // No-op + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomDirectorySearch.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomDirectorySearch.kt new file mode 100644 index 00000000000..b090262e31d --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomDirectorySearch.kt @@ -0,0 +1,37 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.tests.testutils.simulateLongTask +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RoomDirectorySearch +import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntriesListener +import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate +import org.matrix.rustcomponents.sdk.TaskHandle + +class FakeFfiRoomDirectorySearch( + var isAtLastPage: Boolean = false, +) : RoomDirectorySearch(NoPointer) { + override suspend fun isAtLastPage(): Boolean { + return isAtLastPage + } + + override suspend fun search(filter: String?, batchSize: UInt, viaServerName: String?) = simulateLongTask { } + override suspend fun nextPage() = simulateLongTask { } + + private var listener: RoomDirectorySearchEntriesListener? = null + + override suspend fun results(listener: RoomDirectorySearchEntriesListener): TaskHandle { + this.listener = listener + return FakeFfiTaskHandle() + } + + fun emitResult(roomEntriesUpdate: List) { + listener?.onUpdate(roomEntriesUpdate) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomList.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomList.kt new file mode 100644 index 00000000000..d51a742368e --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomList.kt @@ -0,0 +1,13 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RoomList + +class FakeFfiRoomList : RoomList(NoPointer) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListService.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListService.kt new file mode 100644 index 00000000000..604f5289a0d --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListService.kt @@ -0,0 +1,40 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RoomList +import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.RoomListServiceStateListener +import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator +import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener +import org.matrix.rustcomponents.sdk.TaskHandle + +class FakeFfiRoomListService : RoomListService(NoPointer) { + override suspend fun allRooms(): RoomList { + return FakeFfiRoomList() + } + + private var listener: RoomListServiceSyncIndicatorListener? = null + override fun syncIndicator( + delayBeforeShowingInMs: UInt, + delayBeforeHidingInMs: UInt, + listener: RoomListServiceSyncIndicatorListener, + ): TaskHandle { + this.listener = listener + return FakeFfiTaskHandle() + } + + fun emitRoomListServiceSyncIndicator(syncIndicator: RoomListServiceSyncIndicator) { + listener?.onUpdate(syncIndicator) + } + + override fun state(listener: RoomListServiceStateListener): TaskHandle { + return FakeFfiTaskHandle() + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomMembersIterator.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomMembersIterator.kt new file mode 100644 index 00000000000..28ee4791e59 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomMembersIterator.kt @@ -0,0 +1,31 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RoomMember +import org.matrix.rustcomponents.sdk.RoomMembersIterator + +class FakeFfiRoomMembersIterator( + private var members: List? = null +) : RoomMembersIterator(NoPointer) { + override fun len(): UInt { + return members?.size?.toUInt() ?: 0u + } + + override fun nextChunk(chunkSize: UInt): List? { + if (members?.isEmpty() == true) { + return null + } + return members?.let { + val result = it.take(chunkSize.toInt()) + members = it.subList(result.size, it.size) + result + } + } +} 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 new file mode 100644 index 00000000000..32e7dc891d8 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomPowerLevels.kt @@ -0,0 +1,33 @@ +/* + * 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.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RoomPowerLevels +import org.matrix.rustcomponents.sdk.RoomPowerLevelsValues + +class FakeFfiRoomPowerLevels( + private val values: RoomPowerLevelsValues = defaultFfiRoomPowerLevelValues(), + private val users: Map = emptyMap(), +) : RoomPowerLevels(NoPointer) { + override fun values(): RoomPowerLevelsValues = values + override fun userPowerLevels(): Map = users +} + +fun defaultFfiRoomPowerLevelValues() = RoomPowerLevelsValues( + ban = 50, + invite = 0, + kick = 50, + eventsDefault = 0, + redact = 50, + roomName = 100, + roomAvatar = 100, + roomTopic = 100, + stateDefault = 0, + usersDefault = 0, +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSessionVerificationController.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSessionVerificationController.kt new file mode 100644 index 00000000000..2ff1e1d6ac0 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSessionVerificationController.kt @@ -0,0 +1,16 @@ +/* + * 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.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.SessionVerificationController +import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate + +class FakeFfiSessionVerificationController : SessionVerificationController(NoPointer) { + override fun setDelegate(delegate: SessionVerificationControllerDelegate?) {} +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncService.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncService.kt new file mode 100644 index 00000000000..5d32139c8e6 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncService.kt @@ -0,0 +1,24 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.SyncService +import org.matrix.rustcomponents.sdk.SyncServiceStateObserver +import org.matrix.rustcomponents.sdk.TaskHandle + +class FakeFfiSyncService( + private val roomListService: RoomListService = FakeFfiRoomListService(), +) : SyncService(NoPointer) { + override fun roomListService(): RoomListService = roomListService + override fun state(listener: SyncServiceStateObserver): TaskHandle { + return FakeFfiTaskHandle() + } + override suspend fun stop() {} +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncServiceBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncServiceBuilder.kt new file mode 100644 index 00000000000..93dab14a0c4 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncServiceBuilder.kt @@ -0,0 +1,17 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.SyncService +import org.matrix.rustcomponents.sdk.SyncServiceBuilder + +class FakeFfiSyncServiceBuilder : SyncServiceBuilder(NoPointer) { + override fun withOfflineMode(): SyncServiceBuilder = this + override suspend fun finish(): SyncService = FakeFfiSyncService() +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTaskHandle.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTaskHandle.kt new file mode 100644 index 00000000000..66c51017df4 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTaskHandle.kt @@ -0,0 +1,16 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.TaskHandle + +class FakeFfiTaskHandle : TaskHandle(NoPointer) { + override fun cancel() = Unit + override fun destroy() = Unit +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimeline.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimeline.kt new file mode 100644 index 00000000000..09d79f87900 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimeline.kt @@ -0,0 +1,44 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.PaginationStatusListener +import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.Timeline +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineListener +import uniffi.matrix_sdk.RoomPaginationStatus + +class FakeFfiTimeline : Timeline(NoPointer) { + private var listener: TimelineListener? = null + override suspend fun addListener(listener: TimelineListener): TaskHandle { + this.listener = listener + return FakeFfiTaskHandle() + } + + fun emitDiff(diff: List) { + listener!!.onUpdate(diff) + } + + private var paginationStatusListener: PaginationStatusListener? = null + override suspend fun subscribeToBackPaginationStatus(listener: PaginationStatusListener): TaskHandle { + this.paginationStatusListener = listener + return FakeFfiTaskHandle() + } + + fun emitPaginationStatus(status: RoomPaginationStatus) { + paginationStatusListener!!.onUpdate(status) + } + + override suspend fun paginateBackwards(numEvents: UShort): Boolean { + return true + } + + override suspend fun fetchMembers() = Unit +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineDiff.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineDiff.kt new file mode 100644 index 00000000000..605d2d6815c --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineDiff.kt @@ -0,0 +1,30 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.InsertData +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.SetData +import org.matrix.rustcomponents.sdk.TimelineChange +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineItem + +class FakeFfiTimelineDiff( + private val change: TimelineChange, + private val item: TimelineItem? = FakeFfiTimelineItem() +) : TimelineDiff(NoPointer) { + override fun change() = change + override fun append(): List? = item?.let { listOf(it) } + override fun insert(): InsertData? = item?.let { InsertData(1u, it) } + override fun pushBack(): TimelineItem? = item + override fun pushFront(): TimelineItem? = item + override fun remove(): UInt? = 1u + override fun reset(): List? = item?.let { listOf(it) } + override fun set(): SetData? = item?.let { SetData(1u, it) } + override fun truncate(): UInt? = 1u +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEvent.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEvent.kt new file mode 100644 index 00000000000..e514cef7a89 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEvent.kt @@ -0,0 +1,25 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustTimelineEventTypeMessageLike +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.TimelineEvent +import org.matrix.rustcomponents.sdk.TimelineEventType + +class FakeFfiTimelineEvent( + val timestamp: ULong = A_FAKE_TIMESTAMP.toULong(), + val timelineEventType: TimelineEventType = aRustTimelineEventTypeMessageLike(), + val senderId: String = A_USER_ID_2.value, +) : TimelineEvent(NoPointer) { + override fun timestamp(): ULong = timestamp + override fun eventType(): TimelineEventType = timelineEventType + override fun senderId(): String = senderId +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEventTypeFilter.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEventTypeFilter.kt new file mode 100644 index 00000000000..dcab0bba6e4 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEventTypeFilter.kt @@ -0,0 +1,13 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter + +class FakeFfiTimelineEventTypeFilter : TimelineEventTypeFilter(NoPointer) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineItem.kt new file mode 100644 index 00000000000..a3f7a18c215 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineItem.kt @@ -0,0 +1,23 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.EventTimelineItem +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.TimelineItem +import org.matrix.rustcomponents.sdk.TimelineUniqueId +import org.matrix.rustcomponents.sdk.VirtualTimelineItem + +class FakeFfiTimelineItem( + private val asEventResult: EventTimelineItem? = null, +) : TimelineItem(NoPointer) { + override fun asEvent(): EventTimelineItem? = asEventResult + override fun asVirtual(): VirtualTimelineItem? = null + override fun fmtDebug(): String = "fmtDebug" + override fun uniqueId(): TimelineUniqueId = TimelineUniqueId("uniqueId") +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt deleted file mode 100644 index 0a9983d60f7..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession -import io.element.android.libraries.matrix.test.A_DEVICE_ID -import io.element.android.libraries.matrix.test.A_USER_ID -import org.matrix.rustcomponents.sdk.Client -import org.matrix.rustcomponents.sdk.ClientDelegate -import org.matrix.rustcomponents.sdk.Encryption -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.NotificationClient -import org.matrix.rustcomponents.sdk.NotificationProcessSetup -import org.matrix.rustcomponents.sdk.NotificationSettings -import org.matrix.rustcomponents.sdk.PusherIdentifiers -import org.matrix.rustcomponents.sdk.PusherKind -import org.matrix.rustcomponents.sdk.RoomDirectorySearch -import org.matrix.rustcomponents.sdk.Session -import org.matrix.rustcomponents.sdk.SyncServiceBuilder -import org.matrix.rustcomponents.sdk.TaskHandle - -class FakeRustClient( - private val userId: String = A_USER_ID.value, - private val deviceId: String = A_DEVICE_ID.value, - private val notificationClient: NotificationClient = FakeRustNotificationClient(), - private val notificationSettings: NotificationSettings = FakeRustNotificationSettings(), - private val encryption: Encryption = FakeRustEncryption(), - private val session: Session = aRustSession(), -) : Client(NoPointer) { - override fun userId(): String = userId - override fun deviceId(): String = deviceId - override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient - override fun getNotificationSettings(): NotificationSettings = notificationSettings - override fun encryption(): Encryption = encryption - override fun session(): Session = session - override fun setDelegate(delegate: ClientDelegate?): TaskHandle = FakeRustTaskHandle() - override fun cachedAvatarUrl(): String? = null - override suspend fun restoreSession(session: Session) = Unit - override fun syncService(): SyncServiceBuilder = FakeRustSyncServiceBuilder() - override fun roomDirectorySearch(): RoomDirectorySearch = FakeRustRoomDirectorySearch() - override suspend fun setPusher( - identifiers: PusherIdentifiers, - kind: PusherKind, - appDisplayName: String, - deviceDisplayName: String, - profileTag: String?, - lang: String, - ) = Unit - - override suspend fun deletePusher(identifiers: PusherIdentifiers) = Unit -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt deleted file mode 100644 index 21c62cf3ebe..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.Client -import org.matrix.rustcomponents.sdk.ClientBuilder -import org.matrix.rustcomponents.sdk.ClientSessionDelegate -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.OidcConfiguration -import org.matrix.rustcomponents.sdk.QrCodeData -import org.matrix.rustcomponents.sdk.QrLoginProgressListener -import org.matrix.rustcomponents.sdk.RequestConfig -import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder -import uniffi.matrix_sdk.BackupDownloadStrategy -import uniffi.matrix_sdk_crypto.CollectStrategy -import uniffi.matrix_sdk_crypto.TrustRequirement - -class FakeRustClientBuilder : ClientBuilder(NoPointer) { - override fun addRootCertificates(certificates: List) = this - override fun autoEnableBackups(autoEnableBackups: Boolean) = this - override fun autoEnableCrossSigning(autoEnableCrossSigning: Boolean) = this - override fun backupDownloadStrategy(backupDownloadStrategy: BackupDownloadStrategy) = this - override fun disableAutomaticTokenRefresh() = this - override fun disableBuiltInRootCertificates() = this - override fun roomDecryptionTrustRequirement(trustRequirement: TrustRequirement) = this - override fun disableSslVerification() = this - override fun homeserverUrl(url: String) = this - override fun passphrase(passphrase: String?) = this - override fun proxy(url: String) = this - override fun requestConfig(config: RequestConfig) = this - override fun roomKeyRecipientStrategy(strategy: CollectStrategy) = this - override fun serverName(serverName: String) = this - override fun serverNameOrHomeserverUrl(serverNameOrUrl: String) = this - override fun sessionPaths(dataPath: String, cachePath: String) = this - override fun setSessionDelegate(sessionDelegate: ClientSessionDelegate) = this - override fun slidingSyncVersionBuilder(versionBuilder: SlidingSyncVersionBuilder) = this - override fun userAgent(userAgent: String) = this - override fun username(username: String) = this - override fun useEventCachePersistentStorage(value: Boolean) = this - - override suspend fun buildWithQrCode(qrCodeData: QrCodeData, oidcConfiguration: OidcConfiguration, progressListener: QrLoginProgressListener): Client { - return FakeRustClient() - } - - override suspend fun build(): Client { - return FakeRustClient() - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEncryption.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEncryption.kt deleted file mode 100644 index c966025139c..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEncryption.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.Encryption -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.RecoveryStateListener -import org.matrix.rustcomponents.sdk.TaskHandle -import org.matrix.rustcomponents.sdk.VerificationStateListener - -class FakeRustEncryption : Encryption(NoPointer) { - override fun verificationStateListener(listener: VerificationStateListener): TaskHandle { - return FakeRustTaskHandle() - } - - override fun recoveryStateListener(listener: RecoveryStateListener): TaskHandle { - return FakeRustTaskHandle() - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustHomeserverLoginDetails.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustHomeserverLoginDetails.kt deleted file mode 100644 index 2cfca5f1b5c..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustHomeserverLoginDetails.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.HomeserverLoginDetails -import org.matrix.rustcomponents.sdk.NoPointer - -class FakeRustHomeserverLoginDetails( - private val url: String = "https://example.org", - private val supportsPasswordLogin: Boolean = true, - private val supportsOidcLogin: Boolean = false -) : HomeserverLoginDetails(NoPointer) { - override fun url(): String = url - override fun supportsOidcLogin(): Boolean = supportsOidcLogin - override fun supportsPasswordLogin(): Boolean = supportsPasswordLogin -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustLazyTimelineItemProvider.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustLazyTimelineItemProvider.kt deleted file mode 100644 index c6c4a7aa72c..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustLazyTimelineItemProvider.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import io.element.android.libraries.matrix.impl.fixtures.factories.anEventTimelineItemDebugInfo -import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo -import org.matrix.rustcomponents.sdk.LazyTimelineItemProvider -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.SendHandle -import org.matrix.rustcomponents.sdk.ShieldState - -class FakeRustLazyTimelineItemProvider( - private val debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(), - private val shieldsState: ShieldState? = null, -) : LazyTimelineItemProvider(NoPointer) { - override fun getShields(strict: Boolean) = shieldsState - override fun debugInfo() = debugInfo - override fun getSendHandle(): SendHandle? = null -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationClient.kt deleted file mode 100644 index 1e607351b91..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationClient.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import io.element.android.tests.testutils.simulateLongTask -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.NotificationClient -import org.matrix.rustcomponents.sdk.NotificationItem - -class FakeRustNotificationClient( - var notificationItemResult: NotificationItem? = null -) : NotificationClient(NoPointer) { - override suspend fun getNotification(roomId: String, eventId: String): NotificationItem? = simulateLongTask { - notificationItemResult - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationSettings.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationSettings.kt deleted file mode 100644 index 3a96102cb65..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationSettings.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomNotificationSettings -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.NotificationSettings -import org.matrix.rustcomponents.sdk.NotificationSettingsDelegate -import org.matrix.rustcomponents.sdk.RoomNotificationSettings - -class FakeRustNotificationSettings( - private val roomNotificationSettings: RoomNotificationSettings = aRustRoomNotificationSettings(), -) : NotificationSettings(NoPointer) { - private var delegate: NotificationSettingsDelegate? = null - - override fun setDelegate(delegate: NotificationSettingsDelegate?) { - this.delegate = delegate - } - - override suspend fun getRoomNotificationSettings( - roomId: String, - isEncrypted: Boolean, - isOneToOne: Boolean, - ): RoomNotificationSettings = roomNotificationSettings -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt deleted file mode 100644 index 8704f5ee1ba..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.tests.testutils.lambda.lambdaError -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.RoomMembersIterator - -class FakeRustRoom( - private val roomId: RoomId = A_ROOM_ID, - private val getMembers: () -> RoomMembersIterator = { lambdaError() }, - private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() }, -) : Room(NoPointer) { - override fun id(): String { - return roomId.value - } - - override suspend fun members(): RoomMembersIterator { - return getMembers() - } - - override suspend fun membersNoSync(): RoomMembersIterator { - return getMembersNoSync() - } - - override fun close() { - // No-op - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomDirectorySearch.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomDirectorySearch.kt deleted file mode 100644 index 0b2fff64172..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomDirectorySearch.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import io.element.android.tests.testutils.simulateLongTask -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.RoomDirectorySearch -import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntriesListener -import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate -import org.matrix.rustcomponents.sdk.TaskHandle - -class FakeRustRoomDirectorySearch( - var isAtLastPage: Boolean = false, -) : RoomDirectorySearch(NoPointer) { - override suspend fun isAtLastPage(): Boolean { - return isAtLastPage - } - - override suspend fun search(filter: String?, batchSize: UInt, viaServerName: String?) = simulateLongTask { } - override suspend fun nextPage() = simulateLongTask { } - - private var listener: RoomDirectorySearchEntriesListener? = null - - override suspend fun results(listener: RoomDirectorySearchEntriesListener): TaskHandle { - this.listener = listener - return FakeRustTaskHandle() - } - - fun emitResult(roomEntriesUpdate: List) { - listener?.onUpdate(roomEntriesUpdate) - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomList.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomList.kt deleted file mode 100644 index e86e5fd27f3..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomList.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.RoomList - -class FakeRustRoomList : RoomList(NoPointer) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListItem.kt deleted file mode 100644 index acc43a6b24f..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListItem.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo -import org.matrix.rustcomponents.sdk.EventTimelineItem -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.RoomInfo -import org.matrix.rustcomponents.sdk.RoomListItem - -class FakeRustRoomListItem( - private val roomId: RoomId, - private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value), - private val latestEvent: EventTimelineItem? = null, -) : RoomListItem(NoPointer) { - override fun id(): String { - return roomId.value - } - - override suspend fun roomInfo(): RoomInfo { - return roomInfo - } - - override suspend fun latestEvent(): EventTimelineItem? { - return latestEvent - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListService.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListService.kt deleted file mode 100644 index 1251bd514f2..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListService.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.RoomList -import org.matrix.rustcomponents.sdk.RoomListService -import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator -import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener -import org.matrix.rustcomponents.sdk.TaskHandle - -class FakeRustRoomListService : RoomListService(NoPointer) { - override suspend fun allRooms(): RoomList { - return FakeRustRoomList() - } - - private var listener: RoomListServiceSyncIndicatorListener? = null - override fun syncIndicator( - delayBeforeShowingInMs: UInt, - delayBeforeHidingInMs: UInt, - listener: RoomListServiceSyncIndicatorListener, - ): TaskHandle { - this.listener = listener - return FakeRustTaskHandle() - } - - fun emitRoomListServiceSyncIndicator(syncIndicator: RoomListServiceSyncIndicator) { - listener?.onUpdate(syncIndicator) - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomMembersIterator.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomMembersIterator.kt deleted file mode 100644 index 2ef8ba56db0..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomMembersIterator.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.RoomMember -import org.matrix.rustcomponents.sdk.RoomMembersIterator - -class FakeRustRoomMembersIterator( - private var members: List? = null -) : RoomMembersIterator(NoPointer) { - override fun len(): UInt { - return members?.size?.toUInt() ?: 0u - } - - override fun nextChunk(chunkSize: UInt): List? { - if (members?.isEmpty() == true) { - return null - } - return members?.let { - val result = it.take(chunkSize.toInt()) - members = it.subList(result.size, it.size) - result - } - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncService.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncService.kt deleted file mode 100644 index 734968bedab..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncService.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.RoomListService -import org.matrix.rustcomponents.sdk.SyncService - -class FakeRustSyncService( - private val roomListService: RoomListService = FakeRustRoomListService(), -) : SyncService(NoPointer) { - override fun roomListService(): RoomListService = roomListService -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncServiceBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncServiceBuilder.kt deleted file mode 100644 index 0ddfe16eea8..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncServiceBuilder.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.SyncService -import org.matrix.rustcomponents.sdk.SyncServiceBuilder -import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate - -class FakeRustSyncServiceBuilder : SyncServiceBuilder(NoPointer) { - override suspend fun withUtdHook(delegate: UnableToDecryptDelegate): SyncServiceBuilder = this - override fun withOfflineMode(): SyncServiceBuilder = this - override suspend fun finish(): SyncService = FakeRustSyncService() -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTaskHandle.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTaskHandle.kt deleted file mode 100644 index 7aaefb2fd8f..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTaskHandle.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.TaskHandle - -class FakeRustTaskHandle : TaskHandle(NoPointer) { - override fun cancel() = Unit - override fun destroy() = Unit -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt deleted file mode 100644 index 042d4c98a19..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.PaginationStatusListener -import org.matrix.rustcomponents.sdk.TaskHandle -import org.matrix.rustcomponents.sdk.Timeline -import org.matrix.rustcomponents.sdk.TimelineDiff -import org.matrix.rustcomponents.sdk.TimelineListener -import uniffi.matrix_sdk_ui.LiveBackPaginationStatus - -class FakeRustTimeline : Timeline(NoPointer) { - private var listener: TimelineListener? = null - override suspend fun addListener(listener: TimelineListener): TaskHandle { - this.listener = listener - return FakeRustTaskHandle() - } - - fun emitDiff(diff: List) { - listener!!.onUpdate(diff) - } - - private var paginationStatusListener: PaginationStatusListener? = null - override suspend fun subscribeToBackPaginationStatus(listener: PaginationStatusListener): TaskHandle { - this.paginationStatusListener = listener - return FakeRustTaskHandle() - } - - fun emitPaginationStatus(status: LiveBackPaginationStatus) { - paginationStatusListener!!.onUpdate(status) - } - - override suspend fun fetchMembers() = Unit -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineDiff.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineDiff.kt deleted file mode 100644 index f3d0fb0efa1..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineDiff.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.InsertData -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.SetData -import org.matrix.rustcomponents.sdk.TimelineChange -import org.matrix.rustcomponents.sdk.TimelineDiff -import org.matrix.rustcomponents.sdk.TimelineItem - -class FakeRustTimelineDiff( - private val change: TimelineChange, - private val item: TimelineItem? = FakeRustTimelineItem() -) : TimelineDiff(NoPointer) { - override fun change() = change - override fun append(): List? = item?.let { listOf(it) } - override fun insert(): InsertData? = item?.let { InsertData(1u, it) } - override fun pushBack(): TimelineItem? = item - override fun pushFront(): TimelineItem? = item - override fun remove(): UInt? = 1u - override fun reset(): List? = item?.let { listOf(it) } - override fun set(): SetData? = item?.let { SetData(1u, it) } - override fun truncate(): UInt? = 1u -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEvent.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEvent.kt deleted file mode 100644 index 3b70fb2670e..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEvent.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustTimelineEventTypeMessageLike -import io.element.android.libraries.matrix.test.A_USER_ID_2 -import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.TimelineEvent -import org.matrix.rustcomponents.sdk.TimelineEventType - -class FakeRustTimelineEvent( - val timestamp: ULong = A_FAKE_TIMESTAMP.toULong(), - val timelineEventType: TimelineEventType = aRustTimelineEventTypeMessageLike(), - val senderId: String = A_USER_ID_2.value, -) : TimelineEvent(NoPointer) { - override fun timestamp(): ULong = timestamp - override fun eventType(): TimelineEventType = timelineEventType - override fun senderId(): String = senderId -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEventTypeFilter.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEventTypeFilter.kt deleted file mode 100644 index dae1eb366cb..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEventTypeFilter.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter - -class FakeRustTimelineEventTypeFilter : TimelineEventTypeFilter(NoPointer) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt deleted file mode 100644 index e6f1272fb74..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.EventTimelineItem -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.TimelineItem -import org.matrix.rustcomponents.sdk.TimelineUniqueId -import org.matrix.rustcomponents.sdk.VirtualTimelineItem - -class FakeRustTimelineItem( - private val asEventResult: EventTimelineItem? = null, -) : TimelineItem(NoPointer) { - override fun asEvent(): EventTimelineItem? = asEventResult - override fun asVirtual(): VirtualTimelineItem? = null - override fun fmtDebug(): String = "fmtDebug" - override fun uniqueId(): TimelineUniqueId = TimelineUniqueId("uniqueId") -} 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 ce0fc49ae2f..46a4d35c376 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 @@ -72,7 +72,7 @@ class SessionKtTest { @Test fun `toSessionData copy the sliding sync url if present`() { val result = aRustSession( - proxy = SlidingSyncVersion.Proxy("proxyUrl") + proxy = SlidingSyncVersion.NATIVE ).toSessionData( isTokenValid = true, loginType = LoginType.PASSWORD, @@ -80,7 +80,7 @@ class SessionKtTest { sessionPaths = SessionPaths(File("/a/file"), File("/a/cache")), homeserverUrl = A_HOMESERVER_URL_2, ) - assertThat(result.slidingSyncProxy).isEqualTo("proxyUrl") + assertThat(result.slidingSyncProxy).isNull() } @Test diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt index 71623337cd9..3166acc8914 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt @@ -11,13 +11,15 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.impl.fixtures.factories.aRustNotificationItem -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustNotificationClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiNotificationClient import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -27,13 +29,13 @@ import org.matrix.rustcomponents.sdk.NotificationClient class RustNotificationServiceTest { @Test fun test() = runTest { - val notificationClient = FakeRustNotificationClient( - notificationItemResult = aRustNotificationItem(), + val notificationClient = FakeFfiNotificationClient( + notificationItemResult = mapOf(AN_EVENT_ID.value to aRustNotificationItem()), ) val sut = createRustNotificationService( notificationClient = notificationClient, ) - val result = sut.getNotification(A_ROOM_ID, AN_EVENT_ID).getOrThrow()!! + val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!! assertThat(result.isEncrypted).isTrue() assertThat(result.content).isEqualTo( NotificationContent.MessageLike.RoomMessage( @@ -46,11 +48,39 @@ class RustNotificationServiceTest { ) } + @Test + fun `test unable to resolve event`() = runTest { + val notificationClient = FakeFfiNotificationClient( + notificationItemResult = emptyMap(), + ) + val sut = createRustNotificationService( + notificationClient = notificationClient, + ) + val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!! + assertThat(result.content).isEqualTo( + NotificationContent.MessageLike.UnableToResolve + ) + } + + @Test + fun `close should invoke the close method of the service`() = runTest { + val closeResult = lambdaRecorder { } + val notificationClient = FakeFfiNotificationClient( + closeResult = closeResult, + ) + val sut = createRustNotificationService( + notificationClient = notificationClient, + ) + sut.close() + closeResult.assertions().isCalledOnce() + } + private fun TestScope.createRustNotificationService( - notificationClient: NotificationClient = FakeRustNotificationClient(), + notificationClient: NotificationClient = FakeFfiNotificationClient(), clock: SystemClock = FakeSystemClock(), ) = RustNotificationService( + sessionId = A_SESSION_ID, notificationClient = notificationClient, dispatchers = testCoroutineDispatchers(), clock = clock, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt index 83461b97506..d2dd4251327 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt @@ -9,8 +9,8 @@ package io.element.android.libraries.matrix.impl.notificationsettings import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustNotificationSettings +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiNotificationSettings import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope @@ -32,11 +32,12 @@ class RustNotificationSettingsServiceTest { } private fun TestScope.createRustNotificationSettingsService( - notificationSettings: NotificationSettings = FakeRustNotificationSettings(), + notificationSettings: NotificationSettings = FakeFfiNotificationSettings(), ) = RustNotificationSettingsService( - client = FakeRustClient( + client = FakeFfiClient( notificationSettings = notificationSettings, ), + sessionCoroutineScope = this, dispatchers = testCoroutineDispatchers(), ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt index bedfe172811..22e015a27c3 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.pushers import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.runTest import org.junit.Test @@ -18,7 +18,7 @@ class RustPushersServiceTest { @Test fun `setPusher should invoke the client method`() = runTest { val sut = RustPushersService( - client = FakeRustClient(), + client = FakeFfiClient(), dispatchers = testCoroutineDispatchers() ) sut.setHttpPusher( @@ -29,7 +29,7 @@ class RustPushersServiceTest { @Test fun `unsetPusher should invoke the client method`() = runTest { val sut = RustPushersService( - client = FakeRustClient(), + client = FakeFfiClient(), dispatchers = testCoroutineDispatchers() ) sut.unsetHttpPusher( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventTypeFilterFactory.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventTypeFilterFactory.kt index abb6004ac46..d7d800ac239 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventTypeFilterFactory.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventTypeFilterFactory.kt @@ -8,11 +8,11 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.matrix.api.room.StateEventType -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineEventTypeFilter +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEventTypeFilter import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter class FakeTimelineEventTypeFilterFactory : TimelineEventTypeFilterFactory { override fun create(listStateEventType: List): TimelineEventTypeFilter { - return FakeRustTimelineEventTypeFilter() + return FakeFfiTimelineEventTypeFilter() } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt deleted file mode 100644 index aaa8ecc9ec5..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.room - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.RoomAlias -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.MatrixRoomInfo -import io.element.android.libraries.matrix.api.room.RoomNotificationMode -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.user.MatrixUser -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomHero -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomMember -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_ALIAS -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.A_USER_ID_3 -import io.element.android.libraries.matrix.test.A_USER_ID_6 -import io.element.android.libraries.matrix.test.room.aRoomMember -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toImmutableMap -import kotlinx.collections.immutable.toPersistentList -import org.junit.Test -import org.matrix.rustcomponents.sdk.Membership -import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule -import org.matrix.rustcomponents.sdk.RoomHistoryVisibility as RustRoomHistoryVisibility -import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode - -class MatrixRoomInfoMapperTest { - @Test - fun `mapping of RustRoomInfo should map all the fields`() { - assertThat( - MatrixRoomInfoMapper().map( - aRustRoomInfo( - id = A_ROOM_ID.value, - displayName = "displayName", - rawName = "rawName", - topic = "topic", - avatarUrl = AN_AVATAR_URL, - isDirect = true, - isPublic = false, - isSpace = false, - joinRule = RustJoinRule.Invite, - isTombstoned = false, - isFavourite = false, - canonicalAlias = A_ROOM_ALIAS.value, - alternativeAliases = listOf(A_ROOM_ALIAS.value), - membership = Membership.JOINED, - inviter = aRustRoomMember(A_USER_ID), - heroes = listOf(aRustRoomHero()), - activeMembersCount = 2uL, - invitedMembersCount = 3uL, - joinedMembersCount = 4uL, - userPowerLevels = mapOf(A_USER_ID_6.value to 50L), - highlightCount = 10uL, - notificationCount = 11uL, - userDefinedNotificationMode = RustRoomNotificationMode.MUTE, - hasRoomCall = true, - activeRoomCallParticipants = listOf(A_USER_ID_3.value), - isMarkedUnread = false, - numUnreadMessages = 12uL, - numUnreadNotifications = 13uL, - numUnreadMentions = 14uL, - pinnedEventIds = listOf(AN_EVENT_ID.value), - roomCreator = A_USER_ID, - historyVisibility = RustRoomHistoryVisibility.Joined, - ) - ) - ).isEqualTo( - MatrixRoomInfo( - id = A_ROOM_ID, - name = "displayName", - rawName = "rawName", - topic = "topic", - avatarUrl = AN_AVATAR_URL, - isDirect = true, - isPublic = false, - isSpace = false, - isTombstoned = false, - isFavorite = false, - joinRule = JoinRule.Invite, - canonicalAlias = A_ROOM_ALIAS, - alternativeAliases = listOf(A_ROOM_ALIAS).toImmutableList(), - currentUserMembership = CurrentUserMembership.JOINED, - inviter = aRoomMember(A_USER_ID), - activeMembersCount = 2L, - invitedMembersCount = 3L, - joinedMembersCount = 4L, - userPowerLevels = mapOf(A_USER_ID_6 to 50L).toImmutableMap(), - highlightCount = 10L, - notificationCount = 11L, - userDefinedNotificationMode = RoomNotificationMode.MUTE, - hasRoomCall = true, - activeRoomCallParticipants = listOf(A_USER_ID_3).toImmutableList(), - heroes = listOf( - MatrixUser( - userId = A_USER_ID, - displayName = "displayName", - avatarUrl = "avatarUrl", - ) - ).toImmutableList(), - pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(), - creator = A_USER_ID, - isMarkedUnread = false, - numUnreadMessages = 12L, - numUnreadNotifications = 13L, - numUnreadMentions = 14L, - historyVisibility = RoomHistoryVisibility.Joined, - ) - ) - } - - @Test - fun `mapping of RustRoomInfo with null members should map all the fields`() { - assertThat( - MatrixRoomInfoMapper().map( - aRustRoomInfo( - id = A_ROOM_ID.value, - displayName = null, - rawName = null, - topic = null, - avatarUrl = null, - isDirect = false, - isPublic = true, - joinRule = null, - isSpace = false, - isTombstoned = false, - isFavourite = true, - canonicalAlias = null, - alternativeAliases = emptyList(), - membership = Membership.INVITED, - inviter = null, - heroes = listOf(aRustRoomHero()), - activeMembersCount = 2uL, - invitedMembersCount = 3uL, - joinedMembersCount = 4uL, - userPowerLevels = emptyMap(), - highlightCount = 10uL, - notificationCount = 11uL, - userDefinedNotificationMode = null, - hasRoomCall = false, - activeRoomCallParticipants = emptyList(), - isMarkedUnread = true, - numUnreadMessages = 12uL, - numUnreadNotifications = 13uL, - numUnreadMentions = 14uL, - pinnedEventIds = emptyList(), - roomCreator = null, - ) - ) - ).isEqualTo( - MatrixRoomInfo( - id = A_ROOM_ID, - name = null, - rawName = null, - topic = null, - avatarUrl = null, - isDirect = false, - isPublic = true, - joinRule = null, - isSpace = false, - isTombstoned = false, - isFavorite = true, - canonicalAlias = null, - alternativeAliases = emptyList().toPersistentList(), - currentUserMembership = CurrentUserMembership.INVITED, - inviter = null, - activeMembersCount = 2L, - invitedMembersCount = 3L, - joinedMembersCount = 4L, - userPowerLevels = emptyMap().toImmutableMap(), - highlightCount = 10L, - notificationCount = 11L, - userDefinedNotificationMode = null, - hasRoomCall = false, - activeRoomCallParticipants = emptyList().toImmutableList(), - heroes = emptyList().toImmutableList(), - pinnedEventIds = emptyList().toPersistentList(), - creator = null, - isMarkedUnread = true, - numUnreadMessages = 12L, - numUnreadNotifications = 13L, - numUnreadMentions = 14L, - historyVisibility = RoomHistoryVisibility.Joined, - ) - ) - } - - @Test - fun `mapping Membership`() { - assertThat(Membership.INVITED.map()).isEqualTo(CurrentUserMembership.INVITED) - assertThat(Membership.JOINED.map()).isEqualTo(CurrentUserMembership.JOINED) - assertThat(Membership.LEFT.map()).isEqualTo(CurrentUserMembership.LEFT) - } - - @Test - fun `mapping RoomNotificationMode`() { - assertThat(RustRoomNotificationMode.ALL_MESSAGES.map()).isEqualTo(RoomNotificationMode.ALL_MESSAGES) - assertThat(RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY.map()).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) - assertThat(RustRoomNotificationMode.MUTE.map()).isEqualTo(RoomNotificationMode.MUTE) - } -} 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 new file mode 100644 index 00000000000..7140a90dc40 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapperTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.room + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomAlias +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.RoomInfo +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +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.powerlevels.RoomPowerLevels +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomHero +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomMember +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomPowerLevels +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_ALIAS +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.A_USER_ID_6 +import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevelValues +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import org.junit.Test +import org.matrix.rustcomponents.sdk.Membership +import uniffi.matrix_sdk_base.EncryptionState +import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule +import org.matrix.rustcomponents.sdk.RoomHistoryVisibility as RustRoomHistoryVisibility +import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode + +class RoomInfoMapperTest { + @Test + fun `mapping of RustRoomInfo should map all the fields`() { + assertThat( + RoomInfoMapper().map( + aRustRoomInfo( + id = A_ROOM_ID.value, + displayName = "displayName", + rawName = "rawName", + topic = "topic", + avatarUrl = AN_AVATAR_URL, + encryptionState = EncryptionState.ENCRYPTED, + isDirect = true, + isPublic = false, + isSpace = false, + joinRule = RustJoinRule.Invite, + successorRoom = null, + isFavourite = false, + canonicalAlias = A_ROOM_ALIAS.value, + alternativeAliases = listOf(A_ROOM_ALIAS.value), + membership = Membership.JOINED, + inviter = aRustRoomMember(A_USER_ID), + heroes = listOf(aRustRoomHero()), + activeMembersCount = 2uL, + invitedMembersCount = 3uL, + joinedMembersCount = 4uL, + roomPowerLevels = FakeFfiRoomPowerLevels(users = mapOf(A_USER_ID_6.value to 50L)), + highlightCount = 10uL, + notificationCount = 11uL, + userDefinedNotificationMode = RustRoomNotificationMode.MUTE, + hasRoomCall = true, + activeRoomCallParticipants = listOf(A_USER_ID_3.value), + isMarkedUnread = false, + numUnreadMessages = 12uL, + numUnreadNotifications = 13uL, + numUnreadMentions = 14uL, + pinnedEventIds = listOf(AN_EVENT_ID.value), + roomCreator = A_USER_ID, + historyVisibility = RustRoomHistoryVisibility.Joined, + ) + ) + ).isEqualTo( + RoomInfo( + id = A_ROOM_ID, + name = "displayName", + rawName = "rawName", + topic = "topic", + avatarUrl = AN_AVATAR_URL, + isPublic = false, + isDirect = true, + isEncrypted = true, + isSpace = false, + isFavorite = false, + joinRule = JoinRule.Invite, + canonicalAlias = A_ROOM_ALIAS, + alternativeAliases = listOf(A_ROOM_ALIAS).toImmutableList(), + currentUserMembership = CurrentUserMembership.JOINED, + inviter = aRoomMember(A_USER_ID), + activeMembersCount = 2L, + invitedMembersCount = 3L, + joinedMembersCount = 4L, + roomPowerLevels = RoomPowerLevels( + values = defaultRoomPowerLevelValues(), + users = persistentMapOf(A_USER_ID_6 to 50L) + ), + highlightCount = 10L, + notificationCount = 11L, + userDefinedNotificationMode = RoomNotificationMode.MUTE, + hasRoomCall = true, + activeRoomCallParticipants = listOf(A_USER_ID_3).toImmutableList(), + heroes = listOf( + MatrixUser( + userId = A_USER_ID, + displayName = "displayName", + avatarUrl = "avatarUrl", + ) + ).toImmutableList(), + pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(), + creator = A_USER_ID, + isMarkedUnread = false, + numUnreadMessages = 12L, + numUnreadNotifications = 13L, + numUnreadMentions = 14L, + historyVisibility = RoomHistoryVisibility.Joined, + successorRoom = null + ) + ) + } + + @Test + fun `mapping of RustRoomInfo with null members should map all the fields`() { + assertThat( + RoomInfoMapper().map( + aRustRoomInfo( + id = A_ROOM_ID.value, + displayName = null, + rawName = null, + topic = null, + avatarUrl = null, + encryptionState = EncryptionState.UNKNOWN, + isDirect = false, + isPublic = true, + joinRule = null, + isSpace = false, + successorRoom = null, + isFavourite = true, + canonicalAlias = null, + alternativeAliases = emptyList(), + membership = Membership.INVITED, + inviter = null, + heroes = listOf(aRustRoomHero()), + activeMembersCount = 2uL, + invitedMembersCount = 3uL, + joinedMembersCount = 4uL, + roomPowerLevels = FakeFfiRoomPowerLevels(), + highlightCount = 10uL, + notificationCount = 11uL, + userDefinedNotificationMode = null, + hasRoomCall = false, + activeRoomCallParticipants = emptyList(), + isMarkedUnread = true, + numUnreadMessages = 12uL, + numUnreadNotifications = 13uL, + numUnreadMentions = 14uL, + pinnedEventIds = emptyList(), + roomCreator = null, + ) + ) + ).isEqualTo( + RoomInfo( + id = A_ROOM_ID, + name = null, + rawName = null, + topic = null, + avatarUrl = null, + isEncrypted = null, + isPublic = true, + isDirect = false, + joinRule = null, + isSpace = false, + successorRoom = null, + isFavorite = true, + canonicalAlias = null, + alternativeAliases = emptyList().toPersistentList(), + currentUserMembership = CurrentUserMembership.INVITED, + inviter = null, + activeMembersCount = 2L, + invitedMembersCount = 3L, + joinedMembersCount = 4L, + roomPowerLevels = RoomPowerLevels( + values = defaultRoomPowerLevelValues(), + users = persistentMapOf(), + ), + highlightCount = 10L, + notificationCount = 11L, + userDefinedNotificationMode = null, + hasRoomCall = false, + activeRoomCallParticipants = emptyList().toImmutableList(), + heroes = emptyList().toImmutableList(), + pinnedEventIds = emptyList().toPersistentList(), + creator = null, + isMarkedUnread = true, + numUnreadMessages = 12L, + numUnreadNotifications = 13L, + numUnreadMentions = 14L, + historyVisibility = RoomHistoryVisibility.Joined, + ) + ) + } + + @Test + fun `mapping Membership`() { + assertThat(Membership.INVITED.map()).isEqualTo(CurrentUserMembership.INVITED) + assertThat(Membership.JOINED.map()).isEqualTo(CurrentUserMembership.JOINED) + assertThat(Membership.LEFT.map()).isEqualTo(CurrentUserMembership.LEFT) + } + + @Test + fun `mapping RoomNotificationMode`() { + assertThat(RustRoomNotificationMode.ALL_MESSAGES.map()).isEqualTo(RoomNotificationMode.ALL_MESSAGES) + assertThat(RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY.map()).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + assertThat(RustRoomNotificationMode.MUTE.map()).isEqualTo(RoomNotificationMode.MUTE) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt new file mode 100644 index 00000000000..f25f568c5a9 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt @@ -0,0 +1,150 @@ +/* + * 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.matrix.impl.room + +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +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.RoomMembershipObserver +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoom +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService +import io.element.android.libraries.matrix.test.A_DEVICE_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RustBaseRoomTest { + @Test + fun `RustBaseRoom should cancel the room coroutine scope when it is destroyed`() = runTest { + val rustBaseRoom = createRustBaseRoom() + assertThat(rustBaseRoom.roomCoroutineScope.isActive).isTrue() + rustBaseRoom.destroy() + assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse() + } + + @Test + fun `when currentUserMembership=JOINED and user leave room succeed then roomMembershipObserver emits change as LEFT`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val rustBaseRoom = createRustBaseRoom( + initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.JOINED), + innerRoom = FakeFfiRoom( + leaveLambda = { + // Simulate a successful leave + } + ), + roomMembershipObserver = roomMembershipObserver, + ) + leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { + val membershipUpdate = awaitItem() + assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isUserInRoom).isFalse() + assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT) + } + } + + @Test + fun `when currentUserMembership=KNOCKED and user leave room succeed then roomMembershipObserver emits change as KNOCK_RETRACTED`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val rustBaseRoom = createRustBaseRoom( + initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.KNOCKED), + innerRoom = FakeFfiRoom( + leaveLambda = { + // Simulate a successful leave + } + ), + roomMembershipObserver = roomMembershipObserver, + ) + leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { + val membershipUpdate = awaitItem() + assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isUserInRoom).isFalse() + assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED) + } + } + + @Test + fun `when currentUserMembership=INVITED and user leave room succeed then roomMembershipObserver emits change as INVITATION_REJECTED`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val rustBaseRoom = createRustBaseRoom( + initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED), + innerRoom = FakeFfiRoom( + leaveLambda = { + // Simulate a successful leave + } + ), + roomMembershipObserver = roomMembershipObserver, + ) + leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { + val membershipUpdate = awaitItem() + assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isUserInRoom).isFalse() + assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED) + } + } + + @Test + fun `when user leave room fails then roomMembershipObserver emits nothing`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val rustBaseRoom = createRustBaseRoom( + initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED), + innerRoom = FakeFfiRoom( + leaveLambda = { error("Leave failed") } + ), + roomMembershipObserver = roomMembershipObserver, + ) + leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) { + // No emit + } + } + + private suspend fun TestScope.leaveRoomAndObserveMembershipChange( + roomMembershipObserver: RoomMembershipObserver, + rustBaseRoom: RustBaseRoom, + validate: suspend TurbineTestContext.() -> Unit + ) { + val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1) + rustBaseRoom.leave() + shared.test { + validate() + ensureAllEventsConsumed() + } + rustBaseRoom.destroy() + } + + private fun TestScope.createRustBaseRoom( + initialRoomInfo: RoomInfo = aRoomInfo(), + innerRoom: FakeFfiRoom = FakeFfiRoom(), + roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), + ): RustBaseRoom { + val dispatchers = testCoroutineDispatchers() + return RustBaseRoom( + sessionId = A_SESSION_ID, + deviceId = A_DEVICE_ID, + innerRoom = innerRoom, + coroutineDispatchers = dispatchers, + roomSyncSubscriber = RoomSyncSubscriber( + roomListService = FakeFfiRoomListService(), + dispatchers = dispatchers, + ), + roomMembershipObserver = roomMembershipObserver, + // Not using backgroundScope here, but the test scope + sessionCoroutineScope = this, + roomInfoMapper = RoomInfoMapper(), + initialRoomInfo = initialRoomInfo, + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt index ede0b4b728e..d33148931e6 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt @@ -18,8 +18,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.room.aRoomSummary +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -29,10 +29,12 @@ import org.junit.Test class DefaultJoinRoomTest { @Test fun `when using roomId and there is no server names, the classic join room API is used`() = runTest { - val roomSummary = aRoomSummary() - val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } - val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomSummary) } - val roomResult = FakeMatrixRoom() + val roomInfo = aRoomInfo() + val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomInfo) } + val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomInfo) } + val roomResult = FakeBaseRoom().apply { + givenRoomInfo(aRoomInfo()) + } val aTrigger = JoinedRoom.Trigger.MobilePermalink val client: MatrixClient = FakeMatrixClient().also { it.joinRoomLambda = joinRoomLambda @@ -64,10 +66,12 @@ class DefaultJoinRoomTest { @Test fun `when using roomId and server names are available, joinRoomByIdOrAlias API is used`() = runTest { - val roomSummary = aRoomSummary() - val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } - val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomSummary) } - val roomResult = FakeMatrixRoom() + val roomInfo = aRoomInfo() + val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomInfo) } + val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomInfo) } + val roomResult = FakeBaseRoom().apply { + givenRoomInfo(aRoomInfo()) + } val aTrigger = JoinedRoom.Trigger.MobilePermalink val client: MatrixClient = FakeMatrixClient().also { it.joinRoomLambda = joinRoomLambda @@ -100,10 +104,12 @@ class DefaultJoinRoomTest { @Test fun `when using roomAlias, joinRoomByIdOrAlias API is used`() = runTest { - val roomSummary = aRoomSummary() - val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } - val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomSummary) } - val roomResult = FakeMatrixRoom() + val roomInfo = aRoomInfo() + val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomInfo) } + val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomInfo) } + val roomResult = FakeBaseRoom().apply { + givenRoomInfo(aRoomInfo()) + } val aTrigger = JoinedRoom.Trigger.MobilePermalink val client: MatrixClient = FakeMatrixClient().also { it.joinRoomLambda = joinRoomLambda diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt index 19a310c4be1..b11a3a6cd59 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt @@ -9,11 +9,11 @@ package io.element.android.libraries.matrix.impl.room.member import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomMember -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomMembersIterator +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoom +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomMembersIterator import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE_AND_SERVER import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.SERVER @@ -28,8 +28,8 @@ import org.junit.Test class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with CACHE source - emits cached members, if any`() = runTest { - val room = FakeRustRoom(getMembersNoSync = { - FakeRustRoomMembersIterator( + val room = FakeFfiRoom(getMembersNoSync = { + FakeFfiRoomMembersIterator( listOf( aRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID_2), @@ -40,53 +40,53 @@ class RoomMemberListFetcherTest { val fetcher = RoomMemberListFetcher(room, Dispatchers.Default) fetcher.membersFlow.test { - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java) fetcher.fetchRoomMembers(source = CACHE) // Loading state - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java) val cachedItemsState = awaitItem() - assertThat(cachedItemsState).isInstanceOf(MatrixRoomMembersState.Ready::class.java) - assertThat((cachedItemsState as? MatrixRoomMembersState.Ready)?.roomMembers).hasSize(3) + assertThat(cachedItemsState).isInstanceOf(RoomMembersState.Ready::class.java) + assertThat((cachedItemsState as? RoomMembersState.Ready)?.roomMembers).hasSize(3) } } @Test fun `fetchRoomMembers with CACHE source - emits empty list, if no members exist`() = runTest { - val room = FakeRustRoom(getMembersNoSync = { - FakeRustRoomMembersIterator(emptyList()) + val room = FakeFfiRoom(getMembersNoSync = { + FakeFfiRoomMembersIterator(emptyList()) }) val fetcher = RoomMemberListFetcher(room, Dispatchers.Default) fetcher.membersFlow.test { fetcher.fetchRoomMembers(source = CACHE) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java) - assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers).isEmpty() + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java) + assertThat((awaitItem() as? RoomMembersState.Ready)?.roomMembers).isEmpty() } } @Test fun `fetchRoomMembers with CACHE source - emits Error on error found`() = runTest { - val room = FakeRustRoom(getMembersNoSync = { + val room = FakeFfiRoom(getMembersNoSync = { error("Some unexpected issue") }) val fetcher = RoomMemberListFetcher(room, Dispatchers.Default) fetcher.membersFlow.test { fetcher.fetchRoomMembers(source = CACHE) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Error::class.java) } } @Test fun `fetchRoomMembers with CACHE source - emits all items at once`() = runTest { - val room = FakeRustRoom(getMembersNoSync = { - FakeRustRoomMembersIterator( + val room = FakeFfiRoom(getMembersNoSync = { + FakeFfiRoomMembersIterator( listOf( aRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID_2), @@ -100,11 +100,11 @@ class RoomMemberListFetcherTest { fetcher.fetchRoomMembers(source = CACHE) // Initial state - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java) // Started loading cached members - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java) // Finished loading cached members - assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers).hasSize(3) + assertThat((awaitItem() as? RoomMembersState.Ready)?.roomMembers).hasSize(3) ensureAllEventsConsumed() } @@ -112,8 +112,8 @@ class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with SERVER source - emits only new members, if any`() = runTest { - val room = FakeRustRoom(getMembers = { - FakeRustRoomMembersIterator( + val room = FakeFfiRoom(getMembers = { + FakeFfiRoomMembersIterator( listOf( aRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID_2), @@ -126,34 +126,34 @@ class RoomMemberListFetcherTest { fetcher.membersFlow.test { fetcher.fetchRoomMembers(source = SERVER) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java) - assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java) + assertThat((awaitItem() as? RoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3) } } @Test fun `fetchRoomMembers with SERVER source - on error it emits an Error item`() = runTest { - val room = FakeRustRoom(getMembers = { error("An unexpected error") }) + val room = FakeFfiRoom(getMembers = { error("An unexpected error") }) val fetcher = RoomMemberListFetcher(room, Dispatchers.Default) fetcher.membersFlow.test { fetcher.fetchRoomMembers(source = SERVER) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java) - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Pending::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Error::class.java) } } @Test fun `fetchRoomMembers with CACHE_AND_SERVER source - returns cached items first, then new ones`() = runTest { - val room = FakeRustRoom( + val room = FakeFfiRoom( getMembersNoSync = { - FakeRustRoomMembersIterator(listOf(aRustRoomMember(A_USER_ID_4))) + FakeFfiRoomMembersIterator(listOf(aRustRoomMember(A_USER_ID_4))) }, getMembers = { - FakeRustRoomMembersIterator( + FakeFfiRoomMembersIterator( listOf( aRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID_2), @@ -167,20 +167,20 @@ class RoomMemberListFetcherTest { fetcher.membersFlow.test { fetcher.fetchRoomMembers(source = CACHE_AND_SERVER) // Initial - assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java) + assertThat(awaitItem()).isInstanceOf(RoomMembersState.Unknown::class.java) // Loading cached awaitItem().let { pending -> - assertThat(pending).isInstanceOf(MatrixRoomMembersState.Pending::class.java) + assertThat(pending).isInstanceOf(RoomMembersState.Pending::class.java) assertThat(pending.roomMembers()).isEmpty() } // Loaded cached awaitItem().let { cached -> - assertThat(cached).isInstanceOf(MatrixRoomMembersState.Pending::class.java) + assertThat(cached).isInstanceOf(RoomMembersState.Pending::class.java) assertThat(cached.roomMembers()).hasSize(1) } // Start loading new awaitItem().let { ready -> - assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java) + assertThat(ready).isInstanceOf(RoomMembersState.Ready::class.java) assertThat(ready.roomMembers()).hasSize(3) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapperTest.kt deleted file mode 100644 index 2a8f733bb8f..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapperTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.room.powerlevels - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPowerLevels -import org.junit.Test - -class RoomPowerLevelsMapperTest { - @Test - fun `test that mapping of RoomPowerLevels is correct`() { - assertThat( - RoomPowerLevelsMapper.map( - aRustRoomPowerLevels( - ban = 1, - invite = 2, - kick = 3, - redact = 4, - eventsDefault = 5, - stateDefault = 6, - usersDefault = 7, - roomName = 8, - roomAvatar = 9, - roomTopic = 10, - ) - ) - ).isEqualTo( - MatrixRoomPowerLevels( - ban = 1, - invite = 2, - kick = 3, - sendEvents = 5, - redactEvents = 4, - roomName = 8, - roomAvatar = 9, - roomTopic = 10, - ) - ) - } -} 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 new file mode 100644 index 00000000000..a2c6fca7a1c --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapperTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.room.powerlevels + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPowerLevelsValues +import org.junit.Test + +class RoomPowerLevelsValuesMapperTest { + @Test + fun `test that mapping of RoomPowerLevelsValues is correct`() { + assertThat( + RoomPowerLevelsValuesMapper.map( + aRustRoomPowerLevelsValues( + ban = 1, + invite = 2, + kick = 3, + redact = 4, + eventsDefault = 5, + stateDefault = 6, + usersDefault = 7, + roomName = 8, + roomAvatar = 9, + roomTopic = 10, + ) + ) + ).isEqualTo( + RoomPowerLevelsValues( + ban = 1, + invite = 2, + kick = 3, + sendEvents = 5, + redactEvents = 4, + roomName = 8, + roomAvatar = 9, + roomTopic = 10, + ) + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewInfoMapperTest.kt index 11a851eee6f..4b832799cca 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewInfoMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewInfoMapperTest.kt @@ -8,14 +8,16 @@ package io.element.android.libraries.matrix.impl.room.preview import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreviewInfo import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ID import org.junit.Test -import org.matrix.rustcomponents.sdk.JoinRule import org.matrix.rustcomponents.sdk.Membership +import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule class RoomPreviewInfoMapperTest { @Test @@ -23,7 +25,7 @@ class RoomPreviewInfoMapperTest { assertThat( RoomPreviewInfoMapper.map( info = aRustRoomPreviewInfo( - membership = null, + membership = Membership.JOINED, ) ) ).isEqualTo( @@ -36,10 +38,8 @@ class RoomPreviewInfoMapperTest { numberOfJoinedMembers = 1L, roomType = RoomType.Room, isHistoryWorldReadable = true, - isJoined = false, - isInvited = false, - isPublic = true, - canKnock = false, + membership = CurrentUserMembership.JOINED, + joinRule = JoinRule.Public, ) ) } @@ -51,7 +51,7 @@ class RoomPreviewInfoMapperTest { info = aRustRoomPreviewInfo( canonicalAlias = null, membership = Membership.JOINED, - joinRule = JoinRule.Knock, + joinRule = RustJoinRule.Knock, ) ) ).isEqualTo( @@ -64,10 +64,8 @@ class RoomPreviewInfoMapperTest { numberOfJoinedMembers = 1L, roomType = RoomType.Room, isHistoryWorldReadable = true, - isJoined = true, - isInvited = false, - isPublic = false, - canKnock = true, + membership = CurrentUserMembership.JOINED, + joinRule = JoinRule.Knock, ) ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryListTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryListTest.kt new file mode 100644 index 00000000000..460ef37e99e --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryListTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.roomdirectory + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomDirectorySearch +import io.element.android.libraries.matrix.test.A_ROOM_ID_2 +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.rustcomponents.sdk.RoomDirectorySearch +import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate + +@OptIn(ExperimentalCoroutinesApi::class) +class RustBaseRoomDirectoryListTest { + @Test + fun `check that the state emits the expected values`() = runTest { + val roomDirectorySearch = FakeFfiRoomDirectorySearch() + val mapper = RoomDescriptionMapper() + val sut = createRustRoomDirectoryList( + roomDirectorySearch = roomDirectorySearch, + ) + // Let the mxCallback be ready + runCurrent() + sut.state.test { + sut.filter(filter = "", batchSize = 20, viaServerName = null) + roomDirectorySearch.emitResult( + listOf( + RoomDirectorySearchEntryUpdate.Append(listOf(aRustRoomDescription())) + ) + ) + val initialItem = awaitItem() + assertThat(initialItem).isEqualTo( + RoomDirectoryList.State( + hasMoreToLoad = true, + items = listOf(mapper.map(aRustRoomDescription())) + ) + ) + assertThat(initialItem.hasMoreToLoad).isTrue() + roomDirectorySearch.isAtLastPage = true + sut.loadMore() + roomDirectorySearch.emitResult( + listOf( + RoomDirectorySearchEntryUpdate.Append(listOf(aRustRoomDescription(A_ROOM_ID_2.value))) + ) + ) + val nextItem = awaitItem() + assertThat(nextItem).isEqualTo( + RoomDirectoryList.State( + hasMoreToLoad = false, + items = listOf( + mapper.map(aRustRoomDescription()), + ) + ) + ) + val finalItem = awaitItem() + assertThat(finalItem).isEqualTo( + RoomDirectoryList.State( + hasMoreToLoad = false, + items = listOf( + mapper.map(aRustRoomDescription()), + mapper.map(aRustRoomDescription(A_ROOM_ID_2.value)), + ) + ) + ) + } + } + + private fun TestScope.createRustRoomDirectoryList( + roomDirectorySearch: RoomDirectorySearch = FakeFfiRoomDirectorySearch(), + ) = RustRoomDirectoryList( + inner = roomDirectorySearch, + coroutineScope = backgroundScope, + coroutineContext = StandardTestDispatcher(testScheduler), + ) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryServiceTest.kt new file mode 100644 index 00000000000..cc39e00c9fe --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryServiceTest.kt @@ -0,0 +1,25 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.impl.roomdirectory + +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RustBaseRoomDirectoryServiceTest { + @Test + fun test() = runTest { + val client = FakeFfiClient() + val sut = RustRoomDirectoryService( + client = client, + sessionDispatcher = StandardTestDispatcher(testScheduler), + ) + sut.createRoomDirectoryList(backgroundScope) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryListTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryListTest.kt deleted file mode 100644 index b2931ce49e5..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryListTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.roomdirectory - -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomDirectorySearch -import io.element.android.libraries.matrix.test.A_ROOM_ID_2 -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.matrix.rustcomponents.sdk.RoomDirectorySearch -import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate - -@OptIn(ExperimentalCoroutinesApi::class) -class RustRoomDirectoryListTest { - @Test - fun `check that the state emits the expected values`() = runTest { - val roomDirectorySearch = FakeRustRoomDirectorySearch() - val mapper = RoomDescriptionMapper() - val sut = createRustRoomDirectoryList( - roomDirectorySearch = roomDirectorySearch, - scope = backgroundScope, - ) - // Let the mxCallback be ready - runCurrent() - sut.state.test { - sut.filter(filter = "", batchSize = 20, viaServerName = null) - roomDirectorySearch.emitResult( - listOf( - RoomDirectorySearchEntryUpdate.Append(listOf(aRustRoomDescription())) - ) - ) - val initialItem = awaitItem() - assertThat(initialItem).isEqualTo( - RoomDirectoryList.State( - hasMoreToLoad = true, - items = listOf(mapper.map(aRustRoomDescription())) - ) - ) - assertThat(initialItem.hasMoreToLoad).isTrue() - roomDirectorySearch.isAtLastPage = true - sut.loadMore() - roomDirectorySearch.emitResult( - listOf( - RoomDirectorySearchEntryUpdate.Append(listOf(aRustRoomDescription(A_ROOM_ID_2.value))) - ) - ) - val nextItem = awaitItem() - assertThat(nextItem).isEqualTo( - RoomDirectoryList.State( - hasMoreToLoad = false, - items = listOf( - mapper.map(aRustRoomDescription()), - ) - ) - ) - val finalItem = awaitItem() - assertThat(finalItem).isEqualTo( - RoomDirectoryList.State( - hasMoreToLoad = false, - items = listOf( - mapper.map(aRustRoomDescription()), - mapper.map(aRustRoomDescription(A_ROOM_ID_2.value)), - ) - ) - ) - } - } - - private fun TestScope.createRustRoomDirectoryList( - roomDirectorySearch: RoomDirectorySearch = FakeRustRoomDirectorySearch(), - scope: CoroutineScope, - ) = RustRoomDirectoryList( - inner = roomDirectorySearch, - coroutineScope = scope, - coroutineContext = StandardTestDispatcher(testScheduler), - ) -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryServiceTest.kt deleted file mode 100644 index 76a0fac4a9a..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryServiceTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.impl.roomdirectory - -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class RustRoomDirectoryServiceTest { - @Test - fun test() = runTest { - val client = FakeRustClient() - val sut = RustRoomDirectoryService( - client = client, - sessionDispatcher = StandardTestDispatcher(testScheduler), - ) - sut.createRoomDirectoryList(backgroundScope) - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt index 683b2182612..2d97f2c5895 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt @@ -7,8 +7,8 @@ package io.element.android.libraries.matrix.impl.roomlist -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomList -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomList +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.coroutines.EmptyCoroutineContext @@ -17,14 +17,14 @@ class RoomListFactoryTest { @Test fun `createRoomList should work`() = runTest { val sut = RoomListFactory( - innerRoomListService = FakeRustRoomListService(), + innerRoomListService = FakeFfiRoomListService(), sessionCoroutineScope = backgroundScope, ) sut.createRoomList( pageSize = 10, coroutineContext = EmptyCoroutineContext, ) { - FakeRustRoomList() + FakeFfiRoomList() } } } 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 08c61a87097..2b54463deab 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 @@ -8,9 +8,10 @@ package io.element.android.libraries.matrix.impl.roomlist import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.roomlist.RoomSummary -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListItem -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoom +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService 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 @@ -30,7 +31,7 @@ class RoomSummaryListProcessorTest { summaries.value = listOf(aRoomSummary()) val processor = createProcessor() - val newEntry = FakeRustRoomListItem(A_ROOM_ID_2) + val newEntry = aRustRoom(A_ROOM_ID_2) processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(newEntry, newEntry, newEntry)))) assertThat(summaries.value.count()).isEqualTo(4) @@ -41,7 +42,7 @@ class RoomSummaryListProcessorTest { fun `PushBack adds a new entry at the end of the list`() = runTest { summaries.value = listOf(aRoomSummary()) val processor = createProcessor() - processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2)))) + processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(aRustRoom(A_ROOM_ID_2)))) assertThat(summaries.value.count()).isEqualTo(2) assertThat(summaries.value.last().roomId).isEqualTo(A_ROOM_ID_2) @@ -51,7 +52,7 @@ class RoomSummaryListProcessorTest { fun `PushFront inserts a new entry at the start of the list`() = runTest { summaries.value = listOf(aRoomSummary()) val processor = createProcessor() - processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2)))) + processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(aRustRoom(A_ROOM_ID_2)))) assertThat(summaries.value.count()).isEqualTo(2) assertThat(summaries.value.first().roomId).isEqualTo(A_ROOM_ID_2) @@ -63,7 +64,7 @@ class RoomSummaryListProcessorTest { val processor = createProcessor() val index = 0 - processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2)))) + processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), aRustRoom(A_ROOM_ID_2)))) assertThat(summaries.value.count()).isEqualTo(1) assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2) @@ -75,7 +76,7 @@ class RoomSummaryListProcessorTest { val processor = createProcessor() val index = 0 - processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2)))) + processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), aRustRoom(A_ROOM_ID_2)))) assertThat(summaries.value.count()).isEqualTo(2) assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2) @@ -163,15 +164,20 @@ class RoomSummaryListProcessorTest { val processor = createProcessor() val index = 0 - processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(FakeRustRoomListItem(A_ROOM_ID_3))))) + processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(aRustRoom(A_ROOM_ID_3))))) assertThat(summaries.value.count()).isEqualTo(1) assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3) } + private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeFfiRoom( + roomId = roomId, + latestEventLambda = { null }, + ) + private fun TestScope.createProcessor() = RoomSummaryListProcessor( summaries, - FakeRustRoomListService(), + FakeFfiRoomListService(), coroutineContext = StandardTestDispatcher(testScheduler), roomSummaryDetailsFactory = RoomSummaryFactory(), ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustBaseRoomListServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustBaseRoomListServiceTest.kt new file mode 100644 index 00000000000..561c1f245f4 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustBaseRoomListServiceTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 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. + */ +package io.element.android.libraries.matrix.impl.roomlist + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService +import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator +import org.matrix.rustcomponents.sdk.RoomListService as RustRoomListService + +@OptIn(ExperimentalCoroutinesApi::class) +class RustBaseRoomListServiceTest { + @Test + fun `syncIndicator should emit the expected values`() = runTest { + val roomListService = FakeFfiRoomListService() + val sut = createRustRoomListService( + roomListService = roomListService, + ) + // Give time for mxCallback to setup + runCurrent() + sut.syncIndicator.test { + assertThat(awaitItem()).isEqualTo(RoomListService.SyncIndicator.Hide) + roomListService.emitRoomListServiceSyncIndicator(RoomListServiceSyncIndicator.SHOW) + assertThat(awaitItem()).isEqualTo(RoomListService.SyncIndicator.Show) + roomListService.emitRoomListServiceSyncIndicator(RoomListServiceSyncIndicator.HIDE) + assertThat(awaitItem()).isEqualTo(RoomListService.SyncIndicator.Hide) + } + } +} + +private fun TestScope.createRustRoomListService( + roomListService: RustRoomListService = FakeFfiRoomListService(), +) = RustRoomListService( + innerRoomListService = roomListService, + sessionDispatcher = StandardTestDispatcher(testScheduler), + roomListFactory = RoomListFactory( + innerRoomListService = roomListService, + sessionCoroutineScope = backgroundScope, + ), + roomSyncSubscriber = RoomSyncSubscriber( + roomListService = roomListService, + dispatchers = testCoroutineDispatchers(), + ), + sessionCoroutineScope = backgroundScope, +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListServiceTest.kt deleted file mode 100644 index 8279f86483e..00000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListServiceTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 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. - */ -package io.element.android.libraries.matrix.impl.roomlist - -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.roomlist.RoomListService -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService -import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber -import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator -import org.matrix.rustcomponents.sdk.RoomListService as RustRoomListService - -@OptIn(ExperimentalCoroutinesApi::class) -class RustRoomListServiceTest { - @Test - fun `syncIndicator should emit the expected values`() = runTest { - val roomListService = FakeRustRoomListService() - val sut = createRustRoomListService( - sessionCoroutineScope = backgroundScope, - roomListService = roomListService, - ) - // Give time for mxCallback to setup - runCurrent() - sut.syncIndicator.test { - assertThat(awaitItem()).isEqualTo(RoomListService.SyncIndicator.Hide) - roomListService.emitRoomListServiceSyncIndicator(RoomListServiceSyncIndicator.SHOW) - assertThat(awaitItem()).isEqualTo(RoomListService.SyncIndicator.Show) - roomListService.emitRoomListServiceSyncIndicator(RoomListServiceSyncIndicator.HIDE) - assertThat(awaitItem()).isEqualTo(RoomListService.SyncIndicator.Hide) - } - } -} - -private fun TestScope.createRustRoomListService( - sessionCoroutineScope: CoroutineScope, - roomListService: RustRoomListService = FakeRustRoomListService(), -) = RustRoomListService( - innerRoomListService = roomListService, - sessionDispatcher = StandardTestDispatcher(testScheduler), - roomListFactory = RoomListFactory( - innerRoomListService = roomListService, - sessionCoroutineScope = sessionCoroutineScope, - ), - roomSyncSubscriber = RoomSyncSubscriber( - roomListService = roomListService, - dispatchers = testCoroutineDispatchers(), - ), - sessionCoroutineScope = sessionCoroutineScope, -) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt index 72f98db0084..a4029417cf0 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.timeline import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineDiff 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 @@ -33,7 +33,7 @@ class MatrixTimelineDiffProcessorTest { fun `Append adds new entries at the end of the list`() = runTest { timelineItems.value = listOf(anEvent) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.APPEND))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.APPEND))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( anEvent, @@ -45,7 +45,7 @@ class MatrixTimelineDiffProcessorTest { fun `PushBack adds a new entry at the end of the list`() = runTest { timelineItems.value = listOf(anEvent) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_BACK))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.PUSH_BACK))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( anEvent, @@ -57,7 +57,7 @@ class MatrixTimelineDiffProcessorTest { fun `PushFront inserts a new entry at the start of the list`() = runTest { timelineItems.value = listOf(anEvent) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_FRONT))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.PUSH_FRONT))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( MatrixTimelineItem.Other, @@ -69,7 +69,7 @@ class MatrixTimelineDiffProcessorTest { fun `Set replaces an entry at some index`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.SET))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.SET))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( anEvent, @@ -81,7 +81,7 @@ class MatrixTimelineDiffProcessorTest { fun `Insert inserts a new entry at the provided index`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.INSERT))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.INSERT))) assertThat(timelineItems.value.count()).isEqualTo(3) assertThat(timelineItems.value).containsExactly( anEvent, @@ -94,7 +94,7 @@ class MatrixTimelineDiffProcessorTest { fun `Remove removes an entry at some index`() = runTest { timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.REMOVE))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.REMOVE))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( anEvent, @@ -106,7 +106,7 @@ class MatrixTimelineDiffProcessorTest { fun `PopBack removes an entry at the end of the list`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_BACK))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.POP_BACK))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( anEvent, @@ -117,7 +117,7 @@ class MatrixTimelineDiffProcessorTest { fun `PopFront removes an entry at the start of the list`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_FRONT))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.POP_FRONT))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( anEvent2, @@ -128,7 +128,7 @@ class MatrixTimelineDiffProcessorTest { fun `Clear removes all the entries`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.CLEAR))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.CLEAR))) assertThat(timelineItems.value).isEmpty() } @@ -136,7 +136,7 @@ class MatrixTimelineDiffProcessorTest { fun `Truncate removes all entries after the provided length`() = runTest { timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.TRUNCATE))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.TRUNCATE))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( anEvent, @@ -147,7 +147,7 @@ class MatrixTimelineDiffProcessorTest { fun `Reset removes all entries and add the provided ones`() = runTest { timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.RESET))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.RESET))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( MatrixTimelineItem.Other, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt index 8e9120a8399..8fcb352075c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt @@ -12,15 +12,15 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimeline -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimeline +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineDiff import io.element.android.libraries.matrix.impl.room.RoomContentForwarder -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP @@ -34,13 +34,13 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.rustcomponents.sdk.TimelineChange -import uniffi.matrix_sdk_ui.LiveBackPaginationStatus +import uniffi.matrix_sdk.RoomPaginationStatus import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline class RustTimelineTest { @Test fun `ensure that the timeline emits new loading item when pagination does not bring new events`() = runTest { - val inner = FakeRustTimeline() + val inner = FakeFfiTimeline() val systemClock = FakeSystemClock() val sut = createRustTimeline( inner = inner, @@ -51,17 +51,12 @@ class RustTimelineTest { runCurrent() inner.emitDiff( listOf( - FakeRustTimelineDiff( + FakeFfiTimelineDiff( item = null, change = TimelineChange.RESET, ) ) ) - with(awaitItem()) { - assertThat(size).isEqualTo(1) - // Typing notification - assertThat((get(0) as MatrixTimelineItem.Virtual).virtual).isEqualTo(VirtualTimelineItem.TypingNotification) - } with(awaitItem()) { assertThat(size).isEqualTo(2) // The loading @@ -78,10 +73,10 @@ class RustTimelineTest { // Start pagination sut.paginate(Timeline.PaginationDirection.BACKWARDS) // Simulate SDK starting pagination - inner.emitPaginationStatus(LiveBackPaginationStatus.Paginating) + inner.emitPaginationStatus(RoomPaginationStatus.Paginating) // No new events received // Simulate SDK stopping pagination, more event to load - inner.emitPaginationStatus(LiveBackPaginationStatus.Idle(hitStartOfTimeline = false)) + inner.emitPaginationStatus(RoomPaginationStatus.Idle(hitTimelineStart = false)) // expect an item to be emitted, with an updated timestamp with(awaitItem()) { assertThat(size).isEqualTo(2) @@ -103,10 +98,10 @@ private fun TestScope.createRustTimeline( inner: InnerTimeline, mode: Timeline.Mode = Timeline.Mode.LIVE, systemClock: SystemClock = FakeSystemClock(), - matrixRoom: MatrixRoom = FakeMatrixRoom().apply { givenRoomInfo(aRoomInfo()) }, + joinedRoom: JoinedRoom = FakeJoinedRoom().apply { givenRoomInfo(aRoomInfo()) }, coroutineScope: CoroutineScope = backgroundScope, dispatcher: CoroutineDispatcher = testCoroutineDispatchers().io, - roomContentForwarder: RoomContentForwarder = RoomContentForwarder(FakeRustRoomListService()), + roomContentForwarder: RoomContentForwarder = RoomContentForwarder(FakeFfiRoomListService()), featureFlagsService: FeatureFlagService = FakeFeatureFlagService(), onNewSyncedEvent: () -> Unit = {}, ): RustTimeline { @@ -114,7 +109,7 @@ private fun TestScope.createRustTimeline( inner = inner, mode = mode, systemClock = systemClock, - matrixRoom = matrixRoom, + joinedRoom = joinedRoom, coroutineScope = coroutineScope, dispatcher = dispatcher, roomContentForwarder = roomContentForwarder, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt index ac15faea77b..03c1567c952 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt @@ -11,16 +11,13 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.impl.fixtures.factories.aRustEventTimelineItem -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimeline -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineItem +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimeline +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineDiff +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineItem import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -36,9 +33,8 @@ class TimelineItemsSubscriberTest { fun `when timeline emits an empty list of items, the flow must emits an empty list`() = runTest { val timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) - val timeline = FakeRustTimeline() + val timeline = FakeFfiTimeline() val timelineItemsSubscriber = createTimelineItemsSubscriber( - coroutineScope = backgroundScope, timeline = timeline, timelineItems = timelineItems, ) @@ -46,7 +42,7 @@ class TimelineItemsSubscriberTest { timelineItemsSubscriber.subscribeIfNeeded() // Wait for the listener to be set. runCurrent() - timeline.emitDiff(listOf(FakeRustTimelineDiff(item = null, change = TimelineChange.RESET))) + timeline.emitDiff(listOf(FakeFfiTimelineDiff(item = null, change = TimelineChange.RESET))) val final = awaitItem() assertThat(final).isEmpty() timelineItemsSubscriber.unsubscribeIfNeeded() @@ -57,9 +53,8 @@ class TimelineItemsSubscriberTest { fun `when timeline emits a non empty list of items, the flow must emits a non empty list`() = runTest { val timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) - val timeline = FakeRustTimeline() + val timeline = FakeFfiTimeline() val timelineItemsSubscriber = createTimelineItemsSubscriber( - coroutineScope = backgroundScope, timeline = timeline, timelineItems = timelineItems, ) @@ -67,7 +62,7 @@ class TimelineItemsSubscriberTest { timelineItemsSubscriber.subscribeIfNeeded() // Wait for the listener to be set. runCurrent() - timeline.emitDiff(listOf(FakeRustTimelineDiff(item = FakeRustTimelineItem(), change = TimelineChange.RESET))) + timeline.emitDiff(listOf(FakeFfiTimelineDiff(item = FakeFfiTimelineItem(), change = TimelineChange.RESET))) val final = awaitItem() assertThat(final).isNotEmpty() timelineItemsSubscriber.unsubscribeIfNeeded() @@ -78,10 +73,9 @@ class TimelineItemsSubscriberTest { fun `when timeline emits an item with SYNC origin, the callback onNewSyncedEvent is invoked`() = runTest { val timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) - val timeline = FakeRustTimeline() + val timeline = FakeFfiTimeline() val onNewSyncedEventRecorder = lambdaRecorder { } val timelineItemsSubscriber = createTimelineItemsSubscriber( - coroutineScope = backgroundScope, timeline = timeline, timelineItems = timelineItems, onNewSyncedEvent = onNewSyncedEventRecorder, @@ -92,8 +86,8 @@ class TimelineItemsSubscriberTest { runCurrent() timeline.emitDiff( listOf( - FakeRustTimelineDiff( - item = FakeRustTimelineItem( + FakeFfiTimelineDiff( + item = FakeFfiTimelineItem( asEventResult = aRustEventTimelineItem(origin = EventItemOrigin.SYNC), ), change = TimelineChange.RESET, @@ -109,9 +103,7 @@ class TimelineItemsSubscriberTest { @Test fun `multiple subscriptions does not have side effect`() = runTest { - val timelineItemsSubscriber = createTimelineItemsSubscriber( - coroutineScope = backgroundScope, - ) + val timelineItemsSubscriber = createTimelineItemsSubscriber() timelineItemsSubscriber.subscribeIfNeeded() timelineItemsSubscriber.subscribeIfNeeded() timelineItemsSubscriber.unsubscribeIfNeeded() @@ -120,20 +112,15 @@ class TimelineItemsSubscriberTest { } private fun TestScope.createTimelineItemsSubscriber( - coroutineScope: CoroutineScope, - timeline: Timeline = FakeRustTimeline(), + timeline: Timeline = FakeFfiTimeline(), timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE), - initLatch: CompletableDeferred = CompletableDeferred(), - isTimelineInitialized: MutableStateFlow = MutableStateFlow(false), onNewSyncedEvent: () -> Unit = { lambdaError() }, ): TimelineItemsSubscriber { return TimelineItemsSubscriber( - timelineCoroutineScope = coroutineScope, + timelineCoroutineScope = backgroundScope, dispatcher = StandardTestDispatcher(testScheduler), timeline = timeline, timelineDiffProcessor = createMatrixTimelineDiffProcessor(timelineItems), - initLatch = initLatch, - isTimelineInitialized = isTimelineInitialized, onNewSyncedEvent = onNewSyncedEvent, ) } 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 00e93e0e667..63deb34530b 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 @@ -11,25 +11,29 @@ 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.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.OtherState -import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem 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.anEventTimelineItem +import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent +internal val timelineStartEvent = MatrixTimelineItem.Virtual( + uniqueId = UniqueId("timeline_start"), + virtual = VirtualTimelineItem.RoomBeginning, +) internal val roomCreateEvent = MatrixTimelineItem.Event( uniqueId = UniqueId("m.room.create"), event = anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate)) ) internal val roomCreatorJoinEvent = MatrixTimelineItem.Event( uniqueId = UniqueId("m.room.member"), - event = anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED)) + event = anEventTimelineItem(content = aRoomMembershipContent(userId = A_USER_ID, change = MembershipChange.JOINED)) ) internal val otherMemberJoinEvent = MatrixTimelineItem.Event( uniqueId = UniqueId("m.room.member_other"), - event = anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED)) + event = anEventTimelineItem(content = aRoomMembershipContent(userId = A_USER_ID_2, change = MembershipChange.JOINED)) ) internal val messageEvent = MatrixTimelineItem.Event( uniqueId = UniqueId("m.room.message"), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessorTest.kt index aaadf74aeae..c5b1c3edb7f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessorTest.kt @@ -18,21 +18,14 @@ class LastForwardIndicatorsPostProcessorTest { @Test fun `LastForwardIndicatorsPostProcessor does not alter the items with mode not FOCUSED_ON_EVENT`() { val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.LIVE) - val result = sut.process(listOf(messageEvent), true) - assertThat(result).containsExactly(messageEvent) - } - - @Test - fun `LastForwardIndicatorsPostProcessor does not alter the items with mode FOCUSED_ON_EVENT but timeline not initialized`() { - val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) - val result = sut.process(listOf(messageEvent), false) + val result = sut.process(listOf(messageEvent)) assertThat(result).containsExactly(messageEvent) } @Test fun `LastForwardIndicatorsPostProcessor add virtual items`() { val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) - val result = sut.process(listOf(messageEvent), true) + val result = sut.process(listOf(messageEvent)) assertThat(result).containsExactly( messageEvent, MatrixTimelineItem.Virtual( @@ -45,7 +38,7 @@ class LastForwardIndicatorsPostProcessorTest { @Test fun `LastForwardIndicatorsPostProcessor add virtual items on empty list`() { val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) - val result = sut.process(listOf(), true) + val result = sut.process(listOf()) assertThat(result).containsExactly( MatrixTimelineItem.Virtual( uniqueId = UniqueId("last_forward_indicator_fake_id"), @@ -58,9 +51,9 @@ class LastForwardIndicatorsPostProcessorTest { fun `LastForwardIndicatorsPostProcessor add virtual items but does not alter the list if called a second time`() { val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) // Process a first time - sut.process(listOf(messageEvent), true) + sut.process(listOf(messageEvent)) // Process a second time with the same Event - val result = sut.process(listOf(messageEvent), true) + val result = sut.process(listOf(messageEvent)) assertThat(result).containsExactly( messageEvent, MatrixTimelineItem.Virtual( @@ -74,9 +67,9 @@ class LastForwardIndicatorsPostProcessorTest { fun `LastForwardIndicatorsPostProcessor add virtual items each time it is called with new Events`() { val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) // Process a first time - sut.process(listOf(dayEvent, messageEvent), true) + sut.process(listOf(dayEvent, messageEvent)) // Process a second time with the same Event - val result = sut.process(listOf(dayEvent, messageEvent, messageEvent2), true) + val result = sut.process(listOf(dayEvent, messageEvent, messageEvent2)) assertThat(result).containsExactly( dayEvent, messageEvent, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessorTest.kt index b7e174e4a83..881d718392e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessorTest.kt @@ -16,25 +16,12 @@ import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import org.junit.Test class LoadingIndicatorsPostProcessorTest { - @Test - fun `LoadingIndicatorsPostProcessor does not alter the items is the timeline is not initialized`() { - val sut = LoadingIndicatorsPostProcessor(FakeSystemClock()) - val result = sut.process( - items = listOf(messageEvent, messageEvent2), - isTimelineInitialized = false, - hasMoreToLoadBackward = true, - hasMoreToLoadForward = true, - ) - assertThat(result).containsExactly(messageEvent, messageEvent2) - } - @Test fun `LoadingIndicatorsPostProcessor adds Loading indicator at the top of the list if hasMoreToLoadBackward is true`() { val clock = FakeSystemClock() val sut = LoadingIndicatorsPostProcessor(clock) val result = sut.process( items = listOf(messageEvent, messageEvent2), - isTimelineInitialized = true, hasMoreToLoadBackward = true, hasMoreToLoadForward = false, ) @@ -57,7 +44,6 @@ class LoadingIndicatorsPostProcessorTest { val sut = LoadingIndicatorsPostProcessor(clock) val result = sut.process( items = listOf(messageEvent, messageEvent2), - isTimelineInitialized = true, hasMoreToLoadBackward = false, hasMoreToLoadForward = true, ) @@ -80,7 +66,6 @@ class LoadingIndicatorsPostProcessorTest { val sut = LoadingIndicatorsPostProcessor(clock) val result = sut.process( items = listOf(messageEvent, messageEvent2), - isTimelineInitialized = true, hasMoreToLoadBackward = true, hasMoreToLoadForward = true, ) @@ -110,7 +95,6 @@ class LoadingIndicatorsPostProcessorTest { val sut = LoadingIndicatorsPostProcessor(clock) val result = sut.process( items = listOf(), - isTimelineInitialized = true, hasMoreToLoadBackward = true, hasMoreToLoadForward = true, ) 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 5d72a68a686..ba0bef42664 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 @@ -52,6 +52,7 @@ class RoomBeginningPostProcessorTest { @Test fun `processor removes room creation event and self-join event from DM timeline`() { val timelineItems = listOf( + timelineStartEvent, roomCreateEvent, roomCreatorJoinEvent, ) @@ -62,7 +63,7 @@ class RoomBeginningPostProcessorTest { roomCreator = A_USER_ID, hasMoreToLoadBackwards = false, ) - assertThat(processedItems).isEmpty() + assertThat(processedItems).containsExactly(timelineStartEvent) } @Test @@ -98,43 +99,6 @@ class RoomBeginningPostProcessorTest { assertThat(processedItems).isEqualTo(expected) } - @Test - fun `processor will add beginning of room item if it's not a DM`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = false, roomCreator = A_USER_ID, hasMoreToLoadBackwards = false) - assertThat(processedItems).isEqualTo( - listOf(processor.createRoomBeginningItem()) + timelineItems - ) - } - - @Test - fun `processor will not add beginning of room item if it's not a DM but the room has more to load`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = false, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true) - assertThat(processedItems).isEqualTo(timelineItems) - } - - @Test - fun `processor will add beginning of room item if it's not a DM, when the parameter roomCreator is null`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = false, roomCreator = null, hasMoreToLoadBackwards = false) - assertThat(processedItems).isEqualTo( - listOf(processor.createRoomBeginningItem()) + timelineItems - ) - } - @Test fun `processor removes items event it's not at the start of the timeline`() { val timelineItems = listOf( 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 6cce2e2f985..9b609b39c7e 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 @@ -18,24 +18,26 @@ 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.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.PendingRoom +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom +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.preview.RoomPreviewInfo 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.roomlist.RoomSummary import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion 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.test.encryption.FakeEncryptionService 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 import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.pushers.FakePushersService @@ -48,7 +50,6 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.simulateLongTask import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -73,20 +74,26 @@ class FakeMatrixClient( private val syncService: FakeSyncService = FakeSyncService(), private val encryptionService: FakeEncryptionService = FakeEncryptionService(), private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(), - private val accountManagementUrlString: Result = Result.success(null), + private val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(), + private val accountManagementUrlResult: (AccountManagementAction?) -> Result = { lambdaError() }, private val resolveRoomAliasResult: (RoomAlias) -> Result> = { Result.success( - Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList())) - ) + Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList())) + ) }, - private val getRoomPreviewInfoResult: (RoomIdOrAlias, List) -> Result = { _, _ -> Result.failure(AN_EXCEPTION) }, + private val getNotJoinedRoomResult: (RoomIdOrAlias, List) -> Result = { _, _ -> lambdaError() }, private val clearCacheLambda: () -> Unit = { lambdaError() }, private val userIdServerNameLambda: () -> String = { lambdaError() }, private val getUrlLambda: (String) -> Result = { lambdaError() }, private val canDeactivateAccountResult: () -> Boolean = { lambdaError() }, private val deactivateAccountResult: (String, Boolean) -> Result = { _, _ -> lambdaError() }, private val currentSlidingSyncVersionLambda: () -> Result = { lambdaError() }, - private val availableSlidingSyncVersionsLambda: () -> Result> = { lambdaError() } + private val availableSlidingSyncVersionsLambda: () -> Result> = { lambdaError() }, + private val ignoreUserResult: (UserId) -> Result = { lambdaError() }, + private var unIgnoreUserResult: (UserId) -> Result = { Result.success(Unit) }, + private val canReportRoomLambda: () -> Boolean = { false }, + private val isLivekitRtcSupportedLambda: () -> Boolean = { false }, + override val ignoredUsersFlow: StateFlow> = MutableStateFlow(persistentListOf()), ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -97,55 +104,49 @@ class FakeMatrixClient( private val _userProfile: MutableStateFlow = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl)) override val userProfile: StateFlow = _userProfile - override val ignoredUsersFlow: MutableStateFlow> = MutableStateFlow(persistentListOf()) - private var ignoreUserResult: Result = Result.success(Unit) - private var unignoreUserResult: Result = Result.success(Unit) private var createRoomResult: Result = Result.success(A_ROOM_ID) private var createDmResult: Result = Result.success(A_ROOM_ID) - private var findDmResult: RoomId? = A_ROOM_ID - private val getRoomResults = mutableMapOf() - val getPendingRoomResults = mutableMapOf() + private var findDmResult: Result = Result.success(A_ROOM_ID) + private val getRoomResults = mutableMapOf() private val searchUserResults = mutableMapOf>() private val getProfileResults = mutableMapOf>() private var uploadMediaResult: Result = Result.success(AN_AVATAR_URL) private var setDisplayNameResult: Result = Result.success(Unit) private var uploadAvatarResult: Result = Result.success(Unit) private var removeAvatarResult: Result = Result.success(Unit) - var joinRoomLambda: (RoomId) -> Result = { + var joinRoomLambda: (RoomId) -> Result = { Result.success(null) } - var joinRoomByIdOrAliasLambda: (RoomIdOrAlias, List) -> Result = { _, _ -> + var joinRoomByIdOrAliasLambda: (RoomIdOrAlias, List) -> Result = { _, _ -> Result.success(null) } - var knockRoomLambda: (RoomIdOrAlias, String, List) -> Result = { _, _, _ -> + var knockRoomLambda: (RoomIdOrAlias, String, List) -> Result = { _, _, _ -> Result.success(null) } - var getRoomSummaryFlowLambda = { _: RoomIdOrAlias -> - flowOf>(Optional.empty()) - } - var logoutLambda: (Boolean, Boolean) -> String? = { _, _ -> - null + var getRoomInfoFlowLambda = { _: RoomId -> + flowOf>(Optional.empty()) } + var logoutLambda: (Boolean, Boolean) -> Unit = { _, _ -> } - override suspend fun getRoom(roomId: RoomId): MatrixRoom? { + override suspend fun getRoom(roomId: RoomId): BaseRoom? { return getRoomResults[roomId] } - override suspend fun getPendingRoom(roomId: RoomId): PendingRoom? { - return getPendingRoomResults[roomId] + override suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? { + return getRoomResults[roomId] as? JoinedRoom } - override suspend fun findDM(userId: UserId): RoomId? { + override suspend fun findDM(userId: UserId): Result { return findDmResult } override suspend fun ignoreUser(userId: UserId): Result = simulateLongTask { - return ignoreUserResult + return ignoreUserResult(userId) } override suspend fun unignoreUser(userId: UserId): Result = simulateLongTask { - return unignoreUserResult + return unIgnoreUserResult(userId) } override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result = simulateLongTask { @@ -172,12 +173,12 @@ class FakeMatrixClient( return 0 } - override suspend fun clearCache() { + override suspend fun clearCache() = simulateLongTask { clearCacheLambda() } - override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? = simulateLongTask { - return logoutLambda(ignoreSdkError, userInitiated) + override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) = simulateLongTask { + logoutLambda(ignoreSdkError, userInitiated) } override fun canDeactivateAccount() = canDeactivateAccountResult() @@ -186,16 +187,14 @@ class FakeMatrixClient( deactivateAccountResult(password, eraseData) } - override fun close() = Unit - override suspend fun getUserProfile(): Result = simulateLongTask { val result = getProfileResults[sessionId]?.getOrNull() ?: MatrixUser(sessionId, userDisplayName, userAvatarUrl) _userProfile.tryEmit(result) return Result.success(result) } - override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result { - return accountManagementUrlString + override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result = simulateLongTask { + accountManagementUrlResult(action) } override suspend fun uploadMedia( @@ -221,13 +220,13 @@ class FakeMatrixClient( return removeAvatarResult } - override suspend fun joinRoom(roomId: RoomId): Result = joinRoomLambda(roomId) + override suspend fun joinRoom(roomId: RoomId): Result = joinRoomLambda(roomId) - override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result { + override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result { return joinRoomByIdOrAliasLambda(roomIdOrAlias, serverNames) } - override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result { + override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result { return knockRoomLambda(roomIdOrAlias, message, serverNames) } @@ -238,15 +237,12 @@ class FakeMatrixClient( override fun notificationService(): NotificationService = notificationService override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService override fun encryptionService(): EncryptionService = encryptionService + override fun mediaPreviewService(): MediaPreviewService = mediaPreviewService override fun roomMembershipObserver(): RoomMembershipObserver { return RoomMembershipObserver() } - suspend fun emitIgnoreUserList(users: List) { - ignoredUsersFlow.emit(users.toImmutableList()) - } - // Mocks fun givenCreateRoomResult(result: Result) { @@ -257,19 +253,11 @@ class FakeMatrixClient( createDmResult = result } - fun givenIgnoreUserResult(result: Result) { - ignoreUserResult = result - } - - fun givenUnignoreUserResult(result: Result) { - unignoreUserResult = result - } - - fun givenFindDmResult(result: RoomId?) { + fun givenFindDmResult(result: Result) { findDmResult = result } - fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom?) { + fun givenGetRoomResult(roomId: RoomId, result: BaseRoom?) { if (result == null) { getRoomResults.remove(roomId) } else { @@ -313,15 +301,15 @@ class FakeMatrixClient( resolveRoomAliasResult(roomAlias) } - override suspend fun getRoomPreviewInfo(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result = simulateLongTask { - getRoomPreviewInfoResult(roomIdOrAlias, serverNames) + override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result = simulateLongTask { + getNotJoinedRoomResult(roomIdOrAlias, serverNames) } override suspend fun getRecentlyVisitedRooms(): Result> { return Result.success(visitedRoomsId) } - override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias) = getRoomSummaryFlowLambda(roomIdOrAlias) + override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId) var setAllSendQueuesEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean -> // no-op @@ -347,4 +335,12 @@ class FakeMatrixClient( override suspend fun availableSlidingSyncVersions(): Result> { return availableSlidingSyncVersionsLambda() } + + override suspend fun canReportRoom(): Boolean { + return canReportRoomLambda() + } + + override suspend fun isLivekitRtcSupported(): Boolean { + return isLivekitRtcSupportedLambda() + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 509fac833c3..87b8a348dcd 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -19,7 +19,6 @@ import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.api.room.RoomNotificationSettings const val A_USER_NAME = "alice" const val A_USER_NAME_2 = "Bob" @@ -63,21 +62,26 @@ const val A_MESSAGE = "Hello world!" const val A_REPLY = "OK, I'll be there!" const val ANOTHER_MESSAGE = "Hello universe!" const val A_CAPTION = "A media caption" +const val A_REASON = "A reason" const val A_REDACTION_REASON = "A redaction reason" const val A_HOMESERVER_URL = "matrix.org" const val A_HOMESERVER_URL_2 = "matrix-client.org" +const val AN_ACCOUNT_PROVIDER = "matrix.org" +const val AN_ACCOUNT_PROVIDER_2 = "element.io" +const val AN_ACCOUNT_PROVIDER_3 = "other.io" + val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false) val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true) val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE -val A_ROOM_NOTIFICATION_SETTINGS = RoomNotificationSettings(mode = A_ROOM_NOTIFICATION_MODE, isDefault = false) const val AN_AVATAR_URL = "mxc://data" const val A_FAILURE_REASON = "There has been a failure" +@Suppress("unused") val A_THROWABLE = Throwable(A_FAILURE_REASON) val AN_EXCEPTION = Exception(A_FAILURE_REASON) @@ -86,3 +90,6 @@ const val A_RECOVERY_KEY = "1234 5678" val A_SERVER_LIST = listOf("server1", "server2") const val A_TIMESTAMP = 567L +const val A_FORMATTED_DATE = "April 6, 1980 at 6:35 PM" + +const val A_LOGIN_HINT = "mxid:@alice:example.org" 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 3de9da37a49..f236c37da81 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 @@ -87,7 +87,10 @@ class FakeMatrixAuthenticationService( return importCreatedSessionLambda(externalSession) } - override suspend fun getOidcUrl(prompt: OidcPrompt): Result = simulateLongTask { + override suspend fun getOidcUrl( + prompt: OidcPrompt, + loginHint: String?, + ): Result = simulateLongTask { oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOidcRedirectUrlProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOidcRedirectUrlProvider.kt new file mode 100644 index 00000000000..e8226009d46 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOidcRedirectUrlProvider.kt @@ -0,0 +1,18 @@ +/* + * 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.matrix.test.auth + +import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider + +const val FAKE_REDIRECT_URL = "io.element.android:/" + +class FakeOidcRedirectUrlProvider( + private val provideResult: String = FAKE_REDIRECT_URL, +) : OidcRedirectUrlProvider { + override fun provide() = provideResult +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/qrlogin/FakeMatrixQrCodeLoginDataFactory.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/qrlogin/FakeMatrixQrCodeLoginDataFactory.kt index bb12c0120d7..344b5675898 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/qrlogin/FakeMatrixQrCodeLoginDataFactory.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/qrlogin/FakeMatrixQrCodeLoginDataFactory.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.test.auth.qrlogin import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder class FakeMatrixQrCodeLoginDataFactory( @@ -20,4 +21,8 @@ class FakeMatrixQrCodeLoginDataFactory( } } -class FakeMatrixQrCodeLoginData : MatrixQrCodeLoginData +class FakeMatrixQrCodeLoginData( + private val serverNameResult: () -> String? = { lambdaError() }, +) : MatrixQrCodeLoginData { + override fun serverName() = serverNameResult() +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/core/BuildMeta.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/core/BuildMeta.kt index dfc3163da8f..73fd83889b8 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/core/BuildMeta.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/core/BuildMeta.kt @@ -39,5 +39,5 @@ fun aBuildMeta( gitRevision = gitRevision, gitBranchName = gitBranchName, flavorDescription = flavorDescription, - flavorShortDescription = flavorShortDescription + flavorShortDescription = flavorShortDescription, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index ee126e2f92d..25e8a535b06 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow @@ -24,6 +25,8 @@ class FakeEncryptionService( var startIdentityResetLambda: () -> Result = { lambdaError() }, private val pinUserIdentityResult: (UserId) -> Result = { lambdaError() }, private val isUserVerifiedResult: (UserId) -> Result = { lambdaError() }, + private val withdrawVerificationResult: (UserId) -> Result = { lambdaError() }, + private val getUserIdentityResult: (UserId) -> Result = { lambdaError() } ) : EncryptionService { private var disableRecoveryFailure: Exception? = null override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) @@ -124,10 +127,18 @@ class FakeEncryptionService( return pinUserIdentityResult(userId) } + override suspend fun withdrawVerification(userId: UserId): Result { + return withdrawVerificationResult(userId) + } + override suspend fun isUserVerified(userId: UserId): Result = simulateLongTask { isUserVerifiedResult(userId) } + override suspend fun getUserIdentity(userId: UserId): Result = simulateLongTask { + return getUserIdentityResult(userId) + } + companion object { const val FAKE_RECOVERY_KEY = "fake" } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt new file mode 100644 index 00000000000..d8e91148174 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt @@ -0,0 +1,35 @@ +/* + * 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.matrix.test.media + +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewService +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +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 + +class FakeMediaPreviewService( + override val mediaPreviewConfigFlow: StateFlow = MutableStateFlow(MediaPreviewConfig.DEFAULT), + private val fetchMediaPreviewConfigResult: () -> Result = { lambdaError() }, + private val setMediaPreviewValueResult: (MediaPreviewValue) -> Result = { lambdaError() }, + private val setHideInviteAvatarsResult: (Boolean) -> Result = { lambdaError() }, +) : MediaPreviewService { + override suspend fun fetchMediaPreviewConfig(): Result = simulateLongTask { + fetchMediaPreviewConfigResult() + } + + override suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result = simulateLongTask { + setMediaPreviewValueResult(mediaPreviewValue) + } + + override suspend fun setHideInviteAvatars(hide: Boolean): Result = simulateLongTask { + setHideInviteAvatarsResult(hide) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt index 38a8c79e175..4a9671f6773 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt @@ -13,16 +13,13 @@ import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationService class FakeNotificationService : NotificationService { - private var getNotificationResult: Result = Result.success(null) + private var getNotificationsResult: Result> = Result.success(emptyMap()) - fun givenGetNotificationResult(result: Result) { - getNotificationResult = result + fun givenGetNotificationsResult(result: Result>) { + getNotificationsResult = result } - override suspend fun getNotification( - roomId: RoomId, - eventId: EventId, - ): Result { - return getNotificationResult + override suspend fun getNotifications(ids: Map>): Result> { + return getNotificationsResult } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt index f99ff0e3554..2bfd54954b7 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt @@ -7,11 +7,13 @@ package io.element.android.libraries.matrix.test.notification +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.A_USER_NAME_2 @@ -19,13 +21,16 @@ fun aNotificationData( content: NotificationContent = NotificationContent.MessageLike.RoomEncrypted, isDirect: Boolean = false, hasMention: Boolean = false, + threadId: ThreadId? = null, timestamp: Long = A_TIMESTAMP, senderDisplayName: String? = A_USER_NAME_2, senderIsNameAmbiguous: Boolean = false, roomDisplayName: String? = A_ROOM_NAME ): NotificationData { return NotificationData( + sessionId = A_SESSION_ID, eventId = AN_EVENT_ID, + threadId = threadId, roomId = A_ROOM_ID, senderAvatarUrl = null, senderDisplayName = senderDisplayName, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt index 487b898db3b..c00ab8f1134 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt @@ -12,13 +12,13 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.tests.testutils.lambda.lambdaError class FakePermalinkParser( - private var result: () -> PermalinkData = { lambdaError() } + private var result: (String) -> PermalinkData = { lambdaError() } ) : PermalinkParser { fun givenResult(result: PermalinkData) { this.result = { result } } override fun parse(uriString: String): PermalinkData { - return result() + return result(uriString) } } 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 new file mode 100644 index 00000000000..872481a8600 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -0,0 +1,237 @@ +/* + * 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. + */ + +package io.element.android.libraries.matrix.test.room + +import io.element.android.libraries.core.bool.orFalse +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.core.ThreadId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.RoomInfo +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.StateEventType +import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.test.TestScope + +class FakeBaseRoom( + override val sessionId: SessionId = A_SESSION_ID, + override val roomId: RoomId = A_ROOM_ID, + initialRoomInfo: RoomInfo = aRoomInfo(), + override val roomCoroutineScope: CoroutineScope = TestScope(), + private var roomPermalinkResult: () -> Result = { lambdaError() }, + private var eventPermalinkResult: (EventId) -> Result = { lambdaError() }, + private val userDisplayNameResult: (UserId) -> Result = { lambdaError() }, + private val userAvatarUrlResult: () -> Result = { lambdaError() }, + private val userRoleResult: () -> Result = { lambdaError() }, + private val getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, + private val joinRoomResult: () -> Result = { lambdaError() }, + private val canInviteResult: (UserId) -> Result = { lambdaError() }, + private val canKickResult: (UserId) -> Result = { lambdaError() }, + private val canBanResult: (UserId) -> Result = { lambdaError() }, + private val canRedactOwnResult: (UserId) -> Result = { lambdaError() }, + private val canRedactOtherResult: (UserId) -> Result = { lambdaError() }, + private val canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, + private val canUserSendMessageResult: (UserId, MessageEventType) -> Result = { _, _ -> lambdaError() }, + private val canUserTriggerRoomNotificationResult: (UserId) -> Result = { lambdaError() }, + private val canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, + private val canUserPinUnpinResult: (UserId) -> Result = { lambdaError() }, + private val setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, + private val markAsReadResult: (ReceiptType) -> Result = { Result.success(Unit) }, + private val powerLevelsResult: () -> Result = { lambdaError() }, + private val leaveRoomLambda: () -> Result = { lambdaError() }, + private val updateMembersResult: () -> Unit = { lambdaError() }, + private val getMembersResult: (Int) -> Result> = { lambdaError() }, + private val saveComposerDraftLambda: (ComposerDraft) -> Result = { _: ComposerDraft -> Result.success(Unit) }, + private val loadComposerDraftLambda: () -> Result = { Result.success(null) }, + private val clearComposerDraftLambda: () -> Result = { Result.success(Unit) }, + private val subscribeToSyncLambda: () -> Unit = { lambdaError() }, + private val getRoomVisibilityResult: () -> Result = { lambdaError() }, + private val forgetResult: () -> Result = { lambdaError() }, + private val reportRoomResult: (String?) -> Result = { lambdaError() }, + private val predecessorRoomResult: () -> PredecessorRoom? = { null }, +) : BaseRoom { + private val _roomInfoFlow: MutableStateFlow = MutableStateFlow(initialRoomInfo) + override val roomInfoFlow: StateFlow = _roomInfoFlow + + fun givenRoomInfo(roomInfo: RoomInfo) { + _roomInfoFlow.tryEmit(roomInfo) + } + + override val membersStateFlow: MutableStateFlow = MutableStateFlow(RoomMembersState.Unknown) + + override suspend fun updateMembers() = updateMembersResult() + + override suspend fun getUpdatedMember(userId: UserId): Result { + return getUpdatedMemberResult(userId) + } + + override suspend fun getMembers(limit: Int): Result> { + return getMembersResult(limit) + } + + override suspend fun subscribeToSync() { + subscribeToSyncLambda() + } + + override suspend fun powerLevels(): Result { + return powerLevelsResult() + } + + private var isDestroyed = false + + override fun destroy() { + isDestroyed = true + } + + fun assertDestroyed() { + check(isDestroyed) { "Room should be destroyed" } + } + + override suspend fun userDisplayName(userId: UserId): Result = simulateLongTask { + userDisplayNameResult(userId) + } + + override suspend fun userAvatarUrl(userId: UserId): Result = simulateLongTask { + userAvatarUrlResult() + } + + override suspend fun userRole(userId: UserId): Result { + return userRoleResult() + } + + override suspend fun getPermalink(): Result { + return roomPermalinkResult() + } + + override suspend fun getPermalinkFor(eventId: EventId): Result { + return eventPermalinkResult(eventId) + } + + override suspend fun getRoomVisibility(): Result = simulateLongTask { + getRoomVisibilityResult() + } + + override suspend fun leave(): Result = simulateLongTask { + return leaveRoomLambda() + } + + override suspend fun join(): Result { + return joinRoomResult() + } + + override suspend fun forget(): Result { + return forgetResult() + } + + override suspend fun canUserBan(userId: UserId): Result { + return canBanResult(userId) + } + + override suspend fun canUserKick(userId: UserId): Result { + return canKickResult(userId) + } + + override suspend fun canUserInvite(userId: UserId): Result { + return canInviteResult(userId) + } + + override suspend fun canUserRedactOwn(userId: UserId): Result { + return canRedactOwnResult(userId) + } + + override suspend fun canUserRedactOther(userId: UserId): Result { + return canRedactOtherResult(userId) + } + + override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result { + return canSendStateResult(userId, type) + } + + override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result { + return canUserSendMessageResult(userId, type) + } + + override suspend fun canUserTriggerRoomNotification(userId: UserId): Result { + return canUserTriggerRoomNotificationResult(userId) + } + + override suspend fun canUserJoinCall(userId: UserId): Result { + return canUserJoinCallResult(userId) + } + + override suspend fun canUserPinUnpin(userId: UserId): Result { + return canUserPinUnpinResult(userId) + } + + override suspend fun setIsFavorite(isFavorite: Boolean): Result { + return setIsFavoriteResult(isFavorite) + } + + override suspend fun markAsRead(receiptType: ReceiptType): Result { + return markAsReadResult(receiptType) + } + + var setUnreadFlagCalls = mutableListOf() + private set + + override suspend fun setUnreadFlag(isUnread: Boolean): Result { + setUnreadFlagCalls.add(isUnread) + return Result.success(Unit) + } + + override suspend fun saveComposerDraft( + composerDraft: ComposerDraft, + threadRoot: ThreadId? + ) = saveComposerDraftLambda(composerDraft) + + override suspend fun loadComposerDraft(threadRoot: ThreadId?) = loadComposerDraftLambda() + + override suspend fun clearComposerDraft(threadRoot: ThreadId?) = clearComposerDraftLambda() + + override suspend fun getUpdatedIsEncrypted(): Result = simulateLongTask { + Result.success(info().isEncrypted.orFalse()) + } + + fun givenRoomMembersState(state: RoomMembersState) { + membersStateFlow.value = state + } + + override suspend fun clearEventCacheStorage(): Result { + return Result.success(Unit) + } + + override suspend fun reportRoom(reason: String?) = reportRoomResult(reason) + + override fun predecessorRoom(): PredecessorRoom? = predecessorRoomResult() +} + +fun defaultRoomPowerLevelValues() = RoomPowerLevelsValues( + ban = 50, + invite = 0, + kick = 50, + sendEvents = 0, + redactEvents = 50, + roomName = 100, + roomAvatar = 100, + roomTopic = 100 +) 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 new file mode 100644 index 00000000000..c98c8861290 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt @@ -0,0 +1,236 @@ +/* + * 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.matrix.test.room + +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.matrix.api.core.DeviceId +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ProgressCallback +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.SendHandle +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.CreateTimelineParams +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.RoomInfo +import io.element.android.libraries.matrix.api.room.RoomMembersState +import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState +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.knock.KnockRequest +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues +import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import io.element.android.libraries.matrix.api.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.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.test.TestScope + +class FakeJoinedRoom( + val baseRoom: FakeBaseRoom = FakeBaseRoom(), + override val liveTimeline: Timeline = FakeTimeline(), + override val roomCoroutineScope: CoroutineScope = TestScope(), + override val syncUpdateFlow: StateFlow = MutableStateFlow(0), + override val roomTypingMembersFlow: Flow> = MutableStateFlow(emptyList()), + override val identityStateChangesFlow: Flow> = MutableStateFlow(emptyList()), + override val roomNotificationSettingsStateFlow: StateFlow = + MutableStateFlow(RoomNotificationSettingsState.Unknown), + override val knockRequestsFlow: Flow> = MutableStateFlow(emptyList()), + private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), + private var createTimelineResult: (CreateTimelineParams) -> Result = { lambdaError() }, + private val editMessageLambda: (EventId, String, String?, List) -> Result = { _, _, _, _ -> lambdaError() }, + private val sendCallNotificationIfNeededResult: () -> Result = { lambdaError() }, + private val progressCallbackValues: List> = emptyList(), + private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result = { _, _, _, _ -> lambdaError() }, + private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result = { lambdaError() }, + private val typingNoticeResult: (Boolean) -> Result = { lambdaError() }, + private val inviteUserResult: (UserId) -> Result = { lambdaError() }, + private val setNameResult: (String) -> Result = { lambdaError() }, + private val setTopicResult: (String) -> Result = { lambdaError() }, + private val updateAvatarResult: (String, ByteArray) -> Result = { _, _ -> lambdaError() }, + private val removeAvatarResult: () -> Result = { lambdaError() }, + private val updateUserRoleResult: (List) -> Result = { lambdaError() }, + private val updatePowerLevelsResult: (RoomPowerLevelsValues) -> Result = { lambdaError() }, + private val resetPowerLevelsResult: () -> Result = { lambdaError() }, + private val reportContentResult: (EventId, String, UserId?) -> Result = { _, _, _ -> lambdaError() }, + private val kickUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, + private val banUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, + private val unBanUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, + private val ignoreDeviceTrustAndResendResult: (Map>, SendHandle) -> Result = { _, _ -> lambdaError() }, + private val withdrawVerificationAndResendResult: (List, SendHandle) -> Result = { _, _ -> lambdaError() }, + private val updateCanonicalAliasResult: (RoomAlias?, List) -> Result = { _, _ -> lambdaError() }, + private val updateRoomVisibilityResult: (RoomVisibility) -> Result = { lambdaError() }, + private val updateRoomHistoryVisibilityResult: (RoomHistoryVisibility) -> Result = { lambdaError() }, + private val publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, + private val removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, + private val enableEncryptionResult: () -> Result = { lambdaError() }, + private val updateJoinRuleResult: (JoinRule) -> Result = { lambdaError() }, + private val setSendQueueEnabledResult: (Boolean) -> Unit = { _: Boolean -> }, +) : JoinedRoom, BaseRoom by baseRoom { + fun givenRoomMembersState(state: RoomMembersState) { + baseRoom.givenRoomMembersState(state) + } + + fun givenRoomInfo(roomInfo: RoomInfo) { + baseRoom.givenRoomInfo(roomInfo) + } + + override suspend fun createTimeline(createTimelineParams: CreateTimelineParams): Result = simulateLongTask { + createTimelineResult(createTimelineParams) + } + + override suspend fun editMessage( + eventId: EventId, + body: String, + htmlBody: String?, + intentionalMentions: List + ): Result = simulateLongTask { + editMessageLambda(eventId, body, htmlBody, intentionalMentions) + } + + override suspend fun typingNotice(isTyping: Boolean): Result = simulateLongTask { + typingNoticeResult(isTyping) + } + + override suspend fun inviteUserById(id: UserId): Result = simulateLongTask { + inviteUserResult(id) + } + + override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = simulateLongTask { + simulateSendMediaProgress(null) + updateAvatarResult(mimeType, data) + } + + override suspend fun removeAvatar(): Result = simulateLongTask { + removeAvatarResult() + } + + override suspend fun updateRoomNotificationSettings(): Result = simulateLongTask { + val notificationSettings = roomNotificationSettingsService.getRoomNotificationSettings(roomId, info().isEncrypted.orFalse(), isOneToOne).getOrThrow() + (roomNotificationSettingsStateFlow as MutableStateFlow).value = RoomNotificationSettingsState.Ready(notificationSettings) + return Result.success(Unit) + } + + override suspend fun updateCanonicalAlias(canonicalAlias: RoomAlias?, alternativeAliases: List): Result = simulateLongTask { + updateCanonicalAliasResult(canonicalAlias, alternativeAliases) + } + + override suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result = simulateLongTask { + updateRoomVisibilityResult(roomVisibility) + } + + override suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result = simulateLongTask { + updateRoomHistoryVisibilityResult(historyVisibility) + } + + override suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result = simulateLongTask { + publishRoomAliasInRoomDirectoryResult(roomAlias) + } + + override suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result = simulateLongTask { + removeRoomAliasFromRoomDirectoryResult(roomAlias) + } + + override suspend fun enableEncryption(): Result = simulateLongTask { + enableEncryptionResult().onSuccess { + baseRoom.givenRoomInfo(info().copy(isEncrypted = true)) + emitSyncUpdate() + } + } + + override suspend fun updateJoinRule(joinRule: JoinRule): Result = simulateLongTask { + updateJoinRuleResult(joinRule) + } + + override suspend fun updateUsersRoles(changes: List): Result = simulateLongTask { + updateUserRoleResult(changes) + } + + override suspend fun updatePowerLevels(roomPowerLevelsValues: RoomPowerLevelsValues): Result = simulateLongTask { + updatePowerLevelsResult(roomPowerLevelsValues) + } + + override suspend fun resetPowerLevels(): Result = simulateLongTask { + resetPowerLevelsResult() + } + + override suspend fun setName(name: String): Result = simulateLongTask { + setNameResult(name) + } + + override suspend fun setTopic(topic: String): Result = simulateLongTask { + setTopicResult(topic) + } + + override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result = simulateLongTask { + reportContentResult(eventId, reason, blockUserId) + } + + override suspend fun kickUser(userId: UserId, reason: String?): Result = simulateLongTask { + kickUserResult(userId, reason) + } + + override suspend fun banUser(userId: UserId, reason: String?): Result = simulateLongTask { + banUserResult(userId, reason) + } + + override suspend fun unbanUser(userId: UserId, reason: String?): Result = simulateLongTask { + unBanUserResult(userId, reason) + } + + override suspend fun generateWidgetWebViewUrl( + widgetSettings: MatrixWidgetSettings, + clientId: String, + languageTag: String?, + theme: String? + ): Result = simulateLongTask { + generateWidgetWebViewUrlResult(widgetSettings, clientId, languageTag, theme) + } + + override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result { + return getWidgetDriverResult(widgetSettings) + } + + override suspend fun sendCallNotificationIfNeeded(): Result = simulateLongTask { + sendCallNotificationIfNeededResult() + } + + override suspend fun setSendQueueEnabled(enabled: Boolean) = simulateLongTask { + setSendQueueEnabledResult(enabled) + } + + override suspend fun ignoreDeviceTrustAndResend(devices: Map>, sendHandle: SendHandle): Result = simulateLongTask { + ignoreDeviceTrustAndResendResult(devices, sendHandle) + } + + override suspend fun withdrawVerificationAndResend(userIds: List, sendHandle: SendHandle): Result = simulateLongTask { + withdrawVerificationAndResendResult(userIds, sendHandle) + } + + private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) { + progressCallbackValues.forEach { (current, total) -> + progressCallback?.onProgress(current, total) + delay(1) + } + } + + fun emitSyncUpdate() { + (syncUpdateFlow as MutableStateFlow).value = syncUpdateFlow.value + 1 + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt deleted file mode 100644 index d214f91f2b5..00000000000 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ /dev/null @@ -1,644 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.matrix.test.room - -import io.element.android.libraries.matrix.api.core.DeviceId -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.ProgressCallback -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.SendHandle -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.TransactionId -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange -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 -import io.element.android.libraries.matrix.api.media.MediaUploadHandler -import io.element.android.libraries.matrix.api.media.VideoInfo -import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService -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.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState -import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState -import io.element.android.libraries.matrix.api.room.MessageEventType -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.StateEventType -import io.element.android.libraries.matrix.api.room.draft.ComposerDraft -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.knock.KnockRequest -import io.element.android.libraries.matrix.api.room.location.AssetType -import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels -import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange -import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility -import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId -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.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler -import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService -import io.element.android.libraries.matrix.test.timeline.FakeTimeline -import io.element.android.tests.testutils.lambda.lambdaError -import io.element.android.tests.testutils.simulateLongTask -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.test.TestScope -import java.io.File - -class FakeMatrixRoom( - override val sessionId: SessionId = A_SESSION_ID, - override val roomId: RoomId = A_ROOM_ID, - override val displayName: String = "", - override val topic: String? = null, - override val avatarUrl: String? = null, - override var isEncrypted: Boolean = false, - override val canonicalAlias: RoomAlias? = null, - override val alternativeAliases: List = emptyList(), - override val isPublic: Boolean = true, - override val isSpace: Boolean = false, - override val isDirect: Boolean = false, - override val joinedMemberCount: Long = 123L, - override val activeMemberCount: Long = 234L, - val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), - override val liveTimeline: Timeline = FakeTimeline(), - override val roomCoroutineScope: CoroutineScope = TestScope(), - private var roomPermalinkResult: () -> Result = { lambdaError() }, - private var eventPermalinkResult: (EventId) -> Result = { lambdaError() }, - private val sendCallNotificationIfNeededResult: () -> Result = { lambdaError() }, - private val userDisplayNameResult: (UserId) -> Result = { lambdaError() }, - private val userAvatarUrlResult: () -> Result = { lambdaError() }, - private val userRoleResult: () -> Result = { lambdaError() }, - private val getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, - private val joinRoomResult: () -> Result = { lambdaError() }, - private val inviteUserResult: (UserId) -> Result = { lambdaError() }, - private val canInviteResult: (UserId) -> Result = { lambdaError() }, - private val canKickResult: (UserId) -> Result = { lambdaError() }, - private val canBanResult: (UserId) -> Result = { lambdaError() }, - private val canRedactOwnResult: (UserId) -> Result = { lambdaError() }, - private val canRedactOtherResult: (UserId) -> Result = { lambdaError() }, - private val canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, - private val canUserSendMessageResult: (UserId, MessageEventType) -> Result = { _, _ -> lambdaError() }, - private val sendImageResult: (File, File?, ImageInfo, String?, String?, ProgressCallback?) -> Result = - { _, _, _, _, _, _ -> lambdaError() }, - private val sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?) -> Result = - { _, _, _, _, _, _ -> lambdaError() }, - private val sendFileResult: (File, FileInfo, String?, String?, ProgressCallback?) -> Result = - { _, _, _, _, _ -> lambdaError() }, - private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?) -> Result = - { _, _, _, _, _ -> lambdaError() }, - private val sendVoiceMessageResult: (File, AudioInfo, List, ProgressCallback?) -> Result = - { _, _, _, _ -> lambdaError() }, - private val setNameResult: (String) -> Result = { lambdaError() }, - private val setTopicResult: (String) -> Result = { lambdaError() }, - private val updateAvatarResult: (String, ByteArray) -> Result = { _, _ -> lambdaError() }, - private val removeAvatarResult: () -> Result = { lambdaError() }, - private val editMessageLambda: (EventId, String, String?, List) -> Result = { _, _, _, _ -> lambdaError() }, - private val sendMessageResult: (String, String?, List) -> Result = { _, _, _ -> lambdaError() }, - private val updateUserRoleResult: () -> Result = { lambdaError() }, - private val toggleReactionResult: (String, EventOrTransactionId) -> Result = { _, _ -> lambdaError() }, - private val cancelSendResult: (TransactionId) -> Result = { lambdaError() }, - private val forwardEventResult: (EventId, List) -> Result = { _, _ -> lambdaError() }, - private val reportContentResult: (EventId, String, UserId?) -> Result = { _, _, _ -> lambdaError() }, - private val kickUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, - private val banUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, - private val unBanUserResult: (UserId, String?) -> Result = { _, _ -> lambdaError() }, - private val sendLocationResult: (String, String, String?, Int?, AssetType?) -> Result = { _, _, _, _, _ -> lambdaError() }, - private val createPollResult: (String, List, Int, PollKind) -> Result = { _, _, _, _ -> lambdaError() }, - private val editPollResult: (EventId, String, List, Int, PollKind) -> Result = { _, _, _, _, _ -> lambdaError() }, - private val sendPollResponseResult: (EventId, List) -> Result = { _, _ -> lambdaError() }, - private val endPollResult: (EventId, String) -> Result = { _, _ -> lambdaError() }, - private val progressCallbackValues: List> = emptyList(), - private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result = { _, _, _, _ -> lambdaError() }, - private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result = { lambdaError() }, - private val canUserTriggerRoomNotificationResult: (UserId) -> Result = { lambdaError() }, - private val canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, - private val canUserPinUnpinResult: (UserId) -> Result = { lambdaError() }, - private val setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, - private val powerLevelsResult: () -> Result = { lambdaError() }, - private val updatePowerLevelsResult: () -> Result = { lambdaError() }, - private val resetPowerLevelsResult: () -> Result = { lambdaError() }, - private val typingNoticeResult: (Boolean) -> Result = { lambdaError() }, - private val leaveRoomLambda: () -> Result = { lambdaError() }, - private val updateMembersResult: () -> Unit = { lambdaError() }, - private val getMembersResult: (Int) -> Result> = { lambdaError() }, - private val timelineFocusedOnEventResult: (EventId) -> Result = { lambdaError() }, - private val pinnedEventsTimelineResult: () -> Result = { lambdaError() }, - private val mediaTimelineResult: (EventId?) -> Result = { lambdaError() }, - private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> }, - private val saveComposerDraftLambda: (ComposerDraft) -> Result = { _: ComposerDraft -> Result.success(Unit) }, - private val loadComposerDraftLambda: () -> Result = { Result.success(null) }, - private val clearComposerDraftLambda: () -> Result = { Result.success(Unit) }, - private val subscribeToSyncLambda: () -> Unit = { lambdaError() }, - private val ignoreDeviceTrustAndResendResult: (Map>, SendHandle) -> Result = { _, _ -> lambdaError() }, - private val withdrawVerificationAndResendResult: (List, SendHandle) -> Result = { _, _ -> lambdaError() }, - private val updateCanonicalAliasResult: (RoomAlias?, List) -> Result = { _, _ -> lambdaError() }, - private val updateRoomVisibilityResult: (RoomVisibility) -> Result = { lambdaError() }, - private val updateRoomHistoryVisibilityResult: (RoomHistoryVisibility) -> Result = { lambdaError() }, - private val roomVisibilityResult: () -> Result = { lambdaError() }, - private val publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, - private val removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, - private val enableEncryptionResult: () -> Result = { lambdaError() }, - private val updateJoinRuleResult: (JoinRule) -> Result = { lambdaError() }, -) : MatrixRoom { - private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) - override val roomInfoFlow: Flow = _roomInfoFlow - - fun givenRoomInfo(roomInfo: MatrixRoomInfo) { - _roomInfoFlow.tryEmit(roomInfo) - } - - private val _roomTypingMembersFlow: MutableSharedFlow> = MutableSharedFlow(replay = 1) - override val roomTypingMembersFlow: Flow> = _roomTypingMembersFlow - - fun givenRoomTypingMembers(typingMembers: List) { - _roomTypingMembersFlow.tryEmit(typingMembers) - } - - private val _identityStateChangesFlow: MutableSharedFlow> = MutableSharedFlow(replay = 1) - override val identityStateChangesFlow: Flow> = _identityStateChangesFlow - - fun emitIdentityStateChanges(identityStateChanges: List) { - _identityStateChangesFlow.tryEmit(identityStateChanges) - } - - private val _knockRequestsFlow: MutableSharedFlow> = MutableSharedFlow(replay = 1) - override val knockRequestsFlow: Flow> = _knockRequestsFlow - - fun emitKnockRequests(knockRequests: List) { - _knockRequestsFlow.tryEmit(knockRequests) - } - - override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) - - override val roomNotificationSettingsStateFlow: MutableStateFlow = - MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) - - override suspend fun updateMembers() = updateMembersResult() - - override suspend fun getUpdatedMember(userId: UserId): Result { - return getUpdatedMemberResult(userId) - } - - override suspend fun getMembers(limit: Int): Result> { - return getMembersResult(limit) - } - - override suspend fun updateRoomNotificationSettings(): Result = simulateLongTask { - val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() - roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings) - return Result.success(Unit) - } - - override suspend fun enableEncryption(): Result = simulateLongTask { - enableEncryptionResult().onSuccess { - isEncrypted = true - emitSyncUpdate() - } - } - - private val _syncUpdateFlow = MutableStateFlow(0L) - override val syncUpdateFlow: StateFlow = _syncUpdateFlow.asStateFlow() - - fun emitSyncUpdate() { - _syncUpdateFlow.tryEmit(_syncUpdateFlow.value + 1) - } - - override suspend fun timelineFocusedOnEvent(eventId: EventId): Result = simulateLongTask { - timelineFocusedOnEventResult(eventId) - } - - override suspend fun pinnedEventsTimeline(): Result = simulateLongTask { - pinnedEventsTimelineResult() - } - - override suspend fun mediaTimeline(eventId: EventId?): Result = simulateLongTask { - mediaTimelineResult(eventId) - } - - override suspend fun subscribeToSync() { - subscribeToSyncLambda() - } - - override suspend fun powerLevels(): Result { - return powerLevelsResult() - } - - override suspend fun updatePowerLevels(matrixRoomPowerLevels: MatrixRoomPowerLevels): Result = simulateLongTask { - updatePowerLevelsResult() - } - - override suspend fun resetPowerLevels(): Result = simulateLongTask { - resetPowerLevelsResult() - } - - override fun destroy() = Unit - - override suspend fun userDisplayName(userId: UserId): Result = simulateLongTask { - userDisplayNameResult(userId) - } - - override suspend fun userAvatarUrl(userId: UserId): Result = simulateLongTask { - userAvatarUrlResult() - } - - override suspend fun userRole(userId: UserId): Result { - return userRoleResult() - } - - override suspend fun updateUsersRoles(changes: List): Result { - return updateUserRoleResult() - } - - override suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List) = simulateLongTask { - editMessageLambda(eventId, body, htmlBody, intentionalMentions) - } - - override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List) = simulateLongTask { - sendMessageResult(body, htmlBody, intentionalMentions) - } - - override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result { - return toggleReactionResult(emoji, eventOrTransactionId) - } - - override suspend fun cancelSend(transactionId: TransactionId): Result { - return cancelSendResult(transactionId) - } - - override suspend fun getPermalink(): Result { - return roomPermalinkResult() - } - - override suspend fun getPermalinkFor(eventId: EventId): Result { - return eventPermalinkResult(eventId) - } - - override suspend fun leave(): Result { - return leaveRoomLambda() - } - - override suspend fun join(): Result { - return joinRoomResult() - } - - override suspend fun inviteUserById(id: UserId): Result = simulateLongTask { - inviteUserResult(id) - } - - override suspend fun canUserBan(userId: UserId): Result { - return canBanResult(userId) - } - - override suspend fun canUserKick(userId: UserId): Result { - return canKickResult(userId) - } - - override suspend fun canUserInvite(userId: UserId): Result { - return canInviteResult(userId) - } - - override suspend fun canUserRedactOwn(userId: UserId): Result { - return canRedactOwnResult(userId) - } - - override suspend fun canUserRedactOther(userId: UserId): Result { - return canRedactOtherResult(userId) - } - - override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result { - return canSendStateResult(userId, type) - } - - override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result { - return canUserSendMessageResult(userId, type) - } - - override suspend fun canUserTriggerRoomNotification(userId: UserId): Result { - return canUserTriggerRoomNotificationResult(userId) - } - - override suspend fun canUserJoinCall(userId: UserId): Result { - return canUserJoinCallResult(userId) - } - - override suspend fun canUserPinUnpin(userId: UserId): Result { - return canUserPinUnpinResult(userId) - } - - override suspend fun sendImage( - file: File, - thumbnailFile: File?, - imageInfo: ImageInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback? - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendImageResult( - file, - thumbnailFile, - imageInfo, - caption, - formattedCaption, - progressCallback, - ) - } - - override suspend fun sendVideo( - file: File, - thumbnailFile: File?, - videoInfo: VideoInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback? - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendVideoResult( - file, - thumbnailFile, - videoInfo, - caption, - formattedCaption, - progressCallback, - ) - } - - override suspend fun sendAudio( - file: File, - audioInfo: AudioInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback? - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendAudioResult( - file, - audioInfo, - caption, - formattedCaption, - progressCallback, - ) - } - - override suspend fun sendFile( - file: File, - fileInfo: FileInfo, - caption: String?, - formattedCaption: String?, - progressCallback: ProgressCallback? - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendFileResult( - file, - fileInfo, - caption, - formattedCaption, - progressCallback, - ) - } - - private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) { - progressCallbackValues.forEach { (current, total) -> - progressCallback?.onProgress(current, total) - delay(1) - } - } - - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = simulateLongTask { - forwardEventResult(eventId, roomIds) - } - - override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = simulateLongTask { - updateAvatarResult(mimeType, data) - } - - override suspend fun removeAvatar(): Result = simulateLongTask { - removeAvatarResult() - } - - override suspend fun setName(name: String): Result = simulateLongTask { - setNameResult(name) - } - - override suspend fun setTopic(topic: String): Result = simulateLongTask { - setTopicResult(topic) - } - - override suspend fun reportContent( - eventId: EventId, - reason: String, - blockUserId: UserId? - ): Result = simulateLongTask { - return reportContentResult(eventId, reason, blockUserId) - } - - override suspend fun kickUser(userId: UserId, reason: String?): Result { - return kickUserResult(userId, reason) - } - - override suspend fun banUser(userId: UserId, reason: String?): Result { - return banUserResult(userId, reason) - } - - override suspend fun unbanUser(userId: UserId, reason: String?): Result { - return unBanUserResult(userId, reason) - } - - override suspend fun setIsFavorite(isFavorite: Boolean): Result { - return setIsFavoriteResult(isFavorite) - } - - val markAsReadCalls = mutableListOf() - - override suspend fun markAsRead(receiptType: ReceiptType): Result { - markAsReadCalls.add(receiptType) - return Result.success(Unit) - } - - var setUnreadFlagCalls = mutableListOf() - private set - - override suspend fun setUnreadFlag(isUnread: Boolean): Result { - setUnreadFlagCalls.add(isUnread) - return Result.success(Unit) - } - - override suspend fun sendLocation( - body: String, - geoUri: String, - description: String?, - zoomLevel: Int?, - assetType: AssetType?, - ): Result = simulateLongTask { - return sendLocationResult( - body, - geoUri, - description, - zoomLevel, - assetType, - ) - } - - override suspend fun createPoll( - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind - ): Result = simulateLongTask { - return createPollResult( - question, - answers, - maxSelections, - pollKind, - ) - } - - override suspend fun editPoll( - pollStartId: EventId, - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind - ): Result = simulateLongTask { - return editPollResult( - pollStartId, - question, - answers, - maxSelections, - pollKind, - ) - } - - override suspend fun sendPollResponse( - pollStartId: EventId, - answers: List - ): Result = simulateLongTask { - return sendPollResponseResult(pollStartId, answers) - } - - override suspend fun endPoll( - pollStartId: EventId, - text: String - ): Result = simulateLongTask { - return endPollResult(pollStartId, text) - } - - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback? - ): Result = simulateLongTask { - simulateSendMediaProgress(progressCallback) - sendVoiceMessageResult( - file, - audioInfo, - waveform, - progressCallback, - ) - } - - override suspend fun typingNotice(isTyping: Boolean): Result { - return typingNoticeResult(isTyping) - } - - override suspend fun generateWidgetWebViewUrl( - widgetSettings: MatrixWidgetSettings, - clientId: String, - languageTag: String?, - theme: String?, - ): Result = generateWidgetWebViewUrlResult( - widgetSettings, - clientId, - languageTag, - theme, - ) - - override suspend fun sendCallNotificationIfNeeded(): Result { - return sendCallNotificationIfNeededResult() - } - - override suspend fun setSendQueueEnabled(enabled: Boolean) = setSendQueueEnabledLambda(enabled) - - override suspend fun saveComposerDraft(composerDraft: ComposerDraft) = saveComposerDraftLambda(composerDraft) - - override suspend fun loadComposerDraft() = loadComposerDraftLambda() - - override suspend fun clearComposerDraft() = clearComposerDraftLambda() - - override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result { - return getWidgetDriverResult(widgetSettings) - } - - override suspend fun ignoreDeviceTrustAndResend(devices: Map>, sendHandle: SendHandle): Result = simulateLongTask { - return ignoreDeviceTrustAndResendResult(devices, sendHandle) - } - - override suspend fun withdrawVerificationAndResend(userIds: List, sendHandle: SendHandle): Result = simulateLongTask { - return withdrawVerificationAndResendResult(userIds, sendHandle) - } - - override suspend fun updateCanonicalAlias(canonicalAlias: RoomAlias?, alternativeAliases: List): Result = simulateLongTask { - updateCanonicalAliasResult(canonicalAlias, alternativeAliases) - } - - override suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result = simulateLongTask { - updateRoomVisibilityResult(roomVisibility) - } - - override suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result = simulateLongTask { - updateRoomHistoryVisibilityResult(historyVisibility) - } - - override suspend fun getRoomVisibility(): Result = simulateLongTask { - roomVisibilityResult() - } - - override suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result = simulateLongTask { - publishRoomAliasInRoomDirectoryResult(roomAlias) - } - - override suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result = simulateLongTask { - removeRoomAliasFromRoomDirectoryResult(roomAlias) - } - - override suspend fun updateJoinRule(joinRule: JoinRule): Result = simulateLongTask { - updateJoinRuleResult(joinRule) - } - - fun givenRoomMembersState(state: MatrixRoomMembersState) { - membersStateFlow.value = state - } - - override suspend fun clearEventCacheStorage(): Result { - return Result.success(Unit) - } -} - -fun defaultRoomPowerLevels() = MatrixRoomPowerLevels( - ban = 50, - invite = 0, - kick = 50, - sendEvents = 0, - redactEvents = 50, - roomName = 100, - roomAvatar = 100, - roomTopic = 100 -) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeNotJoinedRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeNotJoinedRoom.kt new file mode 100644 index 00000000000..7691aa58c60 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeNotJoinedRoom.kt @@ -0,0 +1,29 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.test.room + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.NotJoinedRoom +import io.element.android.libraries.matrix.api.room.RoomMembershipDetails +import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +@Immutable +class FakeNotJoinedRoom( + override val localRoom: BaseRoom? = null, + override val previewInfo: RoomPreviewInfo = aRoomPreviewInfo(), + private val roomMembershipDetails: () -> Result = { lambdaError() }, +) : NotJoinedRoom { + override suspend fun membershipDetails(): Result = simulateLongTask { + roomMembershipDetails() + } + + override fun close() = Unit +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakePendingRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakePendingRoom.kt deleted file mode 100644 index 79392cbc440..00000000000 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakePendingRoom.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.test.room - -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.PendingRoom -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.tests.testutils.lambda.lambdaError -import io.element.android.tests.testutils.simulateLongTask - -class FakePendingRoom( - override val sessionId: SessionId = A_SESSION_ID, - override val roomId: RoomId = A_ROOM_ID, - private val declineInviteResult: () -> Result = { lambdaError() } -) : PendingRoom { - override suspend fun leave(): Result = simulateLongTask { - declineInviteResult() - } - - override fun close() = Unit -} 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 ed62d96d98b..d7f9ccd6bee 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 @@ -12,18 +12,19 @@ import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.CurrentUserMembership -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode 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.powerlevels.RoomPowerLevels +import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom 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_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_RAW_NAME import io.element.android.libraries.matrix.test.A_ROOM_TOPIC -import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableList @@ -33,24 +34,28 @@ fun aRoomInfo( rawName: String? = A_ROOM_RAW_NAME, topic: String? = A_ROOM_TOPIC, avatarUrl: String? = AN_AVATAR_URL, - isDirect: Boolean = false, isPublic: Boolean = true, + isDirect: Boolean = false, + isEncrypted: Boolean = false, joinRule: JoinRule? = JoinRule.Public, isSpace: Boolean = false, - isTombstoned: Boolean = false, + successorRoom: SuccessorRoom? = null, isFavorite: Boolean = false, canonicalAlias: RoomAlias? = null, alternativeAliases: List = emptyList(), currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, inviter: RoomMember? = null, - activeMembersCount: Long = 1, - invitedMembersCount: Long = 0, + activeMembersCount: Long = 2, + invitedMembersCount: Long = 1, joinedMembersCount: Long = 1, highlightCount: Long = 0, notificationCount: Long = 0, userDefinedNotificationMode: RoomNotificationMode? = null, hasRoomCall: Boolean = false, - userPowerLevels: ImmutableMap = persistentMapOf(), + roomPowerLevels: RoomPowerLevels = RoomPowerLevels( + values = defaultRoomPowerLevelValues(), + users = persistentMapOf(), + ), activeRoomCallParticipants: List = emptyList(), heroes: List = emptyList(), pinnedEventIds: List = emptyList(), @@ -60,17 +65,18 @@ fun aRoomInfo( numUnreadNotifications: Long = 0, numUnreadMentions: Long = 0, historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined, -) = MatrixRoomInfo( +) = RoomInfo( id = id, name = name, rawName = rawName, topic = topic, avatarUrl = avatarUrl, - isDirect = isDirect, isPublic = isPublic, + isDirect = isDirect, + isEncrypted = isEncrypted, joinRule = joinRule, isSpace = isSpace, - isTombstoned = isTombstoned, + successorRoom = successorRoom, isFavorite = isFavorite, canonicalAlias = canonicalAlias, alternativeAliases = alternativeAliases.toImmutableList(), @@ -83,7 +89,7 @@ fun aRoomInfo( notificationCount = notificationCount, userDefinedNotificationMode = userDefinedNotificationMode, hasRoomCall = hasRoomCall, - userPowerLevels = userPowerLevels, + roomPowerLevels = roomPowerLevels, activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(), heroes = heroes.toImmutableList(), pinnedEventIds = pinnedEventIds.toImmutableList(), 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 9a8c35d86d4..f4f7f3d835b 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 @@ -21,6 +21,7 @@ fun aRoomMember( normalizedPowerLevel: Long = 0L, isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.USER, + membershipChangeReason: String? = null, ) = RoomMember( userId = userId, displayName = displayName, @@ -31,4 +32,5 @@ fun aRoomMember( normalizedPowerLevel = normalizedPowerLevel, isIgnored = isIgnored, role = role, + membershipChangeReason = membershipChangeReason, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomPreviewInfoFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomPreviewInfoFixture.kt new file mode 100644 index 00000000000..74a58354a5b --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomPreviewInfoFixture.kt @@ -0,0 +1,55 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.test.room + +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 +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.join.JoinRule +import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo +import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.A_ROOM_TOPIC +import io.element.android.tests.testutils.lambda.lambdaError + +fun aRoomPreview( + localRoom: FakeBaseRoom? = null, + info: RoomPreviewInfo = aRoomPreviewInfo(), + roomMembershipDetails: () -> Result = { lambdaError() }, +) = FakeNotJoinedRoom( + localRoom = localRoom, + previewInfo = info, + roomMembershipDetails = roomMembershipDetails, +) + +fun aRoomPreviewInfo( + roomId: RoomId = A_ROOM_ID, + name: String? = A_ROOM_NAME, + topic: String? = A_ROOM_TOPIC, + avatarUrl: String? = AN_AVATAR_URL, + joinRule: JoinRule = JoinRule.Public, + isSpace: Boolean = false, + canonicalAlias: RoomAlias? = null, + currentUserMembership: CurrentUserMembership? = null, + numberOfJoinedMembers: Long = 1, + isHistoryWorldReadable: Boolean = true, +) = RoomPreviewInfo( + roomId = roomId, + name = name, + topic = topic, + avatarUrl = avatarUrl, + joinRule = joinRule, + canonicalAlias = canonicalAlias, + numberOfJoinedMembers = numberOfJoinedMembers, + roomType = if (isSpace) RoomType.Space else RoomType.Room, + isHistoryWorldReadable = isHistoryWorldReadable, + membership = currentUserMembership, +) 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 df34bcc734b..aab27e8a115 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 @@ -12,12 +12,14 @@ import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.CurrentUserMembership -import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode 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.message.RoomMessage +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels +import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.user.MatrixUser @@ -28,12 +30,11 @@ import io.element.android.libraries.matrix.test.A_ROOM_RAW_NAME import io.element.android.libraries.matrix.test.A_ROOM_TOPIC import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem -import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toPersistentList fun aRoomSummary( - info: MatrixRoomInfo = aRoomInfo(), + info: RoomInfo = aRoomInfo(), lastMessage: RoomMessage? = aRoomMessage(), ) = RoomSummary( info = info, @@ -46,11 +47,12 @@ fun aRoomSummary( rawName: String? = A_ROOM_RAW_NAME, topic: String? = A_ROOM_TOPIC, avatarUrl: String? = null, - isDirect: Boolean = false, isPublic: Boolean = true, + isDirect: Boolean = false, + isEncrypted: Boolean = false, joinRule: JoinRule? = JoinRule.Public, isSpace: Boolean = false, - isTombstoned: Boolean = false, + successorRoom: SuccessorRoom? = null, isFavorite: Boolean = false, canonicalAlias: RoomAlias? = null, alternativeAliases: List = emptyList(), @@ -63,7 +65,10 @@ fun aRoomSummary( notificationCount: Long = 0, userDefinedNotificationMode: RoomNotificationMode? = null, hasRoomCall: Boolean = false, - userPowerLevels: ImmutableMap = persistentMapOf(), + roomPowerLevels: RoomPowerLevels = RoomPowerLevels( + values = defaultRoomPowerLevelValues(), + users = persistentMapOf(), + ), activeRoomCallParticipants: List = emptyList(), heroes: List = emptyList(), pinnedEventIds: List = emptyList(), @@ -75,17 +80,18 @@ fun aRoomSummary( historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined, lastMessage: RoomMessage? = aRoomMessage(), ) = RoomSummary( - info = MatrixRoomInfo( + info = RoomInfo( id = roomId, name = name, rawName = rawName, topic = topic, avatarUrl = avatarUrl, - isDirect = isDirect, isPublic = isPublic, + isDirect = isDirect, + isEncrypted = isEncrypted, joinRule = joinRule, isSpace = isSpace, - isTombstoned = isTombstoned, + successorRoom = successorRoom, isFavorite = isFavorite, canonicalAlias = canonicalAlias, alternativeAliases = alternativeAliases.toPersistentList(), @@ -94,7 +100,7 @@ fun aRoomSummary( activeMembersCount = activeMembersCount, invitedMembersCount = invitedMembersCount, joinedMembersCount = joinedMembersCount, - userPowerLevels = userPowerLevels, + roomPowerLevels = roomPowerLevels, highlightCount = highlightCount, notificationCount = notificationCount, userDefinedNotificationMode = userDefinedNotificationMode, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt index 9019eea6463..5469e5b4aaa 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.test.sync +import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import kotlinx.coroutines.flow.MutableStateFlow @@ -29,6 +30,8 @@ class FakeSyncService( override val syncState: StateFlow = syncStateFlow + override val isOnline: StateFlow = syncState.mapState { it != SyncState.Offline } + suspend fun emitSyncState(syncState: SyncState) { syncStateFlow.emit(syncState) } 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 1c335057b58..c4ab0160c4e 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 @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.test.timeline import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.TransactionId 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 @@ -18,6 +19,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.location.AssetType +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline @@ -25,45 +27,54 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import java.io.File class FakeTimeline( private val name: String = "FakeTimeline", override val timelineItems: Flow> = MutableStateFlow(emptyList()), - private val backwardPaginationStatus: MutableStateFlow = MutableStateFlow( + override val backwardPaginationStatus: MutableStateFlow = MutableStateFlow( Timeline.PaginationStatus( isPaginating = false, hasMoreToLoad = true ) ), - private val forwardPaginationStatus: MutableStateFlow = MutableStateFlow( + override val forwardPaginationStatus: MutableStateFlow = MutableStateFlow( Timeline.PaginationStatus( isPaginating = false, hasMoreToLoad = false ) ), override val membershipChangeEventReceived: Flow = MutableSharedFlow(), + private val progressCallbackValues: List> = emptyList(), + private val cancelSendResult: (TransactionId) -> Result = { lambdaError() }, ) : Timeline { var sendMessageLambda: ( body: String, htmlBody: String?, intentionalMentions: List, ) -> Result = { _, _, _ -> - Result.success(Unit) + lambdaError() + } + + override suspend fun cancelSend(transactionId: TransactionId): Result = simulateLongTask { + cancelSendResult(transactionId) } override suspend fun sendMessage( body: String, htmlBody: String?, intentionalMentions: List, - ): Result = sendMessageLambda(body, htmlBody, intentionalMentions) + ): Result = simulateLongTask { + sendMessageLambda(body, htmlBody, intentionalMentions) + } var redactEventLambda: (eventOrTransactionId: EventOrTransactionId, reason: String?) -> Result = { _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun redactEvent( @@ -77,7 +88,7 @@ class FakeTimeline( htmlBody: String?, intentionalMentions: List, ) -> Result = { _, _, _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun editMessage( @@ -111,23 +122,23 @@ class FakeTimeline( ) var replyMessageLambda: ( - eventId: EventId, + replyParameters: ReplyParameters, body: String, htmlBody: String?, intentionalMentions: List, fromNotification: Boolean, ) -> Result = { _, _, _, _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun replyMessage( - eventId: EventId, + replyParameters: ReplyParameters, body: String, htmlBody: String?, intentionalMentions: List, fromNotification: Boolean, ): Result = replyMessageLambda( - eventId, + replyParameters, body, htmlBody, intentionalMentions, @@ -141,7 +152,8 @@ class FakeTimeline( body: String?, formattedBody: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _, _, _ -> + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } @@ -152,14 +164,19 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, - ): Result = sendImageLambda( - file, - thumbnailFile, - imageInfo, - caption, - formattedCaption, - progressCallback - ) + replyParameters: ReplyParameters?, + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendImageLambda( + file, + thumbnailFile, + imageInfo, + caption, + formattedCaption, + progressCallback, + replyParameters, + ) + } var sendVideoLambda: ( file: File, @@ -168,7 +185,8 @@ class FakeTimeline( body: String?, formattedBody: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _, _, _ -> + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } @@ -179,14 +197,19 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, - ): Result = sendVideoLambda( - file, - thumbnailFile, - videoInfo, - caption, - formattedCaption, - progressCallback - ) + replyParameters: ReplyParameters?, + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendVideoLambda( + file, + thumbnailFile, + videoInfo, + caption, + formattedCaption, + progressCallback, + replyParameters, + ) + } var sendAudioLambda: ( file: File, @@ -194,7 +217,8 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _, _ -> + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } @@ -204,13 +228,18 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, - ): Result = sendAudioLambda( - file, - audioInfo, - caption, - formattedCaption, - progressCallback - ) + replyParameters: ReplyParameters?, + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendAudioLambda( + file, + audioInfo, + caption, + formattedCaption, + progressCallback, + replyParameters, + ) + } var sendFileLambda: ( file: File, @@ -218,7 +247,8 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _, _ -> + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) } @@ -228,22 +258,45 @@ class FakeTimeline( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, - ): Result = sendFileLambda( - file, - fileInfo, - caption, - formattedCaption, - progressCallback - ) + replyParameters: ReplyParameters?, + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendFileLambda( + file, + fileInfo, + caption, + formattedCaption, + progressCallback, + replyParameters, + ) + } - var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result = { _, _ -> Result.success(Unit) } - override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = toggleReactionLambda( - emoji, - eventOrTransactionId - ) + var sendVoiceMessageLambda: ( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } - var forwardEventLambda: (eventId: EventId, roomIds: List) -> Result = { _, _ -> Result.success(Unit) } - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = forwardEventLambda(eventId, roomIds) + override suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, + ): Result = simulateLongTask { + simulateSendMediaProgress(progressCallback) + sendVoiceMessageLambda( + file, + audioInfo, + waveform, + progressCallback, + replyParameters, + ) + } var sendLocationLambda: ( body: String, @@ -251,8 +304,9 @@ class FakeTimeline( description: String?, zoomLevel: Int?, assetType: AssetType?, - ) -> Result = { _, _, _, _, _ -> - Result.success(Unit) + replyParameters: ReplyParameters?, + ) -> Result = { _, _, _, _, _, _ -> + lambdaError() } override suspend fun sendLocation( @@ -261,13 +315,32 @@ class FakeTimeline( description: String?, zoomLevel: Int?, assetType: AssetType?, - ): Result = sendLocationLambda( - body, - geoUri, - description, - zoomLevel, - assetType - ) + replyParameters: ReplyParameters?, + ): Result = simulateLongTask { + sendLocationLambda( + body, + geoUri, + description, + zoomLevel, + assetType, + replyParameters, + ) + } + + var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result = { _, _ -> lambdaError() } + + override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = simulateLongTask { + toggleReactionLambda( + emoji, + eventOrTransactionId, + ) + } + + var forwardEventLambda: (eventId: EventId, roomIds: List) -> Result = { _, _ -> lambdaError() } + + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = simulateLongTask { + forwardEventLambda(eventId, roomIds) + } var createPollLambda: ( question: String, @@ -275,20 +348,17 @@ class FakeTimeline( maxSelections: Int, pollKind: PollKind, ) -> Result = { _, _, _, _ -> - Result.success(Unit) + lambdaError() } - override suspend fun createPoll( - question: String, - answers: List, - maxSelections: Int, - pollKind: PollKind, - ): Result = createPollLambda( - question, - answers, - maxSelections, - pollKind - ) + override suspend fun createPoll(question: String, answers: List, maxSelections: Int, pollKind: PollKind): Result = simulateLongTask { + createPollLambda( + question, + answers, + maxSelections, + pollKind, + ) + } var editPollLambda: ( pollStartId: EventId, @@ -297,7 +367,7 @@ class FakeTimeline( maxSelections: Int, pollKind: PollKind, ) -> Result = { _, _, _, _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun editPoll( @@ -305,65 +375,56 @@ class FakeTimeline( question: String, answers: List, maxSelections: Int, - pollKind: PollKind, - ): Result = editPollLambda( - pollStartId, - question, - answers, - maxSelections, - pollKind - ) + pollKind: PollKind + ): Result = simulateLongTask { + editPollLambda( + pollStartId, + question, + answers, + maxSelections, + pollKind, + ) + } var sendPollResponseLambda: ( pollStartId: EventId, answers: List, ) -> Result = { _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun sendPollResponse( pollStartId: EventId, answers: List, - ): Result = sendPollResponseLambda(pollStartId, answers) + ): Result = simulateLongTask { + sendPollResponseLambda( + pollStartId, + answers, + ) + } var endPollLambda: ( pollStartId: EventId, text: String, ) -> Result = { _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun endPoll( pollStartId: EventId, text: String, - ): Result = endPollLambda(pollStartId, text) - - var sendVoiceMessageLambda: ( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - ) -> Result = { _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) + ): Result = simulateLongTask { + endPollLambda( + pollStartId, + text, + ) } - override suspend fun sendVoiceMessage( - file: File, - audioInfo: AudioInfo, - waveform: List, - progressCallback: ProgressCallback?, - ): Result = sendVoiceMessageLambda( - file, - audioInfo, - waveform, - progressCallback - ) - var sendReadReceiptLambda: ( eventId: EventId, receiptType: ReceiptType, ) -> Result = { _, _ -> - Result.success(Unit) + lambdaError() } override suspend fun sendReadReceipt( @@ -377,13 +438,6 @@ class FakeTimeline( override suspend fun paginate(direction: Timeline.PaginationDirection): Result = paginateLambda(direction) - override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow { - return when (direction) { - Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus - Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus - } - } - var loadReplyDetailsLambda: (eventId: EventId) -> InReplyTo = { InReplyTo.NotLoaded(it) } @@ -407,5 +461,12 @@ class FakeTimeline( closeCounter++ } + private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) { + progressCallbackValues.forEach { (current, total) -> + progressCallback?.onProgress(current, total) + delay(1) + } + } + override fun toString() = "FakeTimeline: $name" } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/LiveTimelineProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/LiveTimelineProvider.kt index 327cec3b08d..1723b56490d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/LiveTimelineProvider.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/LiveTimelineProvider.kt @@ -7,14 +7,14 @@ package io.element.android.libraries.matrix.test.timeline -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class LiveTimelineProvider( - private val room: MatrixRoom, + private val room: JoinedRoom, ) : TimelineProvider { override fun activeTimelineFlow(): StateFlow = MutableStateFlow(room.liveTimeline) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/item/event/Fixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/item/event/Fixture.kt new file mode 100644 index 00000000000..c9e7c304d57 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/item/event/Fixture.kt @@ -0,0 +1,25 @@ +/* + * 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.matrix.test.timeline.item.event + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange +import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent +import io.element.android.libraries.matrix.test.A_USER_ID + +fun aRoomMembershipContent( + userId: UserId = A_USER_ID, + userDisplayName: String? = null, + change: MembershipChange? = null, + reason: String? = null, +) = RoomMembershipContent( + userId = userId, + userDisplayName = userDisplayName, + change = change, + reason = reason, +) 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 65502a7cd7a..d8788e40264 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 @@ -7,11 +7,12 @@ package io.element.android.libraries.matrix.test.verification -import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow @@ -20,13 +21,14 @@ import kotlinx.coroutines.flow.StateFlow class FakeSessionVerificationService( initialSessionVerifiedStatus: SessionVerifiedStatus = SessionVerifiedStatus.Unknown, - private val requestVerificationLambda: () -> 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 startVerificationLambda: () -> Unit = { lambdaError() }, private val resetLambda: (Boolean) -> Unit = { lambdaError() }, - private val acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() }, + private val acknowledgeVerificationRequestLambda: (VerificationRequest.Incoming) -> Unit = { lambdaError() }, private val acceptVerificationRequestLambda: () -> Unit = { lambdaError() }, ) : SessionVerificationService { private val _sessionVerifiedStatus = MutableStateFlow(initialSessionVerifiedStatus) @@ -37,8 +39,12 @@ class FakeSessionVerificationService( override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus override val needsSessionVerification: Flow = _needsSessionVerification - override suspend fun requestVerification() { - requestVerificationLambda() + override suspend fun requestCurrentSessionVerification() { + requestCurrentSessionVerificationLambda() + } + + override suspend fun requestUserVerification(userId: UserId) { + requestUserVerificationLambda(userId) } override suspend fun cancelVerification() { @@ -68,8 +74,8 @@ class FakeSessionVerificationService( this.listener = listener } - override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) { - acknowledgeVerificationRequestLambda(details) + override suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) { + acknowledgeVerificationRequestLambda(verificationRequest) } override suspend fun acceptVerificationRequest() = simulateLongTask { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt index 392d4454e51..1b1c65abe31 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt @@ -15,7 +15,7 @@ class FakeCallWidgetSettingsProvider( ) : CallWidgetSettingsProvider { val providedBaseUrls = mutableListOf() - override fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings { + override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings { providedBaseUrls += baseUrl return provideFn(baseUrl, widgetId) } diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index e7df151eb40..3b54a0224b9 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.testtags) implementation(libs.coil.compose) implementation(libs.coil.gif) + implementation(libs.coil.network.okhttp) implementation(libs.jsoup) testImplementation(libs.coroutines.test) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt index d4a6627125e..7e105a8af7a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt @@ -13,8 +13,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.GraphicEq import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -82,7 +80,7 @@ fun AttachmentThumbnail( } AttachmentThumbnailType.Audio -> { Icon( - imageVector = Icons.Outlined.GraphicEq, + imageVector = CompoundIcons.Audio(), contentDescription = info.textContent, ) } 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 936955008db..fa9cf5156ce 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 @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -97,7 +96,7 @@ private fun AvatarActionBottomSheetContent( Text( text = stringResource(action.titleResId), style = ElementTheme.typography.fontBodyLgRegular, - color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, + color = if (action.destructive) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textPrimary, ) }, leadingContent = ListItemContent.Icon(IconSource.Resource(action.iconResourceId)), 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 new file mode 100644 index 00000000000..b0a19924b3f --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt @@ -0,0 +1,108 @@ +/* + * 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.matrix.ui.components + +import androidx.compose.foundation.layout.Column +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.material3.ExperimentalMaterial3Api +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.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.components.avatar.Avatar +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.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.R +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.getFullName +import io.element.android.libraries.ui.strings.CommonStrings + +/** + * Figma: + * https://www.figma.com/design/dywzKQvHYxFD1Ncn4a5NkI/PSB-675%253A-Improve-invite-into-a-DM?node-id=12-36886 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateDmConfirmationBottomSheet( + matrixUser: MatrixUser, + onSendInvite: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + ModalBottomSheet( + modifier = modifier, + onDismissRequest = onDismiss, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(24.dp)) + Avatar( + avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation), + avatarType = AvatarType.User, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.screen_bottom_sheet_create_dm_title), + style = ElementTheme.typography.fontHeadingMdBold, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName()), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(40.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onSendInvite, + leadingIcon = IconSource.Vector(CompoundIcons.UserAdd()), + text = stringResource(R.string.screen_bottom_sheet_create_dm_confirmation_button_title), + ) + Spacer(modifier = Modifier.height(16.dp)) + TextButton( + modifier = Modifier.fillMaxWidth(), + onClick = onDismiss, + text = stringResource(CommonStrings.action_cancel), + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@PreviewsDayNight +@Composable +internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview { + CreateDmConfirmationBottomSheet( + matrixUser = matrixUser, + onSendInvite = {}, + onDismiss = {}, + ) +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt index 9f7583972f5..3b8b6831ca5 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt @@ -13,29 +13,35 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ripple 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.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons 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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun EditableAvatarView( @@ -43,6 +49,7 @@ fun EditableAvatarView( displayName: String?, avatarUrl: Uri?, avatarSize: AvatarSize, + avatarType: AvatarType, onAvatarClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -50,27 +57,36 @@ fun EditableAvatarView( modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { + val a11yAvatar = stringResource(CommonStrings.a11y_avatar) Box( modifier = Modifier - .size(avatarSize.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, onClick = onAvatarClick, indication = ripple(bounded = false), ) .testTag(TestTags.editAvatar) + .clearAndSetSemantics { + contentDescription = a11yAvatar + }, ) { when (avatarUrl?.scheme) { null, "mxc" -> { Avatar( - avatarData = AvatarData(matrixId, displayName, avatarUrl?.toString(), size = avatarSize), - modifier = Modifier.fillMaxSize(), + avatarData = AvatarData( + id = matrixId, + name = displayName, + url = avatarUrl?.toString(), + size = avatarSize, + ), + avatarType = avatarType, ) } else -> { UnsavedAvatar( avatarUri = avatarUrl, - modifier = Modifier.fillMaxSize(), + avatarSize = avatarSize, + avatarType = avatarType, ) } } @@ -79,7 +95,7 @@ fun EditableAvatarView( modifier = Modifier .align(Alignment.BottomEnd) .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary) + .background(ElementTheme.colors.iconPrimary) .size(24.dp), contentAlignment = Alignment.Center, ) { @@ -87,7 +103,7 @@ fun EditableAvatarView( modifier = Modifier.size(16.dp), imageVector = CompoundIcons.EditSolid(), contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary, + tint = ElementTheme.colors.iconOnSolidPrimary, ) } } @@ -98,12 +114,15 @@ fun EditableAvatarView( @Composable internal fun EditableAvatarViewPreview( @PreviewParameter(EditableAvatarViewUriProvider::class) uri: Uri? -) = ElementPreview { +) = ElementPreview( + drawableFallbackForImages = CommonDrawables.sample_avatar, +) { EditableAvatarView( matrixId = "id", displayName = "A room", avatarUrl = uri, avatarSize = AvatarSize.EditRoomDetails, + avatarType = AvatarType.User, onAvatarClick = {}, ) } @@ -112,7 +131,7 @@ open class EditableAvatarViewUriProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( null, - Uri.parse("mxc://matrix.org/123456"), - Uri.parse("https://example.com/avatar.jpg"), + "mxc://matrix.org/123456".toUri(), + "https://example.com/avatar.jpg".toUri(), ) } 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 a081a990a55..960bdb90986 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 @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -19,6 +18,7 @@ import io.element.android.compound.theme.ElementTheme 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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text @@ -28,19 +28,24 @@ import io.element.android.libraries.matrix.ui.model.InviteSender @Composable fun InviteSenderView( inviteSender: InviteSender, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + hideAvatarImage: Boolean = false, ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier, ) { Box(modifier = Modifier.padding(vertical = 2.dp)) { - Avatar(avatarData = inviteSender.avatarData) - } + Avatar( + avatarData = inviteSender.avatarData, + avatarType = AvatarType.User, + hideImage = hideAvatarImage, + ) + } Text( text = inviteSender.annotatedString(), style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, ) } } @@ -56,8 +61,9 @@ internal fun InviteSenderViewPreview() = ElementPreview { id = "@bob:example.com", name = "Bob", url = null, - size = AvatarSize.InviteSender - ) + size = AvatarSize.InviteSender, + ), + membershipChangeReason = null, ) ) } 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 6aea3b6dfde..cb97c1e9f44 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 @@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text @@ -65,6 +66,7 @@ private fun MatrixUserHeaderContent( modifier = Modifier .padding(vertical = 12.dp), avatarData = matrixUser.getAvatarData(size = AvatarSize.UserPreference), + avatarType = AvatarType.User, ) Spacer(modifier = Modifier.width(16.dp)) Column( @@ -77,14 +79,14 @@ private fun MatrixUserHeaderContent( maxLines = 1, style = ElementTheme.typography.fontHeadingSmMedium, overflow = TextOverflow.Ellipsis, - color = ElementTheme.materialColors.primary, + color = ElementTheme.colors.textPrimary, ) // Id if (matrixUser.displayName.isNullOrEmpty().not()) { Text( text = matrixUser.userId.value, style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.materialColors.secondary, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis ) 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 3f9e945ee55..d10f428f1c6 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 @@ -21,7 +21,7 @@ open class MatrixUserProvider : PreviewParameterProvider { fun aMatrixUser( id: String = "@id_of_alice:server.org", - displayName: String = "Alice", + displayName: String? = "Alice", avatarUrl: String? = null, ) = MatrixUser( userId = UserId(id), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoProvider.kt index 6d85cc28cb4..f6ad30fae15 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoProvider.kt @@ -30,10 +30,12 @@ fun aSelectRoomInfo( canonicalAlias: RoomAlias? = null, avatarUrl: String? = null, heroes: ImmutableList = persistentListOf(), + isTombstoned: Boolean = false, ) = SelectRoomInfo( roomId = roomId, name = name, canonicalAlias = canonicalAlias, avatarUrl = avatarUrl, heroes = heroes, + isTombstoned = isTombstoned, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt index e43c52f5b05..4a672624ab2 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt @@ -26,9 +26,11 @@ 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 io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar +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.theme.components.Icon @@ -52,9 +54,12 @@ fun SelectedRoom( Column( horizontalAlignment = Alignment.CenterHorizontally, ) { - CompositeAvatar( + Avatar( avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom), - heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(), + avatarType = AvatarType.Room( + heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(), + isTombstoned = roomInfo.isTombstoned, + ), ) Text( // If name is null, we do not have space to render "No room name", so just use `#` here. @@ -65,21 +70,21 @@ fun SelectedRoom( ) } Surface( - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.iconPrimary, modifier = Modifier - .clip(CircleShape) - .size(20.dp) - .align(Alignment.TopEnd) - .clickable( - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - onClick = { onRemoveRoom(roomInfo) } - ), + .clip(CircleShape) + .size(20.dp) + .align(Alignment.TopEnd) + .clickable( + indication = ripple(), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onRemoveRoom(roomInfo) } + ), ) { Icon( imageVector = CompoundIcons.Close(), contentDescription = stringResource(id = CommonStrings.action_remove), - tint = MaterialTheme.colorScheme.onPrimary, + tint = ElementTheme.colors.iconOnSolidPrimary, modifier = Modifier.padding(2.dp) ) } 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 ddb61348957..31cc7248388 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 @@ -30,6 +30,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType 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 @@ -54,7 +55,10 @@ fun SelectedUser( Column( horizontalAlignment = Alignment.CenterHorizontally, ) { - Avatar(matrixUser.getAvatarData(size = AvatarSize.SelectedUser)) + Avatar( + avatarData = matrixUser.getAvatarData(size = AvatarSize.SelectedUser), + avatarType = AvatarType.User, + ) Text( modifier = Modifier.clipToBounds(), text = matrixUser.getBestName(), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt index 028c31ec78f..4c1c4d5f3cb 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -28,6 +27,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons 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.preview.ElementThemedPreview import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text @@ -47,7 +47,10 @@ fun UnresolvedUserRow( .padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically ) { - Avatar(avatarData) + Avatar( + avatarData = avatarData, + avatarType = AvatarType.User, + ) Column( modifier = Modifier .padding(start = 12.dp), @@ -58,7 +61,7 @@ fun UnresolvedUserRow( text = id, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontBodyLgMedium, ) @@ -69,18 +72,17 @@ fun UnresolvedUserRow( .padding(top = 3.dp) ) { Icon( - imageVector = CompoundIcons.Error(), + imageVector = CompoundIcons.ErrorSolid(), contentDescription = null, modifier = Modifier .size(18.dp) .align(Alignment.Top) .padding(2.dp), - tint = MaterialTheme.colorScheme.error, + tint = ElementTheme.colors.iconCriticalPrimary, ) - Text( text = stringResource(CommonStrings.common_invite_unknown_profile), - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodySmRegular.copy(lineHeight = 16.sp), ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt index b78f256deb1..1e041a69fe4 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt @@ -9,10 +9,11 @@ package io.element.android.libraries.matrix.ui.components import android.net.Uri import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement 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.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AddAPhoto import androidx.compose.material3.MaterialTheme @@ -24,12 +25,14 @@ import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.ImageRequest +import coil3.compose.AsyncImage +import coil3.request.ImageRequest import io.element.android.compound.theme.ElementTheme +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.avatarShape import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial @@ -41,11 +44,13 @@ import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial @Composable fun UnsavedAvatar( avatarUri: Uri?, + avatarSize: AvatarSize, + avatarType: AvatarType, modifier: Modifier = Modifier, ) { val commonModifier = modifier - .size(70.dp) - .clip(CircleShape) + .size(avatarSize.dp) + .clip(avatarType.avatarShape(avatarSize.dp)) if (avatarUri != null) { val context = LocalContext.current @@ -55,7 +60,7 @@ fun UnsavedAvatar( AsyncImage( modifier = commonModifier, model = model, - placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)), + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), contentScale = ContentScale.Crop, contentDescription = null, ) @@ -66,8 +71,8 @@ fun UnsavedAvatar( contentDescription = null, modifier = Modifier .align(Alignment.Center) - .size(40.dp), - tint = MaterialTheme.colorScheme.secondary, + .size(avatarSize.dp * 4 / 7), + tint = ElementTheme.colors.iconSecondary, ) } } @@ -76,8 +81,13 @@ fun UnsavedAvatar( @PreviewsDayNight @Composable internal fun UnsavedAvatarPreview() = ElementPreview { - Row { - UnsavedAvatar(null) - UnsavedAvatar(Uri.EMPTY) + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.User) + UnsavedAvatar(Uri.EMPTY, AvatarSize.EditRoomDetails, AvatarType.User) + UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.Space()) + UnsavedAvatar(Uri.EMPTY, AvatarSize.EditRoomDetails, AvatarType.Space()) } } 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 2889c33f995..2ee234bd43e 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 @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,6 +21,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme 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.AvatarType import io.element.android.libraries.designsystem.theme.components.Text @Composable @@ -39,7 +39,10 @@ internal fun UserRow( .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically ) { - Avatar(avatarData) + Avatar( + avatarData = avatarData, + avatarType = AvatarType.User, + ) Column( modifier = Modifier .padding(start = 12.dp) @@ -51,14 +54,14 @@ internal fun UserRow( text = name, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontBodyLgRegular, ) // Id subtext?.let { Text( text = subtext, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodySmRegular, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataFetcherFactory.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataFetcherFactory.kt index 178ae376fb7..e176c4c03b9 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataFetcherFactory.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataFetcherFactory.kt @@ -7,9 +7,9 @@ package io.element.android.libraries.matrix.ui.media -import coil.ImageLoader -import coil.fetch.Fetcher -import coil.request.Options +import coil3.ImageLoader +import coil3.fetch.Fetcher +import coil3.request.Options import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.MatrixClient @@ -24,7 +24,6 @@ internal class AvatarDataFetcherFactory( return CoilMediaFetcher( mediaLoader = client.mediaLoader, mediaData = data.toMediaRequestData(), - options = options ) } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt index 404c912e206..9bcae48a6af 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt @@ -7,16 +7,16 @@ package io.element.android.libraries.matrix.ui.media -import coil.decode.DataSource -import coil.decode.ImageSource -import coil.fetch.FetchResult -import coil.fetch.Fetcher -import coil.fetch.SourceResult -import coil.request.Options +import coil3.decode.DataSource +import coil3.decode.ImageSource +import coil3.fetch.FetchResult +import coil3.fetch.Fetcher +import coil3.fetch.SourceFetchResult import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.toFile import okio.Buffer +import okio.FileSystem import okio.Path.Companion.toOkioPath import timber.log.Timber import java.nio.ByteBuffer @@ -24,7 +24,6 @@ import java.nio.ByteBuffer internal class CoilMediaFetcher( private val mediaLoader: MatrixMediaLoader, private val mediaData: MediaRequestData, - private val options: Options ) : Fetcher { override suspend fun fetch(): FetchResult? { if (mediaData.source == null) { @@ -32,8 +31,8 @@ internal class CoilMediaFetcher( return null } return when (mediaData.kind) { - is MediaRequestData.Kind.Content -> fetchContent(mediaData.source, options) - is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaData.source, mediaData.kind, options) + is MediaRequestData.Kind.Content -> fetchContent(mediaData.source) + is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaData.source, mediaData.kind) is MediaRequestData.Kind.File -> fetchFile(mediaData.source, mediaData.kind) } } @@ -47,8 +46,12 @@ internal class CoilMediaFetcher( return mediaLoader.downloadMediaFile(mediaSource, kind.mimeType, kind.fileName) .map { mediaFile -> val file = mediaFile.toFile() - SourceResult( - source = ImageSource(file = file.toOkioPath(), closeable = mediaFile), + SourceFetchResult( + source = ImageSource( + file = file.toOkioPath(), + fileSystem = FileSystem.SYSTEM, + closeable = mediaFile, + ), mimeType = null, dataSource = DataSource.DISK ) @@ -59,37 +62,40 @@ internal class CoilMediaFetcher( .getOrNull() } - private suspend fun fetchContent(mediaSource: MediaSource, options: Options): FetchResult? { + private suspend fun fetchContent(mediaSource: MediaSource): FetchResult? { return mediaLoader.loadMediaContent( source = mediaSource, ).map { byteArray -> - byteArray.asSourceResult(options) + byteArray.asSourceResult() }.onFailure { Timber.e(it) }.getOrNull() } - private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail, options: Options): FetchResult? { + private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail): FetchResult? { return mediaLoader.loadMediaThumbnail( source = mediaSource, width = kind.width, height = kind.height, ).map { byteArray -> - byteArray.asSourceResult(options) + byteArray.asSourceResult() }.onFailure { Timber.e(it) }.getOrNull() } - private fun ByteArray.asSourceResult(options: Options): SourceResult { + private fun ByteArray.asSourceResult(): SourceFetchResult { val byteBuffer = ByteBuffer.wrap(this) val bufferedSource = try { Buffer().apply { write(byteBuffer) } } finally { byteBuffer.position(0) } - return SourceResult( - source = ImageSource(bufferedSource, options.context), + return SourceFetchResult( + source = ImageSource( + source = bufferedSource, + fileSystem = FileSystem.SYSTEM, + ), mimeType = null, dataSource = DataSource.MEMORY ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt index a16b22ea5f4..f8722a93a41 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt @@ -9,10 +9,10 @@ package io.element.android.libraries.matrix.ui.media import android.content.Context import android.os.Build -import coil.ImageLoader -import coil.ImageLoaderFactory -import coil.decode.GifDecoder -import coil.decode.ImageDecoderDecoder +import coil3.ImageLoader +import coil3.gif.AnimatedImageDecoder +import coil3.gif.GifDecoder +import coil3.network.okhttp.OkHttpNetworkFetcherFactory import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -31,13 +31,19 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor( private val okHttpClient: Provider, ) : LoggedInImageLoaderFactory { override fun newImageLoader(matrixClient: MatrixClient): ImageLoader { - return ImageLoader - .Builder(context) - .okHttpClient { okHttpClient.get() } + return ImageLoader.Builder(context) .components { + add( + OkHttpNetworkFetcherFactory( + callFactory = { + // Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient + okHttpClient.get().newBuilder().build() + } + ) + ) // Add gif support if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - add(ImageDecoderDecoder.Factory()) + add(AnimatedImageDecoder.Factory()) } else { add(GifDecoder.Factory()) } @@ -53,11 +59,19 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor( class NotLoggedInImageLoaderFactory @Inject constructor( @ApplicationContext private val context: Context, private val okHttpClient: Provider, -) : ImageLoaderFactory { - override fun newImageLoader(): ImageLoader { - return ImageLoader - .Builder(context) - .okHttpClient { okHttpClient.get() } +) { + fun newImageLoader(): ImageLoader { + return ImageLoader.Builder(context) + .components { + add( + OkHttpNetworkFetcherFactory( + callFactory = { + // Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient + okHttpClient.get().newBuilder().build() + } + ) + ) + } .build() } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt index 70021db5b03..0c9e0cb9381 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.matrix.ui.media -import coil.ImageLoader +import coil3.ImageLoader import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt index aab3380779d..5bca419a840 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.ui.media import io.element.android.libraries.matrix.api.media.MediaSource /** - * Can be use with [coil.compose.AsyncImage] to load a [MediaSource]. + * Can be use with [coil3.compose.AsyncImage] to load a [MediaSource]. * This will go internally through our [CoilMediaFetcher]. * * Example of usage: diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataFetcherFactory.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataFetcherFactory.kt index 2dd02397219..2ac2ebe5118 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataFetcherFactory.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataFetcherFactory.kt @@ -7,9 +7,9 @@ package io.element.android.libraries.matrix.ui.media -import coil.ImageLoader -import coil.fetch.Fetcher -import coil.request.Options +import coil3.ImageLoader +import coil3.fetch.Fetcher +import coil3.request.Options import io.element.android.libraries.matrix.api.MatrixClient internal class MediaRequestDataFetcherFactory( @@ -23,7 +23,6 @@ internal class MediaRequestDataFetcherFactory( return CoilMediaFetcher( mediaLoader = client.mediaLoader, mediaData = data, - options = options ) } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt index 335505a01f2..c6423cfb0fd 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt @@ -7,8 +7,8 @@ package io.element.android.libraries.matrix.ui.media -import coil.key.Keyer -import coil.request.Options +import coil3.key.Keyer +import coil3.request.Options import io.element.android.libraries.designsystem.components.avatar.AvatarData internal class AvatarDataKeyer : Keyer { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomMemberProfilesCache.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomMemberProfilesCache.kt index c71e5679cb6..0c02a24a0ed 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomMemberProfilesCache.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomMemberProfilesCache.kt @@ -7,32 +7,26 @@ package io.element.android.libraries.matrix.ui.messages -import androidx.compose.runtime.staticCompositionLocalOf import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.runningFold import javax.inject.Inject @SingleIn(RoomScope::class) class RoomMemberProfilesCache @Inject constructor() { private val cache = MutableStateFlow(mapOf()) + val updateFlow = cache.drop(1).runningFold(0) { acc, _ -> acc + 1 } - private val _lastCacheUpdate = MutableStateFlow(0L) - val lastCacheUpdate: StateFlow = _lastCacheUpdate - - fun replace(items: List) { + suspend fun replace(items: List) = coroutineScope { cache.value = items.associateBy { it.userId } - _lastCacheUpdate.tryEmit(_lastCacheUpdate.value + 1) } fun getDisplayName(userId: UserId): String? { return cache.value[userId]?.disambiguatedDisplayName } } - -val LocalRoomMemberProfilesCache = staticCompositionLocalOf { - RoomMemberProfilesCache() -} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomNamesCache.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomNamesCache.kt new file mode 100644 index 00000000000..598b7c28da2 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomNamesCache.kt @@ -0,0 +1,42 @@ +/* + * 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.matrix.ui.messages + +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.runningFold +import javax.inject.Inject + +@SingleIn(RoomScope::class) +class RoomNamesCache @Inject constructor() { + private val cache = MutableStateFlow(mapOf()) + val updateFlow = cache.drop(1).runningFold(0) { acc, _ -> acc + 1 } + + suspend fun replace(items: List) = coroutineScope { + val roomNamesByRoomIdOrAlias = LinkedHashMap(items.size * 2) + items + .forEach { summary -> + roomNamesByRoomIdOrAlias[summary.info.id.toRoomIdOrAlias()] = summary.info.name + val canonicalAlias = summary.info.canonicalAlias + if (canonicalAlias != null) { + roomNamesByRoomIdOrAlias[canonicalAlias.toRoomIdOrAlias()] = summary.info.name + } + } + cache.value = roomNamesByRoomIdOrAlias + } + + fun getDisplayName(roomIdOrAlias: RoomIdOrAlias): String? { + return cache.value[roomIdOrAlias] + } +} 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 ed30daf76fe..1b8e8bfd263 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 @@ -24,13 +24,16 @@ 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.isTraversalGroup import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.text.font.FontStyle 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.libraries.core.extensions.toSafeLength import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview @@ -57,7 +60,7 @@ fun InReplyToView( senderId = inReplyTo.senderId, senderProfile = inReplyTo.senderProfile, metadata = inReplyTo.metadata(hideImage), - modifier = modifier + modifier = modifier, ) } is InReplyToDetails.Error -> @@ -95,13 +98,18 @@ private fun ReplyToReadyContent( Spacer(modifier = Modifier.width(8.dp)) } val a11InReplyToText = stringResource(CommonStrings.common_in_reply_to, senderProfile.getDisambiguatedDisplayName(senderId)) - Column(verticalArrangement = Arrangement.SpaceBetween) { + Column( + modifier = Modifier.semantics(mergeDescendants = false) { isTraversalGroup = true }, + verticalArrangement = Arrangement.SpaceBetween + ) { SenderName( senderId = senderId, senderProfile = senderProfile, senderNameMode = SenderNameMode.Reply, modifier = Modifier.semantics { contentDescription = a11InReplyToText + isTraversalGroup = true + traversalIndex = 1f }, ) ReplyToContentText(metadata) @@ -140,7 +148,7 @@ private fun ReplyToErrorContent( Text( text = data.message, style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.error, + color = ElementTheme.colors.textCriticalPrimary, maxLines = 2, overflow = TextOverflow.Ellipsis, ) @@ -152,8 +160,10 @@ 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) - is InReplyToMetadata.Text -> metadata.text - is InReplyToMetadata.Thumbnail -> metadata.text + // Add a limit to the text length to avoid a crash in Compose + is InReplyToMetadata.Text -> metadata.text.toSafeLength() + // Add a limit to the text length to avoid a crash in Compose + is InReplyToMetadata.Thumbnail -> metadata.text.toSafeLength() null -> "" } val iconResourceId = when (metadata) { @@ -166,12 +176,16 @@ private fun ReplyToContentText(metadata: InReplyToMetadata?) { else -> FontStyle.Normal } Row( + modifier = Modifier.semantics(mergeDescendants = false) { + isTraversalGroup = true + traversalIndex = -1f + }, verticalAlignment = Alignment.CenterVertically, ) { if (iconResourceId != null) { Icon( resourceId = iconResourceId, - tint = MaterialTheme.colorScheme.secondary, + tint = ElementTheme.colors.iconSecondary, contentDescription = null, modifier = Modifier.size(16.dp) ) @@ -182,7 +196,7 @@ private fun ReplyToContentText(metadata: InReplyToMetadata?) { style = ElementTheme.typography.fontBodyMdRegular, fontStyle = fontStyle, textAlign = TextAlign.Start, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, maxLines = 2, overflow = TextOverflow.Ellipsis, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt index 0c0f4e4e2db..d59e9cf24f0 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.matrix.ui.messages.sender import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -77,7 +76,7 @@ private fun RowScope.MainText( val color = when (mode) { is SenderNameMode.Timeline -> mode.mainColor SenderNameMode.ActionList, - SenderNameMode.Reply -> MaterialTheme.colorScheme.primary + SenderNameMode.Reply -> ElementTheme.colors.textPrimary } Text( modifier = modifier.clipToBounds(), @@ -108,7 +107,7 @@ private fun RowScope.SecondaryText( modifier = modifier.clipToBounds(), text = text, style = style, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/InviteSender.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/InviteSender.kt index 265668f0f61..778d535fc63 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/InviteSender.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/InviteSender.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.matrix.ui.model -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.platform.LocalContext @@ -15,6 +14,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontWeight +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.UserId @@ -26,6 +26,7 @@ data class InviteSender( val userId: UserId, val displayName: String, val avatarData: AvatarData, + val membershipChangeReason: String?, ) { @Composable fun annotatedString(): AnnotatedString { @@ -37,7 +38,7 @@ data class InviteSender( AnnotatedString.Range( SpanStyle( fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.textPrimary ), start = senderNameStart, end = senderNameStart + displayName.length @@ -52,4 +53,5 @@ fun RoomMember.toInviteSender() = InviteSender( userId = userId, displayName = displayName ?: "", avatarData = getAvatarData(size = AvatarSize.InviteSender), + membershipChangeReason = membershipChangeReason ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt index 464cc57f6e1..72f69932aa3 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt @@ -7,9 +7,12 @@ package io.element.android.libraries.matrix.ui.model +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource 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.user.MatrixUser +import io.element.android.libraries.ui.strings.CommonStrings fun MatrixUser.getAvatarData(size: AvatarSize) = AvatarData( id = userId.value, @@ -21,3 +24,14 @@ fun MatrixUser.getAvatarData(size: AvatarSize) = AvatarData( fun MatrixUser.getBestName(): String { return displayName?.takeIf { it.isNotEmpty() } ?: userId.value } + +@Composable +fun MatrixUser.getFullName(): String { + return displayName.let { name -> + if (name.isNullOrBlank()) { + userId.value + } else { + stringResource(CommonStrings.common_name_and_id, name, userId.value) + } + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/RoomInfoExtension.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/RoomInfoExtension.kt index 413a043f79c..14e64c3557d 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/RoomInfoExtension.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/RoomInfoExtension.kt @@ -9,9 +9,9 @@ package io.element.android.libraries.matrix.ui.model 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.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.RoomInfo -fun MatrixRoomInfo.getAvatarData(size: AvatarSize) = AvatarData( +fun RoomInfo.getAvatarData(size: AvatarSize) = AvatarData( id = id.value, name = name, url = avatarUrl, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt index 8ca5dcd3265..63a0b1cc778 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SelectRoomInfo.kt @@ -21,6 +21,7 @@ data class SelectRoomInfo( val canonicalAlias: RoomAlias?, val avatarUrl: String?, val heroes: ImmutableList, + val isTombstoned: Boolean, ) { fun getAvatarData(size: AvatarSize) = AvatarData( id = roomId.value, @@ -36,4 +37,5 @@ fun RoomSummary.toSelectRoomInfo() = SelectRoomInfo( avatarUrl = info.avatarUrl, heroes = info.heroes, canonicalAlias = info.canonicalAlias, + isTombstoned = info.successorRoom != null, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt similarity index 78% rename from appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt index adba822ff73..7fc95035c6a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt @@ -5,15 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.appnav.room.joined +package io.element.android.libraries.matrix.ui.room import androidx.compose.runtime.Immutable import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.di.SingleIn 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.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -27,7 +25,7 @@ import javax.inject.Inject sealed interface LoadingRoomState { data object Loading : LoadingRoomState data object Error : LoadingRoomState - data class Loaded(val room: MatrixRoom) : LoadingRoomState + data class Loaded(val room: JoinedRoom) : LoadingRoomState } open class LoadingRoomStateProvider : PreviewParameterProvider { @@ -38,10 +36,9 @@ open class LoadingRoomStateProvider : PreviewParameterProvider ) } -@SingleIn(SessionScope::class) class LoadingRoomStateFlowFactory @Inject constructor(private val matrixClient: MatrixClient) { fun create(lifecycleScope: CoroutineScope, roomId: RoomId): StateFlow = - getRoomFlow(roomId) + getJoinedRoomFlow(roomId) .map { room -> if (room != null) { LoadingRoomState.Loaded(room) @@ -51,8 +48,8 @@ class LoadingRoomStateFlowFactory @Inject constructor(private val matrixClient: } .stateIn(lifecycleScope, SharingStarted.Eagerly, LoadingRoomState.Loading) - private fun getRoomFlow(roomId: RoomId): Flow = suspend { - matrixClient.getRoom(roomId = roomId) + private fun getJoinedRoomFlow(roomId: RoomId): Flow = suspend { + matrixClient.getJoinedRoom(roomId = roomId) } .asFlow() } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt index 8ab1a4b9f50..8631066482a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt @@ -14,19 +14,19 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +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.RoomMembersState import io.element.android.libraries.matrix.api.room.roomMembers @Composable -fun MatrixRoom.getRoomMemberAsState(userId: UserId): State { +fun BaseRoom.getRoomMemberAsState(userId: UserId): State { val roomMembersState by membersStateFlow.collectAsState() return getRoomMemberAsState(roomMembersState = roomMembersState, userId = userId) } @Composable -fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserId): State { +fun getRoomMemberAsState(roomMembersState: RoomMembersState, userId: UserId): State { val roomMembers = roomMembersState.roomMembers() return remember(roomMembers) { derivedStateOf { @@ -38,19 +38,20 @@ fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserI } @Composable -fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): State { +fun BaseRoom.getDirectRoomMember(roomMembersState: RoomMembersState): State { val roomMembers = roomMembersState.roomMembers() - return remember(roomMembersState) { + val roomInfo by roomInfoFlow.collectAsState() + return remember(roomMembersState, roomInfo.isDirect) { derivedStateOf { roomMembers ?.filter { it.membership.isActive() } - ?.takeIf { it.size == 2 && isDirect } + ?.takeIf { it.size == 2 && roomInfo.isDirect == true } ?.find { it.userId != sessionId } } } } @Composable -fun MatrixRoom.getCurrentRoomMember(roomMembersState: MatrixRoomMembersState): State { +fun BaseRoom.getCurrentRoomMember(roomMembersState: RoomMembersState): State { return getRoomMemberAsState(roomMembersState = roomMembersState, userId = sessionId) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt index f7f47b78945..0080eea700a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt @@ -12,7 +12,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.isDm @@ -25,77 +25,77 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage @Composable -fun MatrixRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): State { +fun BaseRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): State { return produceState(initialValue = true, key1 = updateKey) { value = canSendMessage(type).getOrElse { true } } } @Composable -fun MatrixRoom.canInviteAsState(updateKey: Long): State { +fun BaseRoom.canInviteAsState(updateKey: Long): State { return produceState(initialValue = false, key1 = updateKey) { value = canInvite().getOrElse { false } } } @Composable -fun MatrixRoom.canRedactOwnAsState(updateKey: Long): State { +fun BaseRoom.canRedactOwnAsState(updateKey: Long): State { return produceState(initialValue = false, key1 = updateKey) { value = canRedactOwn().getOrElse { false } } } @Composable -fun MatrixRoom.canRedactOtherAsState(updateKey: Long): State { +fun BaseRoom.canRedactOtherAsState(updateKey: Long): State { return produceState(initialValue = false, key1 = updateKey) { value = canRedactOther().getOrElse { false } } } @Composable -fun MatrixRoom.canCall(updateKey: Long): State { +fun BaseRoom.canCall(updateKey: Long): State { return produceState(initialValue = false, key1 = updateKey) { value = canUserJoinCall(sessionId).getOrElse { false } } } @Composable -fun MatrixRoom.canPinUnpin(updateKey: Long): State { +fun BaseRoom.canPinUnpin(updateKey: Long): State { return produceState(initialValue = false, key1 = updateKey) { value = canUserPinUnpin(sessionId).getOrElse { false } } } @Composable -fun MatrixRoom.isDmAsState(updateKey: Long): State { - return produceState(initialValue = false, key1 = updateKey) { - value = isDm +fun BaseRoom.isDmAsState(): State { + return produceState(initialValue = false) { + roomInfoFlow.collect { value = it.isDm } } } @Composable -fun MatrixRoom.canKickAsState(updateKey: Long): State { +fun BaseRoom.canKickAsState(updateKey: Long): State { return produceState(initialValue = false, key1 = updateKey) { value = canKick().getOrElse { false } } } @Composable -fun MatrixRoom.canBanAsState(updateKey: Long): State { +fun BaseRoom.canBanAsState(updateKey: Long): State { return produceState(initialValue = false, key1 = updateKey) { value = canBan().getOrElse { false } } } @Composable -fun MatrixRoom.canHandleKnockRequestsAsState(updateKey: Long): State { +fun BaseRoom.canHandleKnockRequestsAsState(updateKey: Long): State { return produceState(initialValue = false, key1 = updateKey) { value = canHandleKnockRequests().getOrElse { false } } } @Composable -fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State { +fun BaseRoom.userPowerLevelAsState(updateKey: Long): State { return produceState(initialValue = 0, key1 = updateKey) { value = userRole(sessionId) .getOrDefault(RoomMember.Role.USER) @@ -104,26 +104,26 @@ fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State { } @Composable -fun MatrixRoom.isOwnUserAdmin(): Boolean { - val roomInfo by roomInfoFlow.collectAsState(initial = null) - val powerLevel = roomInfo?.userPowerLevels?.get(sessionId) ?: 0L +fun BaseRoom.isOwnUserAdmin(): Boolean { + val roomInfo by roomInfoFlow.collectAsState() + val powerLevel = roomInfo.roomPowerLevels?.users?.get(sessionId) ?: 0L return RoomMember.Role.forPowerLevel(powerLevel) == RoomMember.Role.ADMIN } @Composable -fun MatrixRoom.rawName(): String? { - val roomInfo by roomInfoFlow.collectAsState(initial = null) - return roomInfo?.rawName +fun BaseRoom.rawName(): String? { + val roomInfo by roomInfoFlow.collectAsState() + return roomInfo.rawName } @Composable -fun MatrixRoom.topic(): String? { - val roomInfo by roomInfoFlow.collectAsState(initial = null) - return roomInfo?.topic +fun BaseRoom.topic(): String? { + val roomInfo by roomInfoFlow.collectAsState() + return roomInfo.topic } @Composable -fun MatrixRoom.avatarUrl(): String? { - val roomInfo by roomInfoFlow.collectAsState(initial = null) - return roomInfo?.avatarUrl +fun BaseRoom.avatarUrl(): String? { + val roomInfo by roomInfoFlow.collectAsState() + return roomInfo.avatarUrl } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt new file mode 100644 index 00000000000..0001131e596 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt @@ -0,0 +1,80 @@ +/* + * 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.matrix.ui.room + +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.matrix.api.core.UserId +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.roomMembers +import io.element.android.libraries.matrix.ui.model.getAvatarData +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow + +@OptIn(ExperimentalCoroutinesApi::class) +fun JoinedRoom.roomMemberIdentityStateChange(waitForEncryption: Boolean): Flow> { + val encryptionChangeFlow = flow { + if (waitForEncryption) { + // Room cannot become unencrypted, so it's ok to use first here + roomInfoFlow.first { roomInfo -> roomInfo.isEncrypted == true } + } + emit(Unit) + } + return encryptionChangeFlow + .flatMapLatest { + combine(identityStateChangesFlow, membersStateFlow) { identityStateChanges, membersState -> + identityStateChanges.map { identityStateChange -> + val member = membersState.roomMembers() + ?.find { roomMember -> roomMember.userId == identityStateChange.userId } + ?.toIdentityRoomMember() + ?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId) + RoomMemberIdentityStateChange( + identityRoomMember = member, + identityState = identityStateChange.identityState, + ) + }.toPersistentList() + }.distinctUntilChanged() + } +} + +private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember( + userId = userId, + displayNameOrDefault = displayNameOrDefault, + avatarData = getAvatarData(AvatarSize.ComposerAlert), +) + +private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember( + userId = userId, + displayNameOrDefault = userId.extractedDisplayName, + avatarData = AvatarData( + id = userId.value, + name = null, + url = null, + size = AvatarSize.ComposerAlert, + ), +) + +data class RoomMemberIdentityStateChange( + val identityRoomMember: IdentityRoomMember, + val identityState: IdentityState, +) + +data class IdentityRoomMember( + val userId: UserId, + val displayNameOrDefault: String, + val avatarData: AvatarData, +) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt index acd7723b875..f97e2ff7662 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressField.kt @@ -15,6 +15,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.Text import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TextFieldValidity import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -56,7 +57,10 @@ fun RoomAddressField( } else -> supportingText }, - isError = addressValidity.isError(), + validity = when (addressValidity) { + RoomAddressValidity.InvalidSymbols, RoomAddressValidity.NotAvailable -> TextFieldValidity.Invalid + else -> TextFieldValidity.None + }, onValueChange = onAddressChange, singleLine = true, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidity.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidity.kt index 25af0ab98dd..fef36ccd203 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidity.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidity.kt @@ -19,8 +19,4 @@ sealed interface RoomAddressValidity { data object InvalidSymbols : RoomAddressValidity data object NotAvailable : RoomAddressValidity data object Valid : RoomAddressValidity - - fun isError(): Boolean { - return this is InvalidSymbols || this is NotAvailable - } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidityEffect.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidityEffect.kt index 73c14c50dfd..e6ef9de2d35 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidityEffect.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/address/RoomAddressValidityEffect.kt @@ -32,7 +32,7 @@ fun RoomAddressValidityEffect( } // debounce the room address validation delay(300) - val roomAlias = client.roomAliasFromName(newRoomAddress).getOrNull() + val roomAlias = client.roomAliasFromName(newRoomAddress) if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) { onChange(RoomAddressValidity.InvalidSymbols) } else { diff --git a/libraries/matrixui/src/main/res/values-cs/translations.xml b/libraries/matrixui/src/main/res/values-cs/translations.xml index 0e557cc546e..a8e82b288ce 100644 --- a/libraries/matrixui/src/main/res/values-cs/translations.xml +++ b/libraries/matrixui/src/main/res/values-cs/translations.xml @@ -1,4 +1,7 @@ + "Poslat pozvánku" + "Chcete začít chatovat s %1$s?" + "Poslat pozvánku?" "%1$s (%2$s) vás pozval(a)" diff --git a/libraries/matrixui/src/main/res/values-cy/translations.xml b/libraries/matrixui/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..0131131fd7c --- /dev/null +++ b/libraries/matrixui/src/main/res/values-cy/translations.xml @@ -0,0 +1,7 @@ + + + "Anfon gwahoddiad" + "Hoffech chi ddechrau sgwrs gyda %1$s?" + "Anfon gwahoddiad?" + "Mae %1$s (%2$s) wedi eich gwahodd" + diff --git a/libraries/matrixui/src/main/res/values-da/translations.xml b/libraries/matrixui/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..5b4970f003c --- /dev/null +++ b/libraries/matrixui/src/main/res/values-da/translations.xml @@ -0,0 +1,7 @@ + + + "Send invitation" + "Kunne du tænke dig at starte en samtale med %1$s?" + "Send invitation?" + "%1$s(%2$s ) inviterede dig" + diff --git a/libraries/matrixui/src/main/res/values-de/translations.xml b/libraries/matrixui/src/main/res/values-de/translations.xml index 591fe783280..67d42162c3e 100644 --- a/libraries/matrixui/src/main/res/values-de/translations.xml +++ b/libraries/matrixui/src/main/res/values-de/translations.xml @@ -1,4 +1,7 @@ + "Einladung senden" + "Möchten Sie einen Chat mit %1$s starten?" + "Einladung senden?" "%1$s (%2$s) hat dich eingeladen" diff --git a/libraries/matrixui/src/main/res/values-el/translations.xml b/libraries/matrixui/src/main/res/values-el/translations.xml index 089c4d49c3e..9e9f1954640 100644 --- a/libraries/matrixui/src/main/res/values-el/translations.xml +++ b/libraries/matrixui/src/main/res/values-el/translations.xml @@ -1,4 +1,7 @@ + "Αποστολή πρόσκλησης" + "Θα θέλατε να ξεκινήσετε μια συνομιλία με τον χρήστη %1$s;" + "Αποστολή πρόσκλησης;" "%1$s (%2$s) σέ προσκάλεσε" diff --git a/libraries/matrixui/src/main/res/values-es/translations.xml b/libraries/matrixui/src/main/res/values-es/translations.xml index 2d13ddcf912..5d4ac4cf430 100644 --- a/libraries/matrixui/src/main/res/values-es/translations.xml +++ b/libraries/matrixui/src/main/res/values-es/translations.xml @@ -1,4 +1,7 @@ + "Enviar invitación" + "¿Quieres iniciar un chat con %1$s?" + "¿Enviar invitación?" "%1$s (%2$s) te invitó" diff --git a/libraries/matrixui/src/main/res/values-et/translations.xml b/libraries/matrixui/src/main/res/values-et/translations.xml index 5e36d57d33d..bd9840940e1 100644 --- a/libraries/matrixui/src/main/res/values-et/translations.xml +++ b/libraries/matrixui/src/main/res/values-et/translations.xml @@ -1,4 +1,7 @@ + "Saada kutse" + "Kas sa soovid alustada vestlust kasutajaga %1$s?" + "Kas saadame kutse?" "%1$s (%2$s) saatis sulle kutse" diff --git a/libraries/matrixui/src/main/res/values-eu/translations.xml b/libraries/matrixui/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..3bea664f977 --- /dev/null +++ b/libraries/matrixui/src/main/res/values-eu/translations.xml @@ -0,0 +1,6 @@ + + + "Bidali gonbidapena" + "Gonbidapena bidali?" + "%1$s(e)k (%2$s) gonbidatu zaitu" + diff --git a/libraries/matrixui/src/main/res/values-fa/translations.xml b/libraries/matrixui/src/main/res/values-fa/translations.xml index a38fe738f0a..3f5d32f974f 100644 --- a/libraries/matrixui/src/main/res/values-fa/translations.xml +++ b/libraries/matrixui/src/main/res/values-fa/translations.xml @@ -1,4 +1,7 @@ + "فرستادن دعوت" + "می‌خواهید گپی را با %1$s بیاغازید؟" + "فرستادن دعوت؟" "%1$s (%2$s) دعوتتان کرد" diff --git a/libraries/matrixui/src/main/res/values-fi/translations.xml b/libraries/matrixui/src/main/res/values-fi/translations.xml index abed5f4a884..daba3555bbc 100644 --- a/libraries/matrixui/src/main/res/values-fi/translations.xml +++ b/libraries/matrixui/src/main/res/values-fi/translations.xml @@ -1,4 +1,7 @@ + "Lähetä kutsu" + "Haluaisitko aloittaa keskustelun käyttäjän %1$s kanssa?" + "Lähetetäänkö kutsu?" "%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 fc024258fad..ca952f53c07 100644 --- a/libraries/matrixui/src/main/res/values-fr/translations.xml +++ b/libraries/matrixui/src/main/res/values-fr/translations.xml @@ -1,4 +1,7 @@ + "Envoyer l’invitation" + "Voulez-vous entamer une discussion avec %1$s ?" + "Envoyer l’invitation ?" "%1$s (%2$s) vous a invité(e)" diff --git a/libraries/matrixui/src/main/res/values-hu/translations.xml b/libraries/matrixui/src/main/res/values-hu/translations.xml index 573f8e1f10b..f22454cd169 100644 --- a/libraries/matrixui/src/main/res/values-hu/translations.xml +++ b/libraries/matrixui/src/main/res/values-hu/translations.xml @@ -1,4 +1,7 @@ + "Meghívó küldése" + "Csevegést kezd vele: %1$s?" + "Meghívó küldése?" "%1$s (%2$s) meghívta" diff --git a/libraries/matrixui/src/main/res/values-in/translations.xml b/libraries/matrixui/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..e7706afa766 --- /dev/null +++ b/libraries/matrixui/src/main/res/values-in/translations.xml @@ -0,0 +1,7 @@ + + + "Kirim undangan" + "Apakah Anda ingin memulai obrolan dengan %1$s?" + "Kirim undangan?" + "%1$s (%2$s) mengundang Anda" + diff --git a/libraries/matrixui/src/main/res/values-it/translations.xml b/libraries/matrixui/src/main/res/values-it/translations.xml index 5f31ef01ba9..439d61337e3 100644 --- a/libraries/matrixui/src/main/res/values-it/translations.xml +++ b/libraries/matrixui/src/main/res/values-it/translations.xml @@ -1,4 +1,7 @@ + "Invia invito" + "Vuoi iniziare una conversazione con%1$s?" + "Inviare invito?" "%1$s (%2$s) ti ha invitato" diff --git a/libraries/matrixui/src/main/res/values-lt/translations.xml b/libraries/matrixui/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..e80c6ada53e --- /dev/null +++ b/libraries/matrixui/src/main/res/values-lt/translations.xml @@ -0,0 +1,4 @@ + + + "%1$s(%2$s) pakvietė Jus" + diff --git a/libraries/matrixui/src/main/res/values-nb/translations.xml b/libraries/matrixui/src/main/res/values-nb/translations.xml index 08044ee5eaa..01bea5c726c 100644 --- a/libraries/matrixui/src/main/res/values-nb/translations.xml +++ b/libraries/matrixui/src/main/res/values-nb/translations.xml @@ -1,4 +1,7 @@ + "Send invitasjon" + "Vil du starte en chat med %1$s?" + "Vil du sende invitasjon?" "%1$s(%2$s) inviterte deg" diff --git a/libraries/matrixui/src/main/res/values-pl/translations.xml b/libraries/matrixui/src/main/res/values-pl/translations.xml index 3c5d9a0d7f8..caa2c0e07d7 100644 --- a/libraries/matrixui/src/main/res/values-pl/translations.xml +++ b/libraries/matrixui/src/main/res/values-pl/translations.xml @@ -1,4 +1,7 @@ + "Wyślij zaproszenie" + "Czy chcesz rozpocząć czat z %1$s?" + "Wysłać zaproszenie?" "%1$s (%2$s) zaprosił Cię" diff --git a/libraries/matrixui/src/main/res/values-pt-rBR/translations.xml b/libraries/matrixui/src/main/res/values-pt-rBR/translations.xml index 5409152649c..a00e478b924 100644 --- a/libraries/matrixui/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/matrixui/src/main/res/values-pt-rBR/translations.xml @@ -1,4 +1,7 @@ + "Enviar convite" + "Gostaria de iniciar um bate-papo com %1$s?" + "Enviar convite?" "%1$s(%2$s) convidou você" diff --git a/libraries/matrixui/src/main/res/values-pt/translations.xml b/libraries/matrixui/src/main/res/values-pt/translations.xml index 5b5f8269128..c3cb0dcbda8 100644 --- a/libraries/matrixui/src/main/res/values-pt/translations.xml +++ b/libraries/matrixui/src/main/res/values-pt/translations.xml @@ -1,4 +1,7 @@ + "Enviar convite" + "Gostarias de iniciar uma conversa com %1$s?" + "Enviar convite?" "%1$s (%2$s) convidou-te" diff --git a/libraries/matrixui/src/main/res/values-ru/translations.xml b/libraries/matrixui/src/main/res/values-ru/translations.xml index 4e076cde1d4..44c30532d0c 100644 --- a/libraries/matrixui/src/main/res/values-ru/translations.xml +++ b/libraries/matrixui/src/main/res/values-ru/translations.xml @@ -1,4 +1,7 @@ + "Отправить приглашение" + "Хотите начать чат с %1$s?" + "Отправить приглашение?" "%1$s (%2$s) пригласил вас" diff --git a/libraries/matrixui/src/main/res/values-sk/translations.xml b/libraries/matrixui/src/main/res/values-sk/translations.xml index 0fada643d59..57b644f1ee1 100644 --- a/libraries/matrixui/src/main/res/values-sk/translations.xml +++ b/libraries/matrixui/src/main/res/values-sk/translations.xml @@ -1,4 +1,7 @@ + "Odoslať pozvánku" + "Chceli by ste začať rozhovor s používateľom %1$s?" + "Poslať pozvánku?" "%1$s (%2$s) vás pozval/a" diff --git a/libraries/matrixui/src/main/res/values-sv/translations.xml b/libraries/matrixui/src/main/res/values-sv/translations.xml index 64200445db2..7a446ff1a6b 100644 --- a/libraries/matrixui/src/main/res/values-sv/translations.xml +++ b/libraries/matrixui/src/main/res/values-sv/translations.xml @@ -1,4 +1,7 @@ + "Skicka inbjudan" + "Vill du starta en chatt med %1$s?" + "Skicka inbjudan?" "%1$s (%2$s) bjöd in dig" diff --git a/libraries/matrixui/src/main/res/values-tr/translations.xml b/libraries/matrixui/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..dc065e1d751 --- /dev/null +++ b/libraries/matrixui/src/main/res/values-tr/translations.xml @@ -0,0 +1,7 @@ + + + "Davet gönder" + "%1$s ile sohbet başlatmak ister misiniz?" + "Davet gönder?" + "%1$s (%2$s) sizi davet etti" + diff --git a/libraries/matrixui/src/main/res/values-uk/translations.xml b/libraries/matrixui/src/main/res/values-uk/translations.xml index 5b4209626ac..324bb960865 100644 --- a/libraries/matrixui/src/main/res/values-uk/translations.xml +++ b/libraries/matrixui/src/main/res/values-uk/translations.xml @@ -1,4 +1,7 @@ + "Надіслати запрошення" + "Хочете розпочати бесіду з %1$s?" + "Надіслати запрошення?" "%1$s (%2$s) запрошує вас" diff --git a/libraries/matrixui/src/main/res/values-ur/translations.xml b/libraries/matrixui/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..7a6751a1a95 --- /dev/null +++ b/libraries/matrixui/src/main/res/values-ur/translations.xml @@ -0,0 +1,4 @@ + + + "%1$s (%2$s) نے آپ کو مدعو کیا" + 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 eb6d87524ca..f851e399fe8 100644 --- a/libraries/matrixui/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/matrixui/src/main/res/values-zh-rTW/translations.xml @@ -1,4 +1,7 @@ + "傳送邀請" + "您想要開始與 %1$s 聊天嗎?" + "傳送邀請?" "%1$s(%2$s)邀請您" diff --git a/libraries/matrixui/src/main/res/values/localazy.xml b/libraries/matrixui/src/main/res/values/localazy.xml index 80939a8863c..b27021ebd64 100644 --- a/libraries/matrixui/src/main/res/values/localazy.xml +++ b/libraries/matrixui/src/main/res/values/localazy.xml @@ -1,4 +1,7 @@ + "Send invite" + "Would you like to start a chat with %1$s?" + "Send invite?" "%1$s (%2$s) invited you" diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt index 6a67d581b4b..5b3bc33d9fe 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.matrix.ui.media import androidx.test.platform.app.InstrumentationRegistry -import coil.ImageLoader +import coil3.ImageLoader import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.test.A_SESSION_ID diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeLoggedInImageLoaderFactory.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeLoggedInImageLoaderFactory.kt index b5c30a9b607..a4f5bbdfb7a 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeLoggedInImageLoaderFactory.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/media/FakeLoggedInImageLoaderFactory.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.matrix.ui.media -import coil.ImageLoader +import coil3.ImageLoader import io.element.android.libraries.matrix.api.MatrixClient class FakeLoggedInImageLoaderFactory( diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailTest.kt index 20e26cd125a..6fdff3ad1ca 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailTest.kt @@ -13,12 +13,12 @@ import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat -import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails +import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import org.junit.Test class InReplyToDetailTest { @@ -47,9 +47,8 @@ class InReplyToDetailTest { eventId = AN_EVENT_ID, senderId = A_USER_ID, senderProfile = aProfileTimelineDetails(), - content = RoomMembershipContent( + content = aRoomMembershipContent( userId = A_USER_ID, - userDisplayName = null, change = MembershipChange.INVITED, ) ) diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt index 6ef12cc3056..c96e55c6fa7 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt @@ -7,12 +7,6 @@ package io.element.android.libraries.matrix.ui.messages.reply -import android.content.res.Configuration -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow @@ -36,7 +30,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent -import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent 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 @@ -49,9 +42,11 @@ import io.element.android.libraries.matrix.test.media.aMediaSource import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails +import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +import io.element.android.tests.testutils.withConfigurationAndContext import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -350,7 +345,7 @@ class InReplyToMetadataKtTest { @Test fun `a location message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - testEnv { + withConfigurationAndContext { anInReplyToDetailsReady( eventContent = aMessageContent( messageType = LocationMessageType( @@ -380,7 +375,7 @@ class InReplyToMetadataKtTest { @Test fun `a voice message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { - testEnv { + withConfigurationAndContext { anInReplyToDetailsReady( eventContent = aMessageContent( messageType = VoiceMessageType( @@ -501,7 +496,7 @@ class InReplyToMetadataKtTest { fun `room membership content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { anInReplyToDetailsReady( - eventContent = RoomMembershipContent(A_USER_ID, null, null) + eventContent = aRoomMembershipContent(userId = A_USER_ID) ).metadata(hideImage = false) }.test { awaitItem().let { @@ -588,17 +583,3 @@ fun anImageInfo(): ImageInfo { blurhash = A_BLUR_HASH, ) } - -@Composable -private fun testEnv(content: @Composable () -> Any?): Any? { - var result: Any? = null - CompositionLocalProvider( - LocalConfiguration provides Configuration(), - LocalContext provides ApplicationProvider.getApplicationContext(), - ) { - content().apply { - result = this - } - } - return result -} diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt new file mode 100644 index 00000000000..611616cdd77 --- /dev/null +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt @@ -0,0 +1,101 @@ +/* + * 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.matrix.ui.model + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +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.user.MatrixUser +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.withConfigurationAndContext +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MatrixUserExtensionsTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `getAvatarData should return the expected value`() { + val matrixUser = MatrixUser( + userId = A_USER_ID, + displayName = "displayName", + avatarUrl = "avatarUrl", + ) + val expected = AvatarData( + id = A_USER_ID.value, + name = "displayName", + url = "avatarUrl", + size = AvatarSize.UserHeader, + ) + assertThat(matrixUser.getAvatarData(AvatarSize.UserHeader)).isEqualTo(expected) + } + + @Test + fun `getBestName should return the display name is available`() { + val matrixUser = MatrixUser( + userId = A_USER_ID, + displayName = "displayName", + ) + assertThat(matrixUser.getBestName()).isEqualTo("displayName") + } + + @Test + fun `getBestName should return the id when name is not available`() { + val matrixUser = MatrixUser( + userId = A_USER_ID, + displayName = null, + ) + assertThat(matrixUser.getBestName()).isEqualTo(A_USER_ID.value) + } + + @Test + fun `getBestName should return the id when name is empty`() { + val matrixUser = MatrixUser( + userId = A_USER_ID, + displayName = "", + ) + assertThat(matrixUser.getBestName()).isEqualTo(A_USER_ID.value) + } + + @Test + fun `getFullName should return the display name is available and the userId`() = runTest { + val matrixUser = MatrixUser( + userId = A_USER_ID, + displayName = "displayName", + ) + moleculeFlow(RecompositionMode.Immediate) { + withConfigurationAndContext { + matrixUser.getFullName() + } + }.test { + assertThat(awaitItem()).isEqualTo("displayName (@alice:server.org)") + } + } + + @Test + fun `getBestName should return only the id when name is not available`() = runTest { + val matrixUser = MatrixUser( + userId = A_USER_ID, + displayName = null, + ) + moleculeFlow(RecompositionMode.Immediate) { + matrixUser.getFullName() + }.test { + assertThat(awaitItem()).isEqualTo(A_USER_ID.value) + } + } +} diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembersTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembersTest.kt deleted file mode 100644 index 4187fe6e904..00000000000 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembersTest.kt +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.matrix.ui.room - -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState -import io.element.android.libraries.matrix.api.room.RoomMembershipState -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_ID_3 -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.room.aRoomMember -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class MatrixRoomMembersTest { - private val roomMember1 = aRoomMember(A_USER_ID) - private val roomMember2 = aRoomMember(A_USER_ID_2) - private val roomMember3 = aRoomMember(A_USER_ID_3) - - @Test - fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest { - val matrixRoom = FakeMatrixRoom( - sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, - ) - moleculeFlow(RecompositionMode.Immediate) { - matrixRoom.getDirectRoomMember( - MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2)) - ) - }.test { - assertThat(awaitItem().value).isEqualTo(roomMember2) - } - } - - @Test - fun `getDirectRoomMember emit null if the room is not a dm`() = runTest { - val matrixRoom = FakeMatrixRoom( - sessionId = A_USER_ID, - isEncrypted = true, - isDirect = false, - ) - moleculeFlow(RecompositionMode.Immediate) { - matrixRoom.getDirectRoomMember( - MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2)) - ) - }.test { - assertThat(awaitItem().value).isNull() - } - } - - @Test - fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest { - val matrixRoom = FakeMatrixRoom( - sessionId = A_USER_ID, - isEncrypted = false, - isDirect = true, - ) - moleculeFlow(RecompositionMode.Immediate) { - matrixRoom.getDirectRoomMember( - MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2)) - ) - }.test { - assertThat(awaitItem().value).isEqualTo(roomMember2) - } - } - - @Test - fun `getDirectRoomMember emit null if the room has only 1 member`() = runTest { - val matrixRoom = FakeMatrixRoom( - sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, - ) - moleculeFlow(RecompositionMode.Immediate) { - matrixRoom.getDirectRoomMember( - MatrixRoomMembersState.Ready(persistentListOf(roomMember1)) - ) - }.test { - assertThat(awaitItem().value).isNull() - } - } - - @Test - fun `getDirectRoomMember emit null if the room has only 3 members`() = runTest { - val matrixRoom = FakeMatrixRoom( - sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, - ) - moleculeFlow(RecompositionMode.Immediate) { - matrixRoom.getDirectRoomMember( - MatrixRoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3)) - ) - }.test { - assertThat(awaitItem().value).isNull() - } - } - - @Test - fun `getDirectRoomMember emit null if the other member is not active`() = runTest { - val matrixRoom = FakeMatrixRoom( - sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, - ) - moleculeFlow(RecompositionMode.Immediate) { - matrixRoom.getDirectRoomMember( - MatrixRoomMembersState.Ready( - persistentListOf( - roomMember1, - roomMember2.copy(membership = RoomMembershipState.BAN), - ) - ) - ) - }.test { - assertThat(awaitItem().value).isNull() - } - } - - @Test - fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest { - val matrixRoom = FakeMatrixRoom( - sessionId = A_USER_ID, - isEncrypted = true, - isDirect = true, - ) - moleculeFlow(RecompositionMode.Immediate) { - matrixRoom.getDirectRoomMember( - MatrixRoomMembersState.Ready( - persistentListOf( - roomMember1, - roomMember2, - roomMember3.copy(membership = RoomMembershipState.BAN), - ) - ) - ) - }.test { - assertThat(awaitItem().value).isEqualTo(roomMember2) - } - } - - @Test - fun `getCurrentRoomMember returns the current user`() = runTest { - val matrixRoom = FakeMatrixRoom(sessionId = A_USER_ID) - moleculeFlow(RecompositionMode.Immediate) { - matrixRoom.getCurrentRoomMember( - MatrixRoomMembersState.Ready( - persistentListOf( - roomMember1, - roomMember2, - roomMember3, - ) - ) - ) - }.test { - assertThat(awaitItem().value).isEqualTo(roomMember1) - } - } - - @Test - fun `getCurrentRoomMember returns null if the member is not found`() = runTest { - val matrixRoom = FakeMatrixRoom(sessionId = A_USER_ID) - moleculeFlow(RecompositionMode.Immediate) { - matrixRoom.getCurrentRoomMember( - MatrixRoomMembersState.Ready( - persistentListOf( - roomMember2, - roomMember3, - ) - ) - ) - }.test { - assertThat(awaitItem().value).isNull() - } - } -} diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChangeTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChangeTest.kt new file mode 100644 index 00000000000..8abf5092e66 --- /dev/null +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChangeTest.kt @@ -0,0 +1,403 @@ +/* + * 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.matrix.ui.room + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange +import io.element.android.libraries.matrix.api.room.RoomMembersState +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_ID_3 +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.aRoomMember +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ObserveRoomMemberIdentityStateChangeTest { + private val aliceRoomMember = aRoomMember(A_USER_ID, displayName = "Alice") + private val bobRoomMember = aRoomMember(A_USER_ID_2, displayName = "Bob") + private val carolRoomMember = aRoomMember(A_USER_ID_3, displayName = "Carol") + + @Test + fun `roomMemberIdentityStateChange emits empty list for non-encrypted room with no identity changes`() = + runTest { + val identityStateChangesFlow = MutableStateFlow>(emptyList()) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = false)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val result = awaitItem() + assertThat(result).isEmpty() + } + } + + @Test + fun `roomMemberIdentityStateChange emits identity changes for non-encrypted room when waitForEncryption is false`() = + runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.Verified), + IdentityStateChange(carolRoomMember.userId, IdentityState.PinViolation) + ) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = false)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember, + carolRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val result = awaitItem() + assertThat(result).hasSize(2) + + val bobChange = result.find { it.identityRoomMember.userId == bobRoomMember.userId } + assertThat(bobChange).isNotNull() + assertThat(bobChange?.identityState).isEqualTo(IdentityState.Verified) + assertThat(bobChange?.identityRoomMember?.displayNameOrDefault).isEqualTo("Bob") + + val carolChange = result.find { it.identityRoomMember.userId == carolRoomMember.userId } + assertThat(carolChange).isNotNull() + assertThat(carolChange?.identityState).isEqualTo(IdentityState.PinViolation) + assertThat(carolChange?.identityRoomMember?.displayNameOrDefault).isEqualTo("Carol") + } + } + + @Test + fun `roomMemberIdentityStateChange emits identity changes for already encrypted room`() = + runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.VerificationViolation) + ) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = true).test { + val result = awaitItem() + assertThat(result).hasSize(1) + + val bobChange = result.first() + assertThat(bobChange.identityRoomMember.userId).isEqualTo(bobRoomMember.userId) + assertThat(bobChange.identityState).isEqualTo(IdentityState.VerificationViolation) + assertThat(bobChange.identityRoomMember.displayNameOrDefault).isEqualTo("Bob") + } + } + + @Test + fun `roomMemberIdentityStateChange waits for encryption before emitting when waitForEncryption is true`() = + runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned)) + ) + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = false)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = true).test { + // Should not emit anything yet since room is not encrypted + expectNoEvents() + + // Enable encryption + joinedRoom.baseRoom.givenRoomInfo(aRoomInfo(isEncrypted = true)) + + val result = awaitItem() + assertThat(result).hasSize(1) + assertThat(result.first().identityRoomMember.userId).isEqualTo(bobRoomMember.userId) + assertThat(result.first().identityState).isEqualTo(IdentityState.Pinned) + } + } + + @Test + fun `roomMemberIdentityStateChange creates default member when room member not found`() = + runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(carolRoomMember.userId, IdentityState.PinViolation)) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + // Only include aliceRoomMember and bobRoomMember, not carolRoomMember + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val result = awaitItem() + assertThat(result).hasSize(1) + + val carolChange = result.first() + assertThat(carolChange.identityRoomMember.userId).isEqualTo(carolRoomMember.userId) + assertThat(carolChange.identityState).isEqualTo(IdentityState.PinViolation) + // Should use extracted display name from user ID since member not found + assertThat(carolChange.identityRoomMember.displayNameOrDefault).isEqualTo( + carolRoomMember.userId.extractedDisplayName + ) + } + } + + @Test + fun `roomMemberIdentityStateChange updates when identity state changes`() = runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned)) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val firstResult = awaitItem() + assertThat(firstResult).hasSize(1) + assertThat(firstResult.first().identityState).isEqualTo(IdentityState.Pinned) + + // Update identity state + identityStateChangesFlow.value = listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.VerificationViolation) + ) + + val secondResult = awaitItem() + assertThat(secondResult).hasSize(1) + assertThat(secondResult.first().identityState).isEqualTo(IdentityState.VerificationViolation) + } + } + + @Test + fun `roomMemberIdentityStateChange updates when members state changes`() = runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Verified)) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val firstResult = awaitItem() + assertThat(firstResult).hasSize(1) + assertThat(firstResult.first().identityRoomMember.displayNameOrDefault).isEqualTo("Bob") + + // Update room member with different display name + val updatedMember2 = bobRoomMember.copy(displayName = "Bobby") + joinedRoom.baseRoom.givenRoomMembersState( + RoomMembersState.Ready(persistentListOf(aliceRoomMember, updatedMember2)) + ) + + val secondResult = awaitItem() + assertThat(secondResult).hasSize(1) + assertThat(secondResult.first().identityRoomMember.displayNameOrDefault).isEqualTo("Bobby") + } + } + + @Test + fun `roomMemberIdentityStateChange handles multiple identity states`() = runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf( + IdentityStateChange(aliceRoomMember.userId, IdentityState.Verified), + IdentityStateChange(bobRoomMember.userId, IdentityState.PinViolation), + IdentityStateChange(carolRoomMember.userId, IdentityState.VerificationViolation) + ) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember, + carolRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val result = awaitItem() + assertThat(result).hasSize(3) + + val verifiedUser = result.find { it.identityState == IdentityState.Verified } + assertThat(verifiedUser?.identityRoomMember?.userId).isEqualTo(aliceRoomMember.userId) + + val pinViolationUser = result.find { it.identityState == IdentityState.PinViolation } + assertThat(pinViolationUser?.identityRoomMember?.userId).isEqualTo(bobRoomMember.userId) + + val verificationViolationUser = + result.find { it.identityState == IdentityState.VerificationViolation } + assertThat(verificationViolationUser?.identityRoomMember?.userId).isEqualTo(carolRoomMember.userId) + } + } + + @Test + fun `roomMemberIdentityStateChange handles room becoming encrypted scenario`() = runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned)) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = false)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = true).test { + // Should not emit anything initially as room is not encrypted + expectNoEvents() + + // Room becomes encrypted + joinedRoom.baseRoom.givenRoomInfo(aRoomInfo(isEncrypted = true)) + + val result = awaitItem() + assertThat(result).hasSize(1) + assertThat(result.first().identityRoomMember.userId).isEqualTo(bobRoomMember.userId) + assertThat(result.first().identityState).isEqualTo(IdentityState.Pinned) + + // Add more identity changes after encryption is enabled + identityStateChangesFlow.value = listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned), + IdentityStateChange(aliceRoomMember.userId, IdentityState.VerificationViolation) + ) + + val updatedResult = awaitItem() + assertThat(updatedResult).hasSize(2) + } + } + + @Test + fun `roomMemberIdentityStateChange does not emit duplicates for same state`() = runTest { + val identityStateChangesFlow = MutableSharedFlow>() + val identityStateChanges = listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.Verified) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + identityStateChangesFlow.emit(identityStateChanges) + + val firstResult = awaitItem() + assertThat(firstResult).hasSize(1) + + // Emit the same state again + identityStateChangesFlow.emit(identityStateChanges) + + // Should not emit a new item due to distinctUntilChanged + expectNoEvents() + } + } +} 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 new file mode 100644 index 00000000000..87deb9c3232 --- /dev/null +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/RoomMembersTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright 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. + */ + +package io.element.android.libraries.matrix.ui.room + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.RoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembershipState +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_ID_3 +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.aRoomMember +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RoomMembersTest { + private val roomMember1 = aRoomMember(A_USER_ID) + private val roomMember2 = aRoomMember(A_USER_ID_2) + private val roomMember3 = aRoomMember(A_USER_ID_3) + + @Test + fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest { + val joinedRoom = FakeBaseRoom( + sessionId = A_USER_ID, + initialRoomInfo = aRoomInfo( + isDirect = true, + joinedMembersCount = 2, + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + joinedRoom.getDirectRoomMember( + RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2)) + ) + }.test { + assertThat(awaitItem().value).isEqualTo(roomMember2) + } + } + + @Test + fun `getDirectRoomMember emit null if the room is not a dm`() = runTest { + val joinedRoom = FakeBaseRoom( + sessionId = A_USER_ID, + initialRoomInfo = aRoomInfo(isDirect = false) + ) + moleculeFlow(RecompositionMode.Immediate) { + joinedRoom.getDirectRoomMember( + RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2)) + ) + }.test { + assertThat(awaitItem().value).isNull() + } + } + + @Test + fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest { + val joinedRoom = FakeBaseRoom( + sessionId = A_USER_ID, + initialRoomInfo = aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + joinedRoom.getDirectRoomMember( + RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2)) + ) + }.test { + assertThat(awaitItem().value).isEqualTo(roomMember2) + } + } + + @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)) + } + moleculeFlow(RecompositionMode.Immediate) { + joinedRoom.getDirectRoomMember( + RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3)) + ) + }.test { + assertThat(awaitItem().value).isNull() + } + } + + @Test + fun `getDirectRoomMember emit null if the other member is not active`() = runTest { + val joinedRoom = FakeBaseRoom( + sessionId = A_USER_ID, + initialRoomInfo = aRoomInfo(isDirect = true), + ) + moleculeFlow(RecompositionMode.Immediate) { + joinedRoom.getDirectRoomMember( + RoomMembersState.Ready( + persistentListOf( + roomMember1, + roomMember2.copy(membership = RoomMembershipState.BAN), + ) + ) + ) + }.test { + assertThat(awaitItem().value).isNull() + } + } + + @Test + fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest { + val joinedRoom = FakeBaseRoom( + sessionId = A_USER_ID, + initialRoomInfo = aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + joinedRoom.getDirectRoomMember( + RoomMembersState.Ready( + persistentListOf( + roomMember1, + roomMember2, + roomMember3.copy(membership = RoomMembershipState.BAN), + ) + ) + ) + }.test { + assertThat(awaitItem().value).isEqualTo(roomMember2) + } + } + + @Test + fun `getCurrentRoomMember returns the current user`() = runTest { + val joinedRoom = FakeBaseRoom(sessionId = A_USER_ID) + moleculeFlow(RecompositionMode.Immediate) { + joinedRoom.getCurrentRoomMember( + RoomMembersState.Ready( + persistentListOf( + roomMember1, + roomMember2, + roomMember3, + ) + ) + ) + }.test { + assertThat(awaitItem().value).isEqualTo(roomMember1) + } + } + + @Test + fun `getCurrentRoomMember returns null if the member is not found`() = runTest { + val joinedRoom = FakeBaseRoom(sessionId = A_USER_ID) + moleculeFlow(RecompositionMode.Immediate) { + joinedRoom.getCurrentRoomMember( + RoomMembersState.Ready( + persistentListOf( + roomMember2, + roomMember3, + ) + ) + ) + }.test { + assertThat(awaitItem().value).isNull() + } + } +} diff --git a/libraries/mediaplayer/impl/build.gradle.kts b/libraries/mediaplayer/impl/build.gradle.kts index 7b6e8e3d6e7..6bd19680a36 100644 --- a/libraries/mediaplayer/impl/build.gradle.kts +++ b/libraries/mediaplayer/impl/build.gradle.kts @@ -21,11 +21,14 @@ dependencies { implementation(libs.androidx.media3.exoplayer) implementation(libs.dagger) + implementation(projects.libraries.audio.api) + implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(libs.coroutines.core) testImplementation(projects.tests.testutils) + testImplementation(projects.libraries.audio.test) testImplementation(libs.test.junit) testImplementation(libs.test.truth) testImplementation(libs.test.mockk) diff --git a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt index 8c62b474682..8e9027dfb07 100644 --- a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt @@ -11,8 +11,11 @@ import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import com.squareup.anvil.annotations.ContributesBinding +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.SingleIn +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.mediaplayer.api.MediaPlayer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview @@ -35,7 +38,9 @@ import kotlin.time.Duration.Companion.seconds @SingleIn(RoomScope::class) class DefaultMediaPlayer @Inject constructor( private val player: SimplePlayer, - private val coroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, + private val audioFocus: AudioFocus, ) : MediaPlayer { private val listener = object : SimplePlayer.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { @@ -48,8 +53,9 @@ class DefaultMediaPlayer @Inject constructor( ) } if (isPlaying) { - job = coroutineScope.launch { updateCurrentPosition() } + job = sessionCoroutineScope.launch { updateCurrentPosition() } } else { + audioFocus.releaseAudioFocus() job?.cancel() } } @@ -122,6 +128,14 @@ class DefaultMediaPlayer @Inject constructor( } override fun play() { + audioFocus.requestAudioFocus( + requester = AudioFocusRequester.VoiceMessage, + onFocusLost = { + if (player.isPlaying()) { + player.pause() + } + }, + ) if (player.playbackState == Player.STATE_ENDED) { // There's a bug with some ogg files that somehow report to // have no duration. diff --git a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt index 43fd771a562..0800f7046c0 100644 --- a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt @@ -33,6 +33,7 @@ interface SimplePlayer { fun getCurrentMediaItem(): MediaItem? fun prepare() fun play() + fun isPlaying(): Boolean fun pause() fun seekTo(positionMs: Long) fun release() @@ -89,6 +90,8 @@ class DefaultSimplePlayer( override fun play() = p.play() + override fun isPlaying() = p.isPlaying + override fun pause() = p.pause() override fun seekTo(positionMs: Long) = p.seekTo(positionMs) diff --git a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt index d013a22a654..a91a2043784 100644 --- a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt +++ b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt @@ -11,7 +11,10 @@ import androidx.media3.common.MediaItem import androidx.media3.common.Player import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.audio.api.AudioFocus +import io.element.android.libraries.audio.api.AudioFocusRequester import io.element.android.libraries.mediaplayer.api.MediaPlayer +import io.element.android.libraries.mediaplayer.test.FakeAudioFocus import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.TimeoutCancellationException @@ -50,8 +53,15 @@ class DefaultMediaPlayerTest { playLambda = playLambda, pauseLambda = pauseLambda, ) + val requestAudioFocusResult = lambdaRecorder Unit, Unit> { _, _ -> } + val releaseAudioFocusResult = lambdaRecorder {} + val audioFocus = FakeAudioFocus( + requestAudioFocusResult = requestAudioFocusResult, + releaseAudioFocusResult = releaseAudioFocusResult + ) val sut = createDefaultMediaPlayer( simplePlayer = player, + audioFocus = audioFocus, ) sut.state.test { val initialState = awaitItem() @@ -67,6 +77,7 @@ class DefaultMediaPlayerTest { ) sut.play() playLambda.assertions().isCalledOnce() + requestAudioFocusResult.assertions().isCalledOnce() player.durationResult = 123L player.simulateIsPlayingChanged(true) val playingState = awaitItem() @@ -105,6 +116,7 @@ class DefaultMediaPlayerTest { player.pause() pauseLambda.assertions().isCalledOnce() player.simulateIsPlayingChanged(false) + releaseAudioFocusResult.assertions().isCalledOnce() assertThat(awaitItem()).isEqualTo( MediaPlayer.State( isReady = false, @@ -126,8 +138,13 @@ class DefaultMediaPlayerTest { playLambda = playLambda, getCurrentMediaItemLambda = getCurrentMediaItemLambda, ) + val requestAudioFocusResult = lambdaRecorder Unit, Unit> { _, _ -> } + val audioFocus = FakeAudioFocus( + requestAudioFocusResult = requestAudioFocusResult, + ) val sut = createDefaultMediaPlayer( simplePlayer = player, + audioFocus = audioFocus, ) sut.state.test { val initialState = awaitItem() @@ -144,6 +161,7 @@ class DefaultMediaPlayerTest { player.playbackStateResult = Player.STATE_ENDED sut.play() playLambda.assertions().isCalledOnce() + requestAudioFocusResult.assertions().isCalledOnce() } } @@ -153,6 +171,10 @@ class DefaultMediaPlayerTest { val prepareLambda = lambdaRecorder { } val getCurrentMediaItemLambda = lambdaRecorder { aMediaItem } val setMediaItemLambda = lambdaRecorder { _, _ -> } + val requestAudioFocusResult = lambdaRecorder Unit, Unit> { _, _ -> } + val audioFocus = FakeAudioFocus( + requestAudioFocusResult = requestAudioFocusResult, + ) val player = FakeSimplePlayer( playLambda = playLambda, prepareLambda = prepareLambda, @@ -161,6 +183,7 @@ class DefaultMediaPlayerTest { ) val sut = createDefaultMediaPlayer( simplePlayer = player, + audioFocus = audioFocus, ) sut.state.test { val initialState = awaitItem() @@ -182,6 +205,7 @@ class DefaultMediaPlayerTest { ) prepareLambda.assertions().isCalledOnce() playLambda.assertions().isCalledOnce() + requestAudioFocusResult.assertions().isCalledOnce() } } @@ -313,6 +337,7 @@ class DefaultMediaPlayerTest { duration = null, ) ) + @Suppress("RunCatchingNotAllowed") val result = runCatching { sut.setMedia("uri", "mediaId", "mimeType", 12) } @@ -395,8 +420,10 @@ class DefaultMediaPlayerTest { private fun TestScope.createDefaultMediaPlayer( simplePlayer: SimplePlayer = FakeSimplePlayer(), + audioFocus: AudioFocus = FakeAudioFocus(), ): DefaultMediaPlayer = DefaultMediaPlayer( - simplePlayer, - backgroundScope, + player = simplePlayer, + sessionCoroutineScope = backgroundScope, + audioFocus = audioFocus, ) } diff --git a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt index 76e1eec34d1..94c880a862c 100644 --- a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt +++ b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt @@ -16,6 +16,7 @@ class FakeSimplePlayer( private val getCurrentMediaItemLambda: () -> MediaItem? = { lambdaError() }, private val prepareLambda: () -> Unit = { lambdaError() }, private val playLambda: () -> Unit = { lambdaError() }, + private val isPlayingLambda: () -> Boolean = { lambdaError() }, private val pauseLambda: () -> Unit = { lambdaError() }, private val seekToLambda: (Long) -> Unit = { lambdaError() }, private val releaseLambda: () -> Unit = { lambdaError() }, @@ -40,6 +41,7 @@ class FakeSimplePlayer( override fun getCurrentMediaItem(): MediaItem? = getCurrentMediaItemLambda() override fun prepare() = prepareLambda() override fun play() = playLambda() + override fun isPlaying() = isPlayingLambda() override fun pause() = pauseLambda() override fun seekTo(positionMs: Long) = seekToLambda(positionMs) override fun release() = releaseLambda() diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 584539eacc5..5f542bd91a1 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -11,7 +11,9 @@ import android.net.Uri import io.element.android.libraries.core.extensions.flatMapCatching import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.media.MediaUploadHandler -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.message.ReplyParameters +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job @@ -21,7 +23,7 @@ import javax.inject.Inject class MediaSender @Inject constructor( private val preProcessor: MediaPreProcessor, - private val room: MatrixRoom, + private val room: JoinedRoom, private val sessionPreferencesStore: SessionPreferencesStore, ) { private val ongoingUploadJobs = ConcurrentHashMap() @@ -46,12 +48,14 @@ class MediaSender @Inject constructor( caption: String?, formattedCaption: String?, progressCallback: ProgressCallback?, + replyParameters: ReplyParameters?, ): Result { - return room.sendMedia( + return room.liveTimeline.sendMedia( uploadInfo = mediaUploadInfo, progressCallback = progressCallback, caption = caption, - formattedCaption = formattedCaption + formattedCaption = formattedCaption, + replyParameters = replyParameters, ) .handleSendResult() } @@ -61,7 +65,8 @@ class MediaSender @Inject constructor( mimeType: String, caption: String? = null, formattedCaption: String? = null, - progressCallback: ProgressCallback? = null + progressCallback: ProgressCallback? = null, + replyParameters: ReplyParameters? = null, ): Result { val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first() return preProcessor @@ -72,11 +77,12 @@ class MediaSender @Inject constructor( compressIfPossible = compressIfPossible, ) .flatMapCatching { info -> - room.sendMedia( + room.liveTimeline.sendMedia( uploadInfo = info, progressCallback = progressCallback, caption = caption, - formattedCaption = formattedCaption + formattedCaption = formattedCaption, + replyParameters = replyParameters, ) } .handleSendResult() @@ -86,7 +92,8 @@ class MediaSender @Inject constructor( uri: Uri, mimeType: String, waveForm: List, - progressCallback: ProgressCallback? = null + progressCallback: ProgressCallback? = null, + replyParameters: ReplyParameters? = null, ): Result { return preProcessor .process( @@ -102,11 +109,12 @@ class MediaSender @Inject constructor( audioInfo = audioInfo, waveform = waveForm, ) - room.sendMedia( + room.liveTimeline.sendMedia( uploadInfo = newInfo, progressCallback = progressCallback, caption = null, - formattedCaption = null + formattedCaption = null, + replyParameters = replyParameters, ) } .handleSendResult() @@ -123,11 +131,12 @@ class MediaSender @Inject constructor( ongoingUploadJobs.remove(Job) } - private suspend fun MatrixRoom.sendMedia( + private suspend fun Timeline.sendMedia( uploadInfo: MediaUploadInfo, progressCallback: ProgressCallback?, caption: String?, formattedCaption: String?, + replyParameters: ReplyParameters?, ): Result { val handler = when (uploadInfo) { is MediaUploadInfo.Image -> { @@ -137,7 +146,8 @@ class MediaSender @Inject constructor( imageInfo = uploadInfo.imageInfo, caption = caption, formattedCaption = formattedCaption, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } is MediaUploadInfo.Video -> { @@ -147,7 +157,8 @@ class MediaSender @Inject constructor( videoInfo = uploadInfo.videoInfo, caption = caption, formattedCaption = formattedCaption, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } is MediaUploadInfo.Audio -> { @@ -156,7 +167,8 @@ class MediaSender @Inject constructor( audioInfo = uploadInfo.audioInfo, caption = caption, formattedCaption = formattedCaption, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } is MediaUploadInfo.VoiceMessage -> { @@ -164,7 +176,8 @@ class MediaSender @Inject constructor( file = uploadInfo.file, audioInfo = uploadInfo.audioInfo, waveform = uploadInfo.waveform, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } is MediaUploadInfo.AnyFile -> { @@ -173,13 +186,16 @@ class MediaSender @Inject constructor( fileInfo = uploadInfo.fileInfo, caption = caption, formattedCaption = formattedCaption, - progressCallback = progressCallback + progressCallback = progressCallback, + replyParameters = replyParameters, ) } } + // We handle the cancellations here manually, so we suppress the warning + @Suppress("RunCatchingNotAllowed") return handler - .flatMapCatching { uploadHandler -> + .mapCatching { uploadHandler -> ongoingUploadJobs[Job] = uploadHandler uploadHandler.await() } diff --git a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt index 48640da8ea4..7b10e84816f 100644 --- a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt +++ b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt @@ -13,9 +13,11 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.core.ProgressCallback 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.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore @@ -35,7 +37,24 @@ class MediaSenderTest { @Test fun `given an attachment when sending it the preprocessor always runs`() = runTest { val preProcessor = FakeMediaPreProcessor() - val sender = createMediaSender(preProcessor) + val sender = createMediaSender( + preProcessor = preProcessor, + room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendFileLambda = lambdaRecorder< + File, + FileInfo, + String?, + String?, + ProgressCallback?, + ReplyParameters?, + Result, + > { _, _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + }, + ) + ) val uri = Uri.parse("content://image.jpg") sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg) @@ -44,13 +63,15 @@ class MediaSenderTest { } @Test - fun `given an attachment when sending it the MatrixRoom will call sendMedia`() = runTest { + fun `given an attachment when sending it the Room will call sendMedia`() = runTest { val sendImageResult = - lambdaRecorder> { _, _, _, _, _, _ -> + lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? -> Result.success(FakeMediaUploadHandler()) } - val room = FakeMatrixRoom( - sendImageResult = sendImageResult + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendImageLambda = sendImageResult + }, ) val sender = createMediaSender(room = room) @@ -73,14 +94,22 @@ class MediaSenderTest { @Test fun `given a failure in the media upload when sending the whole process fails`() = runTest { + val preProcessor = FakeMediaPreProcessor().apply { + givenImageResult() + } val sendImageResult = - lambdaRecorder> { _, _, _, _, _, _ -> - Result.failure(Exception()) + lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? -> + Result.failure(Exception()) } - val room = FakeMatrixRoom( - sendImageResult = sendImageResult + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendImageLambda = sendImageResult + }, + ) + val sender = createMediaSender( + preProcessor = preProcessor, + room = room, ) - val sender = createMediaSender(room = room) val uri = Uri.parse("content://image.jpg") val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg) @@ -91,11 +120,14 @@ class MediaSenderTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `given a cancellation in the media upload when sending the job is cancelled`() = runTest(StandardTestDispatcher()) { - val sendFileResult = lambdaRecorder> { _, _, _, _, _ -> - Result.success(FakeMediaUploadHandler()) - } - val room = FakeMatrixRoom( - sendFileResult = sendFileResult + val sendFileResult = + lambdaRecorder> { _, _, _, _, _, _ -> + Result.success(FakeMediaUploadHandler()) + } + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, ) val sender = createMediaSender(room = room) val sendJob = launch { @@ -121,7 +153,7 @@ class MediaSenderTest { private fun createMediaSender( preProcessor: MediaPreProcessor = FakeMediaPreProcessor(), - room: MatrixRoom = FakeMatrixRoom(), + room: JoinedRoom = FakeJoinedRoom(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), ) = MediaSender( preProcessor = preProcessor, diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt index 3fb96a75b35..6363fa06c95 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage @@ -62,7 +63,7 @@ class AndroidMediaPreProcessor @Inject constructor( */ private const val IMAGE_SCALE_REF_SIZE = 640 - private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP) + private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP, MimeTypes.Svg) } private val contentResolver = context.contentResolver @@ -73,8 +74,12 @@ class AndroidMediaPreProcessor @Inject constructor( deleteOriginal: Boolean, compressIfPossible: Boolean, ): Result = withContext(coroutineDispatchers.computation) { - runCatching { + runCatchingExceptions { val result = when { + // Special case for SVG, since Android can't read its metadata or create a thumbnail, it must be sent as a file + mimeType == MimeTypes.Svg -> { + processFile(uri, mimeType) + } mimeType.isMimeTypeImage() -> { val shouldBeCompressed = compressIfPossible && mimeType !in notCompressibleImageTypes processImage(uri, mimeType, shouldBeCompressed) @@ -184,20 +189,29 @@ class AndroidMediaPreProcessor @Inject constructor( } private suspend fun processVideo(uri: Uri, mimeType: String?, shouldBeCompressed: Boolean): MediaUploadInfo { - val resultFile = videoCompressor.compress(uri, shouldBeCompressed) - .onEach { - // TODO handle progress - } - .filterIsInstance() - .first() - .file - val thumbnailInfo = thumbnailFactory.createVideoThumbnail(resultFile) - val videoInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo) - return MediaUploadInfo.Video( - file = resultFile, - videoInfo = videoInfo, - thumbnailFile = thumbnailInfo?.file - ) + val resultFile = runCatchingExceptions { + videoCompressor.compress(uri, shouldBeCompressed) + .onEach { + // TODO handle progress + } + .filterIsInstance() + .first() + .file + } + .getOrNull() + + if (resultFile != null) { + val thumbnailInfo = thumbnailFactory.createVideoThumbnail(resultFile) + val videoInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo) + return MediaUploadInfo.Video( + file = resultFile, + videoInfo = videoInfo, + thumbnailFile = thumbnailInfo?.file + ) + } else { + // If the video could not be compressed, just use the original one, but send it as a file + return processFile(uri, MimeTypes.OctetStream) + } } private suspend fun processAudio(uri: Uri, mimeType: String?): MediaUploadInfo { diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt index 2d66a68b06f..f14264c5d77 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.androidutils.bitmap.resizeToMax import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation 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.di.ApplicationContext import kotlinx.coroutines.withContext import java.io.File @@ -38,7 +39,7 @@ class ImageCompressor @Inject constructor( orientation: Int = ExifInterface.ORIENTATION_UNDEFINED, desiredQuality: Int = 78, ): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { val format = mimeTypeToCompressFormat(mimeType) val extension = mimeTypeToCompressFileExtension(mimeType) val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow() @@ -65,7 +66,7 @@ class ImageCompressor @Inject constructor( inputStreamProvider: () -> InputStream, resizeMode: ResizeMode, orientation: Int, - ): Result = runCatching { + ): Result = runCatchingExceptions { val options = BitmapFactory.Options() // Decode bounds inputStreamProvider().use { input -> diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt index a61dbe9568e..9f36b49bc06 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt @@ -8,39 +8,50 @@ package io.element.android.libraries.mediaupload.impl import android.content.Context +import android.media.MediaMetadataRetriever import android.net.Uri +import android.webkit.MimeTypeMap import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.TranscoderListener +import com.otaliastudios.transcoder.internal.media.MediaFormatConstants import com.otaliastudios.transcoder.resize.AtMostResizer import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy +import com.otaliastudios.transcoder.strategy.PassThroughTrackStrategy +import com.otaliastudios.transcoder.strategy.TrackStrategy +import com.otaliastudios.transcoder.validator.WriteAlwaysValidator import io.element.android.libraries.androidutils.file.createTmpFile +import io.element.android.libraries.androidutils.file.getMimeType import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow +import timber.log.Timber import java.io.File import javax.inject.Inject +private const val MP4_EXTENSION = "mp4" + class VideoCompressor @Inject constructor( @ApplicationContext private val context: Context, ) { fun compress(uri: Uri, shouldBeCompressed: Boolean) = callbackFlow { - val tmpFile = context.createTmpFile(extension = "mp4") + val metadata = getVideoMetadata(uri) + + val expectedExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(context.getMimeType(uri)) + + val videoStrategy = VideoStrategyFactory.create( + expectedExtension = expectedExtension, + metadata = metadata, + shouldBeCompressed = shouldBeCompressed + ) + + val tmpFile = context.createTmpFile(extension = MP4_EXTENSION) val future = Transcoder.into(tmpFile.path) - .setVideoTrackStrategy( - DefaultVideoStrategy.Builder() - .addResizer( - AtMostResizer( - if (shouldBeCompressed) { - 720 - } else { - 1080 - } - ) - ) - .build() - ) + .setVideoTrackStrategy(videoStrategy) .addDataSource(context, uri) + // Force the output to be written, even if no transcoding was actually needed + .setValidator(WriteAlwaysValidator()) .setListener(object : TranscoderListener { override fun onTranscodeProgress(progress: Double) { trySend(VideoTranscodingEvent.Progress(progress.toFloat())) @@ -69,9 +80,86 @@ class VideoCompressor @Inject constructor( } } } + + private fun getVideoMetadata(uri: Uri): VideoFileMetadata? { + return runCatchingExceptions { + MediaMetadataRetriever().use { + it.setDataSource(context, uri) + + val width = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: -1 + val height = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: -1 + val bitrate = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toLongOrNull() ?: -1 + val framerate = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)?.toIntOrNull() ?: -1 + + val (actualWidth, actualHeight) = if (width == -1 || height == -1) { + // Try getting the first frame instead + val bitmap = it.getFrameAtTime(0) ?: return null + bitmap.width to bitmap.height + } else { + width to height + } + + VideoFileMetadata( + width = actualWidth, + height = actualHeight, + bitrate = bitrate, + frameRate = framerate + ) + } + }.onFailure { + Timber.e(it, "Failed to get video dimensions") + }.getOrNull() + } } +internal data class VideoFileMetadata( + val width: Int?, + val height: Int?, + val bitrate: Long?, + val frameRate: Int?, +) + sealed interface VideoTranscodingEvent { data class Progress(val value: Float) : VideoTranscodingEvent data class Completed(val file: File) : VideoTranscodingEvent } + +internal object VideoStrategyFactory { + // 720p + private const val MAX_COMPRESSED_PIXEL_SIZE = 1280 + + // 1080p + private const val MAX_PIXEL_SIZE = 1920 + + fun create( + expectedExtension: String?, + metadata: VideoFileMetadata?, + shouldBeCompressed: Boolean, + ): TrackStrategy { + val width = metadata?.width ?: Int.MAX_VALUE + val height = metadata?.height ?: Int.MAX_VALUE + val bitrate = metadata?.bitrate + val frameRate = metadata?.frameRate + + // We only create a resizer if needed + val resizer = when { + shouldBeCompressed && (width > MAX_COMPRESSED_PIXEL_SIZE || height > MAX_COMPRESSED_PIXEL_SIZE) -> AtMostResizer(MAX_COMPRESSED_PIXEL_SIZE) + width > MAX_PIXEL_SIZE || height > MAX_PIXEL_SIZE -> AtMostResizer(MAX_PIXEL_SIZE) + else -> null + } + + return if (resizer == null && expectedExtension == MP4_EXTENSION) { + // If there's no transcoding or resizing needed for the video file, just create a new file with the same contents but no metadata + PassThroughTrackStrategy() + } else { + DefaultVideoStrategy.Builder() + .apply { + resizer?.let { addResizer(it) } + bitrate?.let { bitRate(it) } + frameRate?.let { frameRate(it) } + } + .mimeType(MediaFormatConstants.MIMETYPE_VIDEO_AVC) + .build() + } + } +} diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt index 7aa8c1062fd..7f2f8b0ff60 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt @@ -66,6 +66,7 @@ class AndroidMediaPreProcessorTest { } @Test + @Ignore("Ignore now that min API for enterprise is 33") fun `test processing png`() = runTest { val mediaUploadInfo = process( asset = assetImagePng, @@ -109,6 +110,7 @@ class AndroidMediaPreProcessorTest { } @Test + @Ignore("Ignore now that min API for enterprise is 33") fun `test processing png no compression`() = runTest { val mediaUploadInfo = process( asset = assetImagePng, @@ -130,6 +132,7 @@ class AndroidMediaPreProcessorTest { } @Test + @Ignore("Ignore now that min API for enterprise is 33") fun `test processing png and delete`() = runTest { val mediaUploadInfo = process( asset = assetImagePng, @@ -154,6 +157,7 @@ class AndroidMediaPreProcessorTest { } @Test + @Ignore("Ignore now that min API for enterprise is 33") fun `test processing jpeg`() = runTest { val mediaUploadInfo = process( asset = assetImageJpeg, @@ -197,6 +201,7 @@ class AndroidMediaPreProcessorTest { } @Test + @Ignore("Ignore now that min API for enterprise is 33") fun `test processing jpeg no compression`() = runTest { val mediaUploadInfo = process( asset = assetImageJpeg, @@ -218,6 +223,7 @@ class AndroidMediaPreProcessorTest { } @Test + @Ignore("Ignore now that min API for enterprise is 33") fun `test processing jpeg and delete`() = runTest { val mediaUploadInfo = process( asset = assetImageJpeg, @@ -242,6 +248,7 @@ class AndroidMediaPreProcessorTest { } @Test + @Ignore("Ignore now that min API for enterprise is 33") fun `test processing gif`() = runTest { val mediaUploadInfo = process( asset = assetAnimatedGif, diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoStrategyFactoryTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoStrategyFactoryTest.kt new file mode 100644 index 00000000000..65bf12c22ac --- /dev/null +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoStrategyFactoryTest.kt @@ -0,0 +1,153 @@ +/* + * 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.mediaupload.impl + +import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy +import com.otaliastudios.transcoder.strategy.PassThroughTrackStrategy +import com.otaliastudios.transcoder.strategy.TrackStrategy +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@Suppress("NOTHING_TO_INLINE") +@RunWith(RobolectricTestRunner::class) +class VideoStrategyFactoryTest { + @Test + fun `if we don't have metadata the video will be transcoded just in case`() { + // Given + val expectedExtension = "mp4" + val metadata = null + val shouldBeCompressed = true + + // When + val videoStrategy = VideoStrategyFactory.create( + expectedExtension = expectedExtension, + metadata = metadata, + shouldBeCompressed = shouldBeCompressed + ) + + // Then + assertIsTranscoded(videoStrategy) + } + + @Test + fun `if the video should be compressed and is larger than 720p it will be transcoded`() { + // Given + val expectedExtension = "mp4" + val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50) + val shouldBeCompressed = true + + // When + val videoStrategy = VideoStrategyFactory.create( + expectedExtension = expectedExtension, + metadata = metadata, + shouldBeCompressed = shouldBeCompressed + ) + + // Then + assertIsTranscoded(videoStrategy) + } + + @Test + fun `if the video should be compressed, has the right format and is smaller or equal to 720p it will not be transcoded`() { + // Given + val expectedExtension = "mp4" + val metadata = VideoFileMetadata(width = 1280, height = 720, bitrate = 1_000_000, frameRate = 50) + val shouldBeCompressed = true + + // When + val videoStrategy = VideoStrategyFactory.create( + expectedExtension = expectedExtension, + metadata = metadata, + shouldBeCompressed = shouldBeCompressed + ) + + // Then + assertIsNotTranscoded(videoStrategy) + } + + @Test + fun `if the video should not be compressed and is larger than 1080p it will be transcoded`() { + // Given + val expectedExtension = "mp4" + val metadata = VideoFileMetadata(width = 2560, height = 1440, bitrate = 1_000_000, frameRate = 50) + val shouldBeCompressed = false + + // When + val videoStrategy = VideoStrategyFactory.create( + expectedExtension = expectedExtension, + metadata = metadata, + shouldBeCompressed = shouldBeCompressed + ) + + // Then + assertIsTranscoded(videoStrategy) + } + + @Test + fun `if the video should not be compressed, has the right format and is smaller or equal than 1080p it will not be transcoded`() { + // Given + val expectedExtension = "mp4" + val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50) + val shouldBeCompressed = false + + // When + val videoStrategy = VideoStrategyFactory.create( + expectedExtension = expectedExtension, + metadata = metadata, + shouldBeCompressed = shouldBeCompressed + ) + + // Then + assertIsNotTranscoded(videoStrategy) + } + + @Test + fun `if the video should not be compressed but has a wrong format it will be transcoded`() { + // Given + val expectedExtension = "mkv" + val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50) + val shouldBeCompressed = false + + // When + val videoStrategy = VideoStrategyFactory.create( + expectedExtension = expectedExtension, + metadata = metadata, + shouldBeCompressed = shouldBeCompressed + ) + + // Then + assertIsTranscoded(videoStrategy) + } + + @Test + fun `if the video should be compressed and has a wrong format it will be transcoded`() { + // Given + val expectedExtension = "mkv" + val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50) + val shouldBeCompressed = true + + // When + val videoStrategy = VideoStrategyFactory.create( + expectedExtension = expectedExtension, + metadata = metadata, + shouldBeCompressed = shouldBeCompressed + ) + + // Then + assertIsTranscoded(videoStrategy) + } + + private inline fun assertIsTranscoded(videoStrategy: TrackStrategy) { + assert(videoStrategy is DefaultVideoStrategy) + } + + private inline fun assertIsNotTranscoded(videoStrategy: TrackStrategy) { + assert(videoStrategy is PassThroughTrackStrategy) + } +} 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 7426251ca0f..f5440a48b6a 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 @@ -156,3 +156,24 @@ fun aVoiceMediaInfo( waveform = waveForm, duration = duration, ) + +fun aTxtMediaInfo( + filename: String = "a text file.txt", + caption: String? = null, + senderName: String? = null, + dateSent: String? = null, + dateSentFull: String? = null, +): MediaInfo = MediaInfo( + filename = filename, + caption = caption, + mimeType = MimeTypes.PlainText, + formattedFileSize = "2kB", + fileExtension = "txt", + senderId = UserId("@alice:server.org"), + senderName = senderName, + senderAvatar = null, + dateSent = dateSent, + dateSentFull = dateSentFull, + waveform = null, + duration = null, +) diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt index a824fc55406..906283c4570 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.mediaviewer.api +import android.os.Parcelable import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -14,6 +15,8 @@ import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs 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 +import kotlinx.parcelize.Parcelize interface MediaViewerEntryPoint : FeatureEntryPoint { fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder @@ -39,9 +42,14 @@ interface MediaViewerEntryPoint : FeatureEntryPoint { val canShowInfo: Boolean, ) : NodeInputs - enum class MediaViewerMode { - SingleMedia, - TimelineImagesAndVideos, - TimelineFilesAndAudios, + sealed interface MediaViewerMode : Parcelable { + @Parcelize + data object SingleMedia : MediaViewerMode + + @Parcelize + data class TimelineImagesAndVideos(val timelineMode: Timeline.Mode) : MediaViewerMode + + @Parcelize + data class TimelineFilesAndAudios(val timelineMode: Timeline.Mode) : MediaViewerMode } } diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts index bbe984fe9c1..311a0441c84 100644 --- a/libraries/mediaviewer/impl/build.gradle.kts +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -33,8 +33,10 @@ dependencies { implementation(libs.vanniktech.blurhash) implementation(libs.telephoto.flick) + implementation(projects.features.viewfolder.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) + implementation(projects.libraries.audio.api) implementation(projects.libraries.core) implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.di) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedTimelineMediaGalleryDataSourceFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedTimelineMediaGalleryDataSourceFactory.kt index f7426aa4e95..bdbffbfd1a9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedTimelineMediaGalleryDataSourceFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedTimelineMediaGalleryDataSourceFactory.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.datasource import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.mediaviewer.impl.model.MediaItem import javax.inject.Inject @@ -18,18 +18,20 @@ interface FocusedTimelineMediaGalleryDataSourceFactory { fun createFor( eventId: EventId, mediaItem: MediaItem.Event, + onlyPinnedEvents: Boolean, ): MediaGalleryDataSource } @ContributesBinding(RoomScope::class) class DefaultFocusedTimelineMediaGalleryDataSourceFactory @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, private val timelineMediaItemsFactory: TimelineMediaItemsFactory, private val mediaItemsPostProcessor: MediaItemsPostProcessor, ) : FocusedTimelineMediaGalleryDataSourceFactory { override fun createFor( eventId: EventId, mediaItem: MediaItem.Event, + onlyPinnedEvents: Boolean, ): MediaGalleryDataSource { return TimelineMediaGalleryDataSource( room = room, @@ -37,6 +39,7 @@ class DefaultFocusedTimelineMediaGalleryDataSourceFactory @Inject constructor( room = room, eventId = eventId, initialMediaItem = mediaItem, + onlyPinnedEvents = onlyPinnedEvents, ), timelineMediaItemsFactory = timelineMediaItemsFactory, mediaItemsPostProcessor = mediaItemsPostProcessor, 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 4ad36d88277..a7ee01aa574 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 @@ -12,13 +12,14 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn @@ -39,7 +40,7 @@ interface MediaGalleryDataSource { @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class) class TimelineMediaGalleryDataSource @Inject constructor( - private val room: MatrixRoom, + private val room: BaseRoom, private val mediaTimeline: MediaTimeline, private val timelineMediaItemsFactory: TimelineMediaItemsFactory, private val mediaItemsPostProcessor: MediaItemsPostProcessor, @@ -85,11 +86,13 @@ class TimelineMediaGalleryDataSource @Inject constructor( } }.flatMapLatest { timelineMediaItemsFactory.timelineItems - }.map { timelineItems -> - mediaItemsPostProcessor.process(mediaItems = timelineItems) - }.map { - mediaTimeline.orCache(it) - }.onEach { groupedMediaItems -> + } + .distinctUntilChanged() + .map { timelineItems -> + val groupedItems = mediaItemsPostProcessor.process(mediaItems = timelineItems) + mediaTimeline.orCache(groupedItems) + } + .onEach { groupedMediaItems -> groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems)) } .onCompletion { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaTimeline.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaTimeline.kt index 6dc41361936..c014689003f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaTimeline.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaTimeline.kt @@ -12,7 +12,8 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.CreateTimelineParams +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.libraries.mediaviewer.impl.model.MediaItem @@ -36,7 +37,7 @@ interface MediaTimeline { @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class) class LiveMediaTimeline @Inject constructor( - private val room: MatrixRoom, + private val room: JoinedRoom, ) : MediaTimeline { private var timeline: Timeline? = null private val mutex = Mutex() @@ -44,7 +45,7 @@ class LiveMediaTimeline @Inject constructor( override suspend fun getTimeline(): Result = mutex.withLock { val currentTimeline = timeline if (currentTimeline == null) { - room.mediaTimeline(null) + room.createTimeline(CreateTimelineParams.MediaOnly) .onSuccess { timeline = it } } else { Result.success(currentTimeline) @@ -58,14 +59,22 @@ class LiveMediaTimeline @Inject constructor( /** * A class that will provide a media timeline that is focused on a particular event. + * Optionally, the timeline will only contain the pinned events. */ class FocusedMediaTimeline( - private val room: MatrixRoom, + private val room: JoinedRoom, private val eventId: EventId, + private val onlyPinnedEvents: Boolean, initialMediaItem: MediaItem.Event, ) : MediaTimeline { override suspend fun getTimeline(): Result { - return room.mediaTimeline(eventId) + return room.createTimeline( + createTimelineParams = if (onlyPinnedEvents) { + CreateTimelineParams.PinnedOnly + } else { + CreateTimelineParams.MediaOnlyFocused(eventId) + }, + ) } override val cache = persistentListOf( 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 451569233f9..a35099cd19b 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 @@ -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.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -26,7 +25,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.components.BigIcon @@ -141,7 +140,7 @@ private fun MediaRow( // Info Text( text = state.mediaInfo.mimeType + " - " + state.mediaInfo.formattedFileSize, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodySmRegular, 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 90baecb9889..22c29c590bc 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 @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -30,6 +29,7 @@ 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 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.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -140,12 +140,13 @@ private fun SenderRow( ) { val id = mediaInfo.senderId?.value ?: "@Alice:domain" Avatar( - AvatarData( + avatarData = AvatarData( id = id, name = mediaInfo.senderName, url = mediaInfo.senderAvatar, size = AvatarSize.MediaSender, - ) + ), + avatarType = AvatarType.User, ) Column( modifier = Modifier @@ -165,7 +166,7 @@ private fun SenderRow( // Id Text( text = mediaInfo.senderId?.value.orEmpty(), - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodyMdRegular, 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 6617a8b9cbc..27fcf2ce3e6 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 @@ -22,12 +22,13 @@ import dagger.assisted.AssistedInject import io.element.android.libraries.androidutils.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState 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.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.mediaviewer.api.local.LocalMedia @@ -45,7 +46,7 @@ import kotlinx.coroutines.launch class MediaGalleryPresenter @AssistedInject constructor( @Assisted private val navigator: MediaGalleryNavigator, - private val room: MatrixRoom, + private val room: BaseRoom, private val mediaGalleryDataSource: MediaGalleryDataSource, private val localMediaFactory: LocalMediaFactory, private val mediaLoader: MatrixMediaLoader, @@ -64,7 +65,7 @@ class MediaGalleryPresenter @AssistedInject constructor( val coroutineScope = rememberCoroutineScope() var mode by remember { mutableStateOf(MediaGalleryMode.Images) } - val roomInfo by room.roomInfoFlow.collectAsState(null) + val roomInfo by room.roomInfoFlow.collectAsState() var mediaBottomSheetState by remember { mutableStateOf(MediaBottomSheetState.Hidden) } @@ -139,7 +140,7 @@ class MediaGalleryPresenter @AssistedInject constructor( } return MediaGalleryState( - roomName = roomInfo?.name ?: room.displayName, + roomName = roomInfo.name.orEmpty(), mode = mode, groupedMediaItems = groupedMediaItems, mediaBottomSheetState = mediaBottomSheetState, @@ -154,7 +155,7 @@ class MediaGalleryPresenter @AssistedInject constructor( mimeType = mediaItem.mediaInfo().mimeType, filename = mediaItem.mediaInfo().filename ) - .mapCatching { mediaFile -> + .mapCatchingExceptions { mediaFile -> localMediaFactory.createFromMediaFile( mediaFile = mediaFile, mediaInfo = mediaItem.mediaInfo() @@ -164,7 +165,7 @@ class MediaGalleryPresenter @AssistedInject constructor( private suspend fun saveOnDisk(mediaItem: MediaItem.Event) { downloadMedia(mediaItem) - .mapCatching { localMedia -> + .mapCatchingExceptions { localMedia -> localMediaActions.saveOnDisk(localMedia) } .onSuccess { @@ -179,7 +180,7 @@ class MediaGalleryPresenter @AssistedInject constructor( private suspend fun share(mediaItem: MediaItem.Event) { downloadMedia(mediaItem) - .mapCatching { localMedia -> + .mapCatchingExceptions { localMedia -> localMediaActions.share(localMedia) } .onFailure { 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 38eed3252f8..23f68c290d7 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 @@ -35,6 +35,8 @@ 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.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -95,6 +97,9 @@ fun MediaGalleryView( TopAppBar( title = { Text( + modifier = Modifier.semantics { + heading() + }, text = state.roomName, style = ElementTheme.typography.aliasScreenTitle, maxLines = 1, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt index 7317920005f..f862788c30d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.overlay.operation.show import io.element.android.libraries.di.RoomScope 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 import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint @@ -96,9 +97,9 @@ class MediaGalleryRootNode @AssistedInject constructor( val mode = when (item) { is MediaItem.Audio, is MediaItem.Voice, - is MediaItem.File -> MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios + is MediaItem.File -> MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(Timeline.Mode.MEDIA) is MediaItem.Image, - is MediaItem.Video -> MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos + is MediaItem.Video -> MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(Timeline.Mode.MEDIA) } overlay.show( NavTarget.MediaViewer( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt index 2cbbe6826ca..c8d735d4858 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column @@ -20,16 +19,16 @@ 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.material.icons.Icons -import androidx.compose.material.icons.outlined.GraphicEq 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.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.extensions.withBrackets import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -37,6 +36,7 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun AudioItemView( @@ -66,7 +66,6 @@ fun AudioItemView( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun FilenameRow( audio: MediaItem.Audio, @@ -80,7 +79,11 @@ private fun FilenameRow( color = ElementTheme.colors.bgSubtleSecondary, shape = RoundedCornerShape(12.dp), ) - .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) .fillMaxWidth() .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, @@ -93,7 +96,7 @@ private fun FilenameRow( ) .size(32.dp) .padding(6.dp), - imageVector = Icons.Outlined.GraphicEq, + imageVector = CompoundIcons.Audio(), contentDescription = null, ) Spacer(modifier = Modifier.width(8.dp)) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt index 84fe175fc9b..ff59a67b490 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -28,7 +30,10 @@ fun DateItemView( Text( modifier = modifier .fillMaxWidth() - .padding(12.dp), + .padding(12.dp) + .semantics { + heading() + }, text = item.formattedDate, textAlign = TextAlign.Center, style = ElementTheme.typography.fontBodyMdMedium, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt index bdb6539ecc5..1144fc3eaff 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column @@ -24,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -36,6 +36,7 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun FileItemView( @@ -65,7 +66,6 @@ fun FileItemView( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun FilenameRow( file: MediaItem.File, @@ -79,7 +79,11 @@ private fun FilenameRow( color = ElementTheme.colors.bgSubtleSecondary, shape = RoundedCornerShape(12.dp), ) - .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) .fillMaxWidth() .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt index 8df451abd64..917c60df9e2 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box @@ -22,16 +21,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalInspectionMode -import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter -import io.element.android.compound.theme.ElementTheme +import androidx.compose.ui.res.stringResource +import coil3.compose.AsyncImage +import coil3.compose.AsyncImagePainter import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage +import io.element.android.libraries.ui.strings.CommonStrings -@OptIn(ExperimentalFoundationApi::class) @Composable fun ImageItemView( image: MediaItem.Image, @@ -39,16 +37,14 @@ fun ImageItemView( onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - val bgColor = if (LocalInspectionMode.current) { - ElementTheme.colors.bgDecorative1 - } else { - Color.Transparent - } Box( modifier = modifier .aspectRatio(1f) - .combinedClickable(onClick = onClick, onLongClick = onLongClick) - .background(bgColor), + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ), ) { var isLoaded by remember { mutableStateOf(false) } AsyncImage( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt index f23be0f382d..cdc66c4eae5 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box @@ -27,11 +26,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter +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.libraries.designsystem.preview.ElementPreview @@ -39,8 +38,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import io.element.android.libraries.ui.strings.CommonStrings -@OptIn(ExperimentalFoundationApi::class) @Composable fun VideoItemView( video: MediaItem.Video, @@ -48,16 +47,14 @@ fun VideoItemView( onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - val bgColor = if (LocalInspectionMode.current) { - ElementTheme.colors.bgDecorative2 - } else { - Color.Transparent - } Box( modifier = modifier .aspectRatio(1f) - .combinedClickable(onClick = onClick, onLongClick = onLongClick) - .background(bgColor), + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ), ) { var isLoaded by remember { mutableStateOf(false) } AsyncImage( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt index 98dc091acbc..0ff75bc2444 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable @@ -85,7 +84,6 @@ fun VoiceItemView( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun VoiceInfoRow( state: VoiceMessageState, @@ -113,7 +111,11 @@ private fun VoiceInfoRow( color = ElementTheme.colors.bgSubtleSecondary, shape = RoundedCornerShape(12.dp), ) - .combinedClickable(onClick = {}, onLongClick = onLongClick) + .combinedClickable( + onClick = {}, + onLongClick = onLongClick, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) .fillMaxWidth() .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt index 8750647d972..d6c43ce4c41 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.mediaviewer.impl.local +import android.Manifest import android.app.Activity import android.content.ContentResolver import android.content.ContentValues @@ -26,10 +27,12 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import androidx.core.content.FileProvider +import androidx.core.content.PermissionChecker import androidx.core.net.toFile import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.androidutils.system.startInstallFromSourceIntent import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.AppScope @@ -81,7 +84,7 @@ class AndroidLocalMediaActions @Inject constructor( override suspend fun saveOnDisk(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) - runCatching { + runCatchingExceptions { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { saveOnDiskUsingMediaStore(localMedia) } else { @@ -96,7 +99,7 @@ class AndroidLocalMediaActions @Inject constructor( override suspend fun share(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) - runCatching { + runCatchingExceptions { val shareableUri = localMedia.toShareableUri() val shareMediaIntent = Intent(Intent.ACTION_SEND) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) @@ -115,11 +118,18 @@ class AndroidLocalMediaActions @Inject constructor( override suspend fun open(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) - runCatching { + runCatchingExceptions { when (localMedia.info.mimeType) { MimeTypes.Apk -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (activityContext?.packageManager?.canRequestPackageInstalls() == false) { + if (PermissionChecker.checkPermission( + context, + Manifest.permission.REQUEST_INSTALL_PACKAGES, + -1, + -1, + context.packageName + ) == PermissionChecker.PERMISSION_GRANTED && + activityContext?.packageManager?.canRequestPackageInstalls() == false) { pendingMedia = localMedia activityContext?.startInstallFromSourceIntent(apkInstallLauncher!!).let { } } else { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt index 4449e5eefdd..41c292a9213 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt @@ -11,20 +11,26 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.viewfolder.api.TextFileViewer +import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.di.AppScope import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer +import me.saket.telephoto.zoomable.OverzoomEffect import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.rememberZoomableState import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultLocalMediaRenderer @Inject constructor() : LocalMediaRenderer { +class DefaultLocalMediaRenderer @Inject constructor( + private val textFileViewer: TextFileViewer, + private val audioFocus: AudioFocus, +) : LocalMediaRenderer { @Composable override fun Render(localMedia: LocalMedia) { val localMediaViewState = rememberLocalMediaViewState( zoomableState = rememberZoomableState( - zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false) + zoomSpec = ZoomSpec(maxZoomFactor = 4f, overzoomEffect = OverzoomEffect.NoLimits) ) ) LocalMediaView( @@ -32,7 +38,9 @@ class DefaultLocalMediaRenderer @Inject constructor() : LocalMediaRenderer { bottomPaddingInPixels = 0, localMedia = localMedia, localMediaViewState = localMediaViewState, - onClick = {} + textFileViewer = textFileViewer, + audioFocus = audioFocus, + onClick = {}, ) } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt index 8752b19080a..1ce9bd33eb9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt @@ -9,6 +9,8 @@ package io.element.android.libraries.mediaviewer.impl.local import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import io.element.android.features.viewfolder.api.TextFileViewer +import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage @@ -19,15 +21,19 @@ import io.element.android.libraries.mediaviewer.impl.local.audio.MediaAudioView import io.element.android.libraries.mediaviewer.impl.local.file.MediaFileView import io.element.android.libraries.mediaviewer.impl.local.image.MediaImageView import io.element.android.libraries.mediaviewer.impl.local.pdf.MediaPdfView +import io.element.android.libraries.mediaviewer.impl.local.txt.TextFileView import io.element.android.libraries.mediaviewer.impl.local.video.MediaVideoView @Composable fun LocalMediaView( localMedia: LocalMedia?, bottomPaddingInPixels: Int, + audioFocus: AudioFocus?, onClick: () -> Unit, + textFileViewer: TextFileViewer, modifier: Modifier = Modifier, isDisplayed: Boolean = true, + isUserSelected: Boolean = false, localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(), mediaInfo: MediaInfo? = localMedia?.info, ) { @@ -44,6 +50,13 @@ fun LocalMediaView( localMediaViewState = localMediaViewState, bottomPaddingInPixels = bottomPaddingInPixels, localMedia = localMedia, + autoplay = isUserSelected, + audioFocus = audioFocus, + modifier = modifier, + ) + mimeType == MimeTypes.PlainText -> TextFileView( + localMedia = localMedia, + textFileViewer = textFileViewer, modifier = modifier, ) mimeType == MimeTypes.Pdf -> MediaPdfView( @@ -58,6 +71,7 @@ fun LocalMediaView( bottomPaddingInPixels = bottomPaddingInPixels, localMedia = localMedia, info = mediaInfo, + audioFocus = audioFocus, modifier = modifier, ) else -> MediaFileView( 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 226aa85234e..729e2c16051 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 @@ -23,9 +23,6 @@ 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.material.icons.Icons -import androidx.compose.material.icons.outlined.GraphicEq -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -54,6 +51,8 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -82,6 +81,7 @@ fun MediaAudioView( bottomPaddingInPixels: Int, localMedia: LocalMedia?, info: MediaInfo?, + audioFocus: AudioFocus?, modifier: Modifier = Modifier, isDisplayed: Boolean = true, ) { @@ -93,6 +93,7 @@ fun MediaAudioView( exoPlayer = exoPlayer, localMedia = localMedia, info = info, + audioFocus = audioFocus, modifier = modifier, ) } @@ -106,6 +107,7 @@ private fun ExoPlayerMediaAudioView( exoPlayer: ExoPlayer, localMedia: LocalMedia?, info: MediaInfo?, + audioFocus: AudioFocus?, modifier: Modifier = Modifier, ) { var mediaPlayerControllerState: MediaPlayerControllerState by remember { @@ -113,6 +115,7 @@ private fun ExoPlayerMediaAudioView( MediaPlayerControllerState( isVisible = true, isPlaying = false, + isReady = false, progressInMillis = 0, durationInMillis = 0, canMute = false, @@ -261,13 +264,13 @@ private fun ExoPlayerMediaAudioView( modifier = Modifier .size(72.dp) .clip(CircleShape) - .background(MaterialTheme.colorScheme.onBackground), + .background(ElementTheme.colors.iconPrimary), contentAlignment = Alignment.Center, ) { Icon( - imageVector = Icons.Outlined.GraphicEq, + imageVector = CompoundIcons.Audio(), contentDescription = null, - tint = MaterialTheme.colorScheme.background, + tint = ElementTheme.colors.iconOnSolidPrimary, modifier = Modifier .size(32.dp), ) @@ -295,6 +298,7 @@ private fun ExoPlayerMediaAudioView( onToggleMute = { // Cannot happen for audio files }, + audioFocus = audioFocus, modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) @@ -335,7 +339,7 @@ private fun AudioInfoView( style = ElementTheme.typography.fontBodyMdRegular, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.textPrimary ) } if (info != null) { @@ -346,7 +350,7 @@ private fun AudioInfoView( style = ElementTheme.typography.fontBodyLgRegular, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.textPrimary ) Spacer(modifier = Modifier.height(4.dp)) Text( @@ -354,7 +358,7 @@ private fun AudioInfoView( style = ElementTheme.typography.fontBodyMdRegular, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.textPrimary ) } } @@ -370,6 +374,7 @@ internal fun MediaAudioViewPreview( bottomPaddingInPixels = 0, localMediaViewState = rememberLocalMediaViewState(), info = info, + audioFocus = null, localMedia = null, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt index 7cc48dbd698..29e743c7b0f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt @@ -19,9 +19,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.GraphicEq -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -72,13 +69,13 @@ fun MediaFileView( modifier = Modifier .size(72.dp) .clip(CircleShape) - .background(MaterialTheme.colorScheme.onBackground), + .background(ElementTheme.colors.iconPrimary), contentAlignment = Alignment.Center, ) { Icon( - imageVector = if (isAudio) Icons.Outlined.GraphicEq else CompoundIcons.Attachment(), + imageVector = if (isAudio) CompoundIcons.Audio() else CompoundIcons.Attachment(), contentDescription = null, - tint = MaterialTheme.colorScheme.background, + tint = ElementTheme.colors.iconOnSolidPrimary, modifier = Modifier .size(32.dp) .rotate(if (isAudio) 0f else -45f), @@ -92,7 +89,7 @@ fun MediaFileView( style = ElementTheme.typography.fontBodyLgRegular, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.textPrimary ) Spacer(modifier = Modifier.height(4.dp)) Text( @@ -100,7 +97,7 @@ fun MediaFileView( style = ElementTheme.typography.fontBodyMdRegular, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.textPrimary ) } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt index fc1110086a5..9f6d10b6b06 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt @@ -10,10 +10,11 @@ package io.element.android.libraries.mediaviewer.impl.local.pdf import android.content.Context import android.net.Uri import android.os.ParcelFileDescriptor +import io.element.android.libraries.core.extensions.runCatchingExceptions import java.io.File class ParcelFileDescriptorFactory(private val context: Context) { - fun create(model: Any?) = runCatching { + fun create(model: Any?) = runCatchingExceptions { when (model) { is File -> ParcelFileDescriptor.open(model, ParcelFileDescriptor.MODE_READ_ONLY) is Uri -> context.contentResolver.openFileDescriptor(model, "r")!! diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt index fa3719bf8ba..c7562e98e7e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.local.pdf import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.extensions.runCatchingExceptions import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -35,7 +36,7 @@ class PdfRendererManager( coroutineScope.launch { mutex.withLock { withContext(Dispatchers.IO) { - pdfRenderer = runCatching { + pdfRenderer = runCatchingExceptions { PdfRenderer(parcelFileDescriptor) }.fold( onSuccess = { pdfRenderer -> diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewer.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewer.kt index b8b87f46054..3493c277cb0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewer.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfViewer.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.roundToPx import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.mediaviewer.impl.viewer.topAppBarHeight import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import me.saket.telephoto.zoomable.zoomable @@ -126,7 +127,7 @@ private fun PdfPagesContentView( ) { // Add a fake item to the top so that the first item is not at the top of the screen. item { - Spacer(modifier = Modifier.height(80.dp)) + Spacer(modifier = Modifier.height(topAppBarHeight)) } items(pdfPages.size) { index -> val pdfPage = pdfPages[index] diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt index b89f6a324fc..a098a531743 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt @@ -187,10 +187,6 @@ class ExoPlayerForPreview( override fun setDeviceMuted(muted: Boolean) {} override fun setDeviceMuted(muted: Boolean, flags: Int) {} override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) {} - override fun getAudioComponent(): ExoPlayer.AudioComponent? = throw NotImplementedError() - override fun getVideoComponent(): ExoPlayer.VideoComponent? = throw NotImplementedError() - override fun getTextComponent(): ExoPlayer.TextComponent? = throw NotImplementedError() - override fun getDeviceComponent(): ExoPlayer.DeviceComponent? = throw NotImplementedError() override fun addAudioOffloadListener(listener: ExoPlayer.AudioOffloadListener) {} override fun removeAudioOffloadListener(listener: ExoPlayer.AudioOffloadListener) {} override fun getAnalyticsCollector(): AnalyticsCollector = throw NotImplementedError() diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerState.kt index 349044439e4..6160b6758cc 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerState.kt @@ -12,6 +12,7 @@ import androidx.annotation.FloatRange data class MediaPlayerControllerState( val isVisible: Boolean, val isPlaying: Boolean, + val isReady: Boolean, val progressInMillis: Long, val durationInMillis: Long, val canMute: Boolean, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt index 88e4c2f7c77..2ce66a79e31 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt @@ -27,6 +27,7 @@ open class MediaPlayerControllerStateProvider : PreviewParameterProvider Unit, onSeekChange: (Float) -> Unit, onToggleMute: () -> Unit, + audioFocus: AudioFocus?, modifier: Modifier = Modifier, ) { + if (audioFocus != null) { + val latestOnTogglePlay by rememberUpdatedState(onTogglePlay) + LaunchedEffect(state.isPlaying) { + if (state.isPlaying) { + audioFocus.requestAudioFocus( + requester = AudioFocusRequester.MediaViewer, + onFocusLost = { + Timber.w("Audio focus lost") + latestOnTogglePlay() + }, + ) + } else { + audioFocus.releaseAudioFocus() + } + } + } + AnimatedVisibility( visible = state.isVisible, modifier = modifier, @@ -167,5 +190,6 @@ internal fun MediaPlayerControllerViewPreview( onTogglePlay = {}, onSeekChange = {}, onToggleMute = {}, + audioFocus = null, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileContentProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileContentProvider.kt new file mode 100644 index 00000000000..00480f2d42b --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileContentProvider.kt @@ -0,0 +1,23 @@ +/* + * 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.mediaviewer.impl.local.txt + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncData +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +open class TextFileContentProvider : PreviewParameterProvider>> { + override val values: Sequence>> + get() = sequenceOf( + AsyncData.Uninitialized, + AsyncData.Loading(), + AsyncData.Success(persistentListOf("Hello, World!")), + AsyncData.Failure(Exception("Failed to load text")), + ) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt new file mode 100644 index 00000000000..4c4429b775c --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt @@ -0,0 +1,111 @@ +/* + * 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.mediaviewer.impl.local.txt + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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 io.element.android.features.viewfolder.api.TextFileViewer +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.extensions.runCatchingExceptions +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.Text +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.impl.viewer.topAppBarHeight +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun TextFileView( + localMedia: LocalMedia?, + textFileViewer: TextFileViewer, + modifier: Modifier = Modifier, +) { + val data = remember { mutableStateOf>>(AsyncData.Uninitialized) } + val context = LocalContext.current + LaunchedEffect(localMedia?.uri) { + data.value = AsyncData.Loading() + if (localMedia?.uri != null) { + // Load the file content + val result = runCatchingExceptions { + context.contentResolver.openInputStream(localMedia.uri).use { + it?.bufferedReader()?.readLines()?.toList().orEmpty() + } + } + data.value = if (result.isSuccess) { + AsyncData.Success(result.getOrNull().orEmpty().toImmutableList()) + } else { + AsyncData.Failure(result.exceptionOrNull() ?: Exception("An error occurred")) + } + } + } + TextFileContentView( + data = data.value, + textFileViewer = textFileViewer, + modifier = modifier, + ) +} + +@Composable +private fun TextFileContentView( + data: AsyncData>, + textFileViewer: TextFileViewer, + modifier: Modifier = Modifier, +) { + when (data) { + AsyncData.Uninitialized, + is AsyncData.Loading -> Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + is AsyncData.Failure -> Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = data.error.message ?: stringResource(id = CommonStrings.error_unknown)) + } + is AsyncData.Success -> { + textFileViewer.Render( + lines = data.data, + modifier = modifier + .fillMaxSize() + .padding(top = topAppBarHeight), + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun TextFileContentViewPreview( + @PreviewParameter(TextFileContentProvider::class) text: AsyncData>, +) = ElementPreview { + TextFileContentView( + data = text, + textFileViewer = { lines, modifier -> + Text( + modifier = modifier, + text = lines.firstOrNull() ?: "File content" + ) + } + ) +} 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 934f7e8352d..8e619fbdc97 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 @@ -25,17 +25,21 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.Player.STATE_READY import androidx.media3.common.Timeline +import androidx.media3.common.VideoSize import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp @@ -52,6 +56,8 @@ import io.element.android.libraries.mediaviewer.impl.local.player.seekToEnsurePl import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState import kotlinx.coroutines.delay +import me.saket.telephoto.zoomable.ZoomableContentLocation +import me.saket.telephoto.zoomable.zoomable import kotlin.time.Duration.Companion.seconds @SuppressLint("UnsafeOptInUsageError") @@ -61,6 +67,8 @@ fun MediaVideoView( localMediaViewState: LocalMediaViewState, bottomPaddingInPixels: Int, localMedia: LocalMedia?, + autoplay: Boolean, + audioFocus: AudioFocus?, modifier: Modifier = Modifier, ) { val exoPlayer = rememberExoPlayer() @@ -70,6 +78,8 @@ fun MediaVideoView( bottomPaddingInPixels = bottomPaddingInPixels, exoPlayer = exoPlayer, localMedia = localMedia, + autoplay = autoplay, + audioFocus = audioFocus, modifier = modifier, ) } @@ -82,6 +92,8 @@ private fun ExoPlayerMediaVideoView( bottomPaddingInPixels: Int, exoPlayer: ExoPlayer, localMedia: LocalMedia?, + autoplay: Boolean, + audioFocus: AudioFocus?, modifier: Modifier = Modifier, ) { var mediaPlayerControllerState: MediaPlayerControllerState by remember { @@ -89,6 +101,7 @@ private fun ExoPlayerMediaVideoView( MediaPlayerControllerState( isVisible = true, isPlaying = false, + isReady = false, progressInMillis = 0, durationInMillis = 0, canMute = true, @@ -135,6 +148,24 @@ private fun ExoPlayerMediaVideoView( } } } + + override fun onPlaybackStateChanged(playbackState: Int) { + mediaPlayerControllerState = mediaPlayerControllerState.copy( + isReady = playbackState == STATE_READY, + ) + } + + override fun onVideoSizeChanged(videoSize: VideoSize) { + // Ensure that the user cannot zoom/move outside of the video bounds + localMediaViewState.zoomableState.setContentLocation( + ZoomableContentLocation.scaledInsideAndCenterAligned( + Size( + videoSize.width.toFloat(), + videoSize.height.toFloat(), + ) + ) + ) + } } } @@ -164,9 +195,17 @@ private fun ExoPlayerMediaVideoView( ) } } - LaunchedEffect(isDisplayed) { - // If not displayed, make sure to pause the video - if (!isDisplayed) { + + var needsAutoPlay by remember { mutableStateOf(autoplay) } + + LaunchedEffect(needsAutoPlay, isDisplayed, mediaPlayerControllerState.isReady) { + val isReadyAndNotPlaying = mediaPlayerControllerState.isReady && !mediaPlayerControllerState.isPlaying + if (needsAutoPlay && isDisplayed && isReadyAndNotPlaying) { + // When displayed, start autoplaying + exoPlayer.play() + needsAutoPlay = false + } else if (!isDisplayed && mediaPlayerControllerState.isPlaying) { + // If not displayed, make sure to pause the video exoPlayer.pause() } } @@ -193,24 +232,26 @@ private fun ExoPlayerMediaVideoView( ) } else { AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { - PlayerView(context).apply { - player = exoPlayer - resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT - layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) - setOnClickListener { + modifier = Modifier + .fillMaxSize() + .zoomable( + state = localMediaViewState.zoomableState, + onClick = { autoHideController++ mediaPlayerControllerState = mediaPlayerControllerState.copy( isVisible = !mediaPlayerControllerState.isVisible, ) } + ), + factory = { + PlayerView(context).apply { + player = exoPlayer + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) useController = false } }, onRelease = { playerView -> - playerView.setOnClickListener(null) - playerView.setControllerVisibilityListener(null as PlayerView.ControllerVisibilityListener?) playerView.player = null }, ) @@ -229,6 +270,7 @@ private fun ExoPlayerMediaVideoView( autoHideController++ exoPlayer.volume = if (exoPlayer.volume == 1f) 0f else 1f }, + audioFocus = audioFocus, modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) @@ -259,5 +301,7 @@ internal fun MediaVideoViewPreview() = ElementPreview { bottomPaddingInPixels = 0, localMediaViewState = rememberLocalMediaViewState(), localMedia = null, + audioFocus = null, + autoplay = 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 ebaba4cdcdf..b2eb7e7b6f9 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 @@ -15,6 +15,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf 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.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.timeline.Timeline @@ -52,8 +53,8 @@ class MediaViewerDataSource( private val galleryMode = when (mode) { MediaViewerMode.SingleMedia, - MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images - MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files + is MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images + is MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files } // Map of sourceUrl to local media state @@ -76,7 +77,7 @@ class MediaViewerDataSource( } @VisibleForTesting - fun dataFlow(): Flow> { + internal fun dataFlow(): Flow> { return galleryDataSource.groupedMediaItemsFlow() .map { groupedItems -> when (groupedItems) { @@ -173,7 +174,7 @@ class MediaViewerDataSource( .onSuccess { mediaFile -> mediaFiles.add(mediaFile) } - .mapCatching { mediaFile -> + .mapCatchingExceptions { mediaFile -> localMediaFactory.createFromMediaFile( mediaFile = mediaFile, mediaInfo = data.mediaInfo diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerFlickToDismiss.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerFlickToDismiss.kt index 1b99cdfa03c..43880809d9f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerFlickToDismiss.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerFlickToDismiss.kt @@ -18,11 +18,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import io.element.android.compound.theme.ElementTheme import kotlinx.coroutines.delay +import me.saket.telephoto.ExperimentalTelephotoApi import me.saket.telephoto.flick.FlickToDismiss import me.saket.telephoto.flick.FlickToDismissState import me.saket.telephoto.flick.rememberFlickToDismissState import kotlin.time.Duration +@OptIn(ExperimentalTelephotoApi::class) @Composable fun MediaViewerFlickToDismiss( onDismiss: () -> Unit, 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 78766410514..2da3e1c3f2b 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 @@ -17,11 +17,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.compound.theme.ForcedDarkElementTheme +import io.element.android.features.viewfolder.api.TextFileViewer import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.RoomScope 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.timeline.Timeline import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.impl.datasource.FocusedTimelineMediaGalleryDataSourceFactory @@ -41,6 +44,8 @@ class MediaViewerNode @AssistedInject constructor( coroutineDispatchers: CoroutineDispatchers, systemClock: SystemClock, pagerKeysHandler: PagerKeysHandler, + private val textFileViewer: TextFileViewer, + private val audioFocus: AudioFocus, ) : Node(buildContext, plugins = plugins), MediaViewerNavigator { private val inputs = inputs() @@ -69,16 +74,37 @@ class MediaViewerNode @AssistedInject constructor( // Should not happen timelineMediaGalleryDataSource } else { - // Does timelineMediaGalleryDataSource knows the eventId? - val lastData = timelineMediaGalleryDataSource.getLastData().dataOrNull() - val isEventKnown = lastData?.hasEvent(eventId) == true - if (isEventKnown) { - timelineMediaGalleryDataSource - } else { - focusedTimelineMediaGalleryDataSourceFactory.createFor( - eventId = eventId, - mediaItem = inputs.toMediaItem(), - ) + // Can we use a specific timeline? + val timelineMode = when (val mode = inputs.mode) { + is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> mode.timelineMode + is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> mode.timelineMode + else -> null + } + when (timelineMode) { + null -> timelineMediaGalleryDataSource + Timeline.Mode.LIVE, + Timeline.Mode.FOCUSED_ON_EVENT -> { + // Does timelineMediaGalleryDataSource knows the eventId? + val lastData = timelineMediaGalleryDataSource.getLastData().dataOrNull() + val isEventKnown = lastData?.hasEvent(eventId) == true + if (isEventKnown) { + timelineMediaGalleryDataSource + } else { + focusedTimelineMediaGalleryDataSourceFactory.createFor( + eventId = eventId, + mediaItem = inputs.toMediaItem(), + onlyPinnedEvents = false, + ) + } + } + Timeline.Mode.PINNED_EVENTS -> { + focusedTimelineMediaGalleryDataSourceFactory.createFor( + eventId = eventId, + mediaItem = inputs.toMediaItem(), + onlyPinnedEvents = true, + ) + } + Timeline.Mode.MEDIA -> timelineMediaGalleryDataSource } } } @@ -103,8 +129,10 @@ class MediaViewerNode @AssistedInject constructor( val state = presenter.present() MediaViewerView( state = state, + textFileViewer = textFileViewer, modifier = modifier, - onBackClick = ::onDone + audioFocus = audioFocus, + onBackClick = ::onDone, ) } } 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 b9262914612..f9005103210 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 @@ -30,7 +30,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId @@ -53,7 +53,7 @@ class MediaViewerPresenter @AssistedInject constructor( @Assisted private val inputs: MediaViewerEntryPoint.Params, @Assisted private val navigator: MediaViewerNavigator, @Assisted private val dataSource: MediaViewerDataSource, - private val room: MatrixRoom, + private val room: JoinedRoom, private val localMediaActions: LocalMediaActions, ) : Presenter { @AssistedFactory @@ -148,6 +148,7 @@ class MediaViewerPresenter @AssistedInject constructor( } return MediaViewerState( + initiallySelectedEventId = inputs.eventId, listData = data.value, currentIndex = currentIndex.intValue, snackbarMessage = snackbarMessage, @@ -164,7 +165,9 @@ class MediaViewerPresenter @AssistedInject constructor( ) { val isRenderingLoadingBackward by remember { derivedStateOf { - currentIndex.intValue == data.value.lastIndex && data.value.lastOrNull() is MediaViewerPageData.Loading + currentIndex.intValue == data.value.lastIndex && + data.value.size > 1 && + data.value.lastOrNull() is MediaViewerPageData.Loading } } if (isRenderingLoadingBackward) { @@ -186,7 +189,9 @@ class MediaViewerPresenter @AssistedInject constructor( ) { val isRenderingLoadingForward by remember { derivedStateOf { - currentIndex.intValue == 0 && data.value.firstOrNull() is MediaViewerPageData.Loading + currentIndex.intValue == 0 && + data.value.size > 1 && + data.value.firstOrNull() is MediaViewerPageData.Loading } } if (isRenderingLoadingForward) { @@ -204,8 +209,8 @@ class MediaViewerPresenter @AssistedInject constructor( private fun showNoMoreItemsSnackbar() { val messageResId = when (inputs.mode) { MediaViewerEntryPoint.MediaViewerMode.SingleMedia, - MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> R.string.screen_media_details_no_more_media_to_show - MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> R.string.screen_media_details_no_more_files_to_show + is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> R.string.screen_media_details_no_more_media_to_show + is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> R.string.screen_media_details_no_more_files_to_show } val message = SnackbarMessage(messageResId) snackbarDispatcher.post(message) 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 32c22b04706..5a0c4c23077 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 @@ -19,6 +19,7 @@ import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetSta import kotlinx.collections.immutable.ImmutableList data class MediaViewerState( + val initiallySelectedEventId: EventId?, val listData: ImmutableList, val currentIndex: Int, val snackbarMessage: SnackbarMessage?, 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 95557c13d67..324b0933251 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 @@ -12,10 +12,12 @@ 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.aWaveForm +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 import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo +import io.element.android.libraries.mediaviewer.api.aTxtMediaInfo import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo import io.element.android.libraries.mediaviewer.api.anApkMediaInfo import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo @@ -159,6 +161,14 @@ open class MediaViewerStateProvider : PreviewParameterProvider MediaViewerPageData.Failure(Exception("error")) ), ), + aMediaViewerState( + listOf( + aMediaViewerPageData( + downloadedMedia = AsyncData.Loading(), + mediaInfo = aTxtMediaInfo(), + ) + ) + ), ) } @@ -193,6 +203,7 @@ fun aMediaViewerState( mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden, eventSink: (MediaViewerEvents) -> Unit = {}, ) = MediaViewerState( + initiallySelectedEventId = EventId("\$a:b"), listData = listData.toPersistentList(), currentIndex = currentIndex, snackbarMessage = null, 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 3ba57cae337..110054eb20b 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 @@ -23,8 +23,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.TopAppBarDefaults @@ -39,19 +37,24 @@ 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 import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage +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.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 @@ -81,13 +84,18 @@ import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaVie import io.element.android.libraries.mediaviewer.impl.util.bgCanvasWithTransparency import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.delay +import me.saket.telephoto.zoomable.OverzoomEffect import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.rememberZoomableState +val topAppBarHeight = 88.dp + @Composable fun MediaViewerView( state: MediaViewerState, + textFileViewer: TextFileViewer, onBackClick: () -> Unit, + audioFocus: AudioFocus?, modifier: Modifier = Modifier, ) { val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) @@ -139,11 +147,17 @@ fun MediaViewerView( 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 + } MediaViewerPage( - isDisplayed = page == pagerState.settledPage, + isDisplayed = isDisplayed, showOverlay = showOverlay, bottomPaddingInPixels = bottomPaddingInPixels, data = dataForPage, + textFileViewer = textFileViewer, onDismiss = onBackClick, onRetry = { state.eventSink(MediaViewerEvents.LoadMedia(dataForPage)) @@ -153,7 +167,9 @@ fun MediaViewerView( }, onShowOverlayChange = { showOverlay = it - } + }, + audioFocus = audioFocus, + isUserSelected = (state.listData[page] as? MediaViewerPageData.MediaViewerData)?.eventId == state.initiallySelectedEventId, ) // Bottom bar AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { @@ -198,6 +214,9 @@ fun MediaViewerView( title = { if (currentData is MediaViewerPageData.Loading) { Text( + modifier = Modifier.semantics { + heading() + }, text = stringResource(id = CommonStrings.common_loading_more), style = ElementTheme.typography.fontBodyMdMedium, color = ElementTheme.colors.textPrimary, @@ -268,10 +287,13 @@ private fun MediaViewerPage( showOverlay: Boolean, bottomPaddingInPixels: Int, data: MediaViewerPageData.MediaViewerData, + textFileViewer: TextFileViewer, + isUserSelected: Boolean, onDismiss: () -> Unit, onRetry: () -> Unit, onDismissError: () -> Unit, onShowOverlayChange: (Boolean) -> Unit, + audioFocus: AudioFocus?, modifier: Modifier = Modifier, ) { val currentShowOverlay by rememberUpdatedState(showOverlay) @@ -297,7 +319,7 @@ private fun MediaViewerPage( ) { Box(contentAlignment = Alignment.Center) { val zoomableState = rememberZoomableState( - zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false) + zoomSpec = ZoomSpec(maxZoomFactor = 4f, overzoomEffect = OverzoomEffect.NoLimits) ) val localMediaViewState = rememberLocalMediaViewState(zoomableState) val showThumbnail = !localMediaViewState.isReady @@ -317,11 +339,14 @@ private fun MediaViewerPage( localMediaViewState = localMediaViewState, localMedia = downloadedMedia.dataOrNull(), mediaInfo = data.mediaInfo, + textFileViewer = textFileViewer, onClick = { if (playableState is PlayableState.NotPlayable) { currentOnShowOverlayChange(!currentShowOverlay) } }, + isUserSelected = isUserSelected, + audioFocus = audioFocus, ) ThumbnailView( mediaInfo = data.mediaInfo, @@ -434,6 +459,9 @@ private fun MediaViewerTopBar( .fillMaxWidth() ) { Text( + modifier = Modifier.semantics { + heading() + }, text = senderName, style = ElementTheme.typography.fontBodyMdMedium, color = ElementTheme.colors.textPrimary, @@ -467,7 +495,7 @@ private fun MediaViewerTopBar( contentDescription = stringResource(id = CommonStrings.common_install_apk_android) ) else -> Icon( - imageVector = Icons.AutoMirrored.Filled.OpenInNew, + imageVector = CompoundIcons.PopOut(), contentDescription = stringResource(id = CommonStrings.action_open_with) ) } @@ -535,8 +563,11 @@ private fun ThumbnailView( source = thumbnailSource, kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType) ) + val alpha = if (LocalInspectionMode.current) 0.1f else 1f AsyncImage( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .alpha(alpha), model = mediaRequestData, contentScale = ContentScale.Fit, contentDescription = null, @@ -564,6 +595,8 @@ private fun ErrorView( internal fun MediaViewerViewPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = ElementPreviewDark { MediaViewerView( state = state, - onBackClick = {} + audioFocus = null, + textFileViewer = { _, _ -> }, + onBackClick = {}, ) } diff --git a/libraries/mediaviewer/impl/src/main/res/values-bg/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 00000000000..e6874e67430 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,6 @@ + + + "Файлове" + "Медия" + "Медия и файлове" + 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 3cab2b499a8..bcb7b642377 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml @@ -16,8 +16,6 @@ "Název souboru" "Žádné další soubory k zobrazení" "Žádná další média k zobrazení" - "Tento soubor bude odstraněn z místnosti a členové k němu nebudou mít přístup." - "Smazat soubor?" "Nahrál(a)" "Nahráno" diff --git a/libraries/mediaviewer/impl/src/main/res/values-cy/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..6e08baa5638 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,21 @@ + + + "Bydd y ffeil hon yn cael ei thynnu o\'r ystafell a bydd gan aelodau ddim mynediad iddi." + "Dileu ffeil?" + "Gwiriwch eich cysylltiad rhyngrwyd a rhowch gynnig arall arni." + "Bydd dogfennau, ffeiliau sain, a negeseuon llais llwythwyd i\'r ystafell hon yn cael eu dangos yma." + "Dim ffeiliau wedi\'u llwytho eto" + "Wrthi\'n llwytho ffeiliau…" + "Wrthi\'n llwytho cyfryngau…" + "Ffeiliau" + "Cyfryngau" + "Bydd delweddau a fideos llwythwyd i\'r ystafell hon yn cael eu dangos yma." + "Dim cyfrwng wedi\'i llwytho eto" + "Cyfryngau a ffeiliau" + "Fformat ffeil" + "Enw\'r ffeil" + "Dim mwy o ffeiliau i\'w dangos" + "Dim mwy o gyfryngau i\'w dangos" + "Llwythwyd gan" + "Llwythwyd i fyny ar" + diff --git a/libraries/mediaviewer/impl/src/main/res/values-da/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..5d88458495a --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,21 @@ + + + "Denne fil vil blive fjernet fra rummet, og medlemmer vil ikke længere have adgang til den." + "Vil du slette filen?" + "Tjek din internetforbindelse, og prøv igen." + "Dokumenter, lydfiler og stemmemeddelelser uploadet til dette rum vises her." + "Ingen filer uploadet endnu" + "Indlæser filer…" + "Indlæser medier…" + "Filer" + "Medier" + "Billeder og videoer uploadet til dette rum vil blive vist her." + "Ingen medier uploadet endnu" + "Medier og filer" + "Filformat" + "Filnavn" + "Ikke flere filer at vise" + "Ikke flere medier at vise" + "Uploadet af" + "Uploadet på" + diff --git a/libraries/mediaviewer/impl/src/main/res/values-de/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-de/translations.xml index f7cb2c9e606..e27fd3f1cc1 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-de/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-de/translations.xml @@ -16,8 +16,6 @@ "Dateiname" "Keine weiteren Dateien zum Anzeigen" "Keine weiteren Medien mehr zum Anzeigen" - "Diese Datei wird aus dem Chatroom entfernt und die Mitglieder werden keinen Zugriff mehr darauf haben." - "Datei löschen?" "Hochgeladen von" "Hochgeladen am" diff --git a/libraries/mediaviewer/impl/src/main/res/values-el/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-el/translations.xml index 8452eb9158d..c273012001b 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-el/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-el/translations.xml @@ -2,17 +2,20 @@ "Αυτό το αρχείο θα αφαιρεθεί από την αίθουσα και τα μέλη δεν θα έχουν πρόσβαση σε αυτό." "Διαγραφή αρχείου;" + "Ελέγξτε τη σύνδεσή σας στο διαδίκτυο και δοκιμάστε ξανά." + "Τα έγγραφα, τα αρχεία ήχου και τα φωνητικά μηνύματα που μεταφορτώνονται σε αυτή την αίθουσα θα εμφανίζονται εδώ." + "Δεν έχουν μεταφορτωθεί ακόμα αρχεία" "Φόρτωση αρχείων…" "Φόρτωση πολυμέσων…" "Αρχεία" "Πολυμέσα" - "Εικόνες και βίντεο που μεταφορτώνονται σε αυτό το δωμάτιο θα εμφανίζονται εδώ." + "Οι εικόνες και τα βίντεο που μεταφορτώνονται σε αυτή την αίθουσα θα εμφανίζονται εδώ." "Δεν έχουν μεταφορτωθεί ακόμα πολυμέσα" "Πολυμέσα και αρχεία" "Μορφή αρχείου" "Όνομα αρχείου" - "Αυτό το αρχείο θα αφαιρεθεί από το δωμάτιο και τα μέλη δεν θα έχουν πρόσβαση σε αυτό." - "Διαγραφή αρχείου;" + "Δεν υπάρχουν άλλα αρχεία για εμφάνιση" + "Δεν υπάρχουν άλλα μέσα για εμφάνιση" "Μεταφορτώθηκε από" "Μεταφορτώθηκε στις" diff --git a/libraries/mediaviewer/impl/src/main/res/values-es/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..686dc606587 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,21 @@ + + + "Este archivo se eliminará de la sala y los miembros no tendrán acceso a él." + "¿Borrar archivo?" + "Verifica tu conexión a Internet e inténtalo de nuevo." + "Los documentos, archivos de audio y mensajes de voz subidos a esta sala se mostrarán aquí." + "Aún no se ha subido ningún archivo" + "Cargando archivos…" + "Cargando medios…" + "Archivos" + "Medios" + "Las imágenes y vídeos subidos a esta sala se mostrarán aquí." + "Aún no se ha subido ningún medio" + "Medios y archivos" + "Formato de archivo" + "Nombre del archivo" + "No hay más archivos que mostrar" + "No hay más medios que mostrar" + "Subido por" + "Subido el" + 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 d66433e52ec..63329a797d1 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml @@ -16,8 +16,6 @@ "Failinimi" "Pole enam kuvatavaid faile" "Pole enam kuvatavat meediat" - "Järgnevaga eemaldame selle faili jututoast ja tema liikmed enam ei pääse failile ligi." - "Kas kustutame faili?" "Üleslaadija" "Üleslaaditud" diff --git a/libraries/mediaviewer/impl/src/main/res/values-eu/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..b7f00b4dcba --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,19 @@ + + + "Fitxategia gelatik kenduko da eta kideek ezingo dute atzitu." + "Fitxategia ezabatu?" + "Egiaztatu Interneteko konexioa eta saiatu berriro." + "Gela honetara igotako dokumentuak, audio-fitxategiak, eta ahots-mezuak hemen erakutsiko dira." + "Oraindik ez da fitxategirik igo" + "Fitxategiak kargatzen…" + "Multimedia kargatzen…" + "Fitxategiak" + "Multimedia" + "Gela honetara igotako irudiak eta bideoak hemen erakutsiko dira." + "Oraindik ez da multimedia fitxategirik igo" + "Multimedia eta fitxategiak" + "Fitxategiaren formatua" + "Fitxategiaren izena" + "Nork igota:" + "Noiz igota:" + diff --git a/libraries/mediaviewer/impl/src/main/res/values-fa/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-fa/translations.xml new file mode 100644 index 00000000000..cbf9f63758f --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,9 @@ + + + "پرونده‌ها" + "رسانه" + "قالب پرونده" + "نام پرونده" + "بارگذاشته به دست" + "بارگذاشته در" + 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 b477b7c94fd..b3728cb9b7b 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml @@ -16,8 +16,6 @@ "Tiedostonimi" "Ei enää näytettäviä tiedostoja" "Ei enää näytettävää mediaa" - "Tämä tiedosto poistetaan huoneesta, eikä jäsenillä ole enää pääsyä siihen." - "Poistetaanko tiedosto?" "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 007356e3f6e..b6334095ab0 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml @@ -16,8 +16,6 @@ "Nom du fichier" "Il n’y a plus de fichiers à montrer" "Il n’y a plus de médias à montrer" - "Ce fichier sera supprimé du salon et les membres n’y auront plus accès." - "Supprimer le fichier ?" "Envoyé par" "Envoyé le" 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 8876145e582..14cc11fb96c 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml @@ -16,8 +16,6 @@ "Fájlnév" "Nincs több megjeleníthető fájl" "Nincs több megjeleníthető média" - "Ez a fájl el lesz távolítva a szobából, és a tagok nem férhetnek hozzá." - "Törli a fájlt?" "Feltöltötte:" "Feltöltve:" diff --git a/libraries/mediaviewer/impl/src/main/res/values-in/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..b5c2bde7cbf --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,21 @@ + + + "Berkas ini akan dihapus dari ruangan dan anggota tidak akan memiliki akses ke sana." + "Hapus berkas?" + "Periksa koneksi internet Anda dan coba lagi." + "Dokumen, berkas audio, dan pesan suara yang diunggah ke ruangan ini akan ditampilkan di sini." + "Belum ada berkas yang diunggah" + "Memuat berkas…" + "Memuat media…" + "Berkas" + "Media" + "Gambar dan video yang diunggah ke ruangan ini akan ditampilkan di sini." + "Belum ada media yang diunggah" + "Media dan berkas" + "Format berkas" + "Nama berkas" + "Tidak ada lagi berkas untuk ditampilkan" + "Tidak ada lagi media untuk ditampilkan" + "Diunggah oleh" + "Diunggah pada" + 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 1bdbfa08d93..72701194fc6 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml @@ -16,8 +16,6 @@ "Nome del file" "Nessun altro file da mostrare" "Non ci sono più contenuti multimediali da mostrare" - "Questo file verrà rimosso dalla stanza e i membri non ne avranno accesso." - "Eliminare il file?" "Caricato da" "Caricato il" diff --git a/libraries/mediaviewer/impl/src/main/res/values-nb/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..8cc6eb3c842 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,21 @@ + + + "Denne filen vil bli fjernet fra rommet, og medlemmene vil ikke lenger ha tilgang til den." + "Vil du slette filen?" + "Sjekk internettforbindelsen din og prøv igjen." + "Dokumenter, lydfiler og talemeldinger som lastes opp til dette rommet, vises her." + "Ingen filer lastet opp ennå" + "Laster inn filer…" + "Laster inn medier…" + "Filer" + "Mediefiler" + "Bilder og videoer som lastes opp til dette rommet, vises her." + "Ingen mediefiler lastet opp ennå" + "Medier og filer" + "Filformat" + "Filnavn" + "Ingen flere filer å vise" + "Ikke flere mediefiler å vise" + "Lastet opp av" + "Lastet opp den" + 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 289b75a4d86..c398123873b 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-pl/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-pl/translations.xml @@ -2,6 +2,7 @@ "Ten plik zostanie usunięty z pokoju, a członkowie nie będą mieli do niego dostępu." "Usunąć plik?" + "Sprawdź połączenie internetowe i spróbuj ponownie." "Dokumenty, pliki audio i wiadomości głosowe przesłane do tego pokoju będą wyświetlane tutaj." "Jeszcze nie przesłano plików" "Wczytywanie plików…" @@ -13,8 +14,8 @@ "Media i pliki" "Format pliku" "Nazwa pliku" - "Plik zostanie usunięty z pokoju, a członkowie nie będą mieli do niego dostępu." - "Usunąć plik?" + "Brak plików do pokazania" + "Brak mediów do pokazania" "Przesłane przez" "Przesłane w dniu" diff --git a/libraries/mediaviewer/impl/src/main/res/values-pt-rBR/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..061b43e5ea1 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,21 @@ + + + "Esse arquivo será removido da sala e os membros não terão acesso a ele." + "Excluir arquivo?" + "Verifique sua conexão com a Internet e tente novamente." + "Os documentos, arquivos de áudio e mensagens de voz enviados para esta sala serão exibidos aqui." + "Ainda não foram enviados arquivos" + "Carregando arquivos…" + "Carregando mídia…" + "Arquivos" + "Mídia" + "As imagens e os vídeos enviados para esta sala serão exibidos aqui." + "Nenhuma mídia foi carregada ainda" + "Mídia e arquivos" + "Formato de arquivo" + "Nome do arquivo" + "Não há mais arquivos para mostrar" + "Não há mais mídia para mostrar" + "Enviado por" + "Enviado em" + diff --git a/libraries/mediaviewer/impl/src/main/res/values-pt/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-pt/translations.xml index d36fb9cf83b..453a00969d5 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-pt/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-pt/translations.xml @@ -2,6 +2,9 @@ "Este ficheiro será removido da sala e os membros deixarão de o poder aceder." "Eliminar ficheiro?" + "Verifica a tua ligação à Internet e tenta novamente." + "Documentos, ficheiros de áudio e mensagens de voz carregados para esta sala serão mostrados aqui." + "Ainda sem qualquer ficheiro" "A carregar ficheiros…" "A carregar multimédia…" "Ficheiros" @@ -11,7 +14,8 @@ "Multimédia e ficheiros" "Formato do ficheiro" "Nome do ficheiro" - "Eliminar ficheiro?" + "Sem mais ficheiros para mostrar" + "Sem mais multimédia para mostrar" "Enviado por" "Enviado a" 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 df898e332fd..0598d68fa54 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml @@ -16,8 +16,6 @@ "Имя файла" "Больше нет файлов для показа" "Больше нет медиа для показа" - "Этот файл будет удален из комнаты и у участников не будет к нему доступа." - "Удалить файл?" "Загружено" "Загружено на" diff --git a/libraries/mediaviewer/impl/src/main/res/values-sk/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-sk/translations.xml index 2b9dbd39529..4cfc16b3b3e 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-sk/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-sk/translations.xml @@ -16,8 +16,6 @@ "Názov súboru" "Žiadne ďalšie súbory na zobrazenie" "Žiadne ďalšie médiá na zobrazenie" - "Tento súbor bude odstránený z miestnosti a členovia k nemu nebudú mať prístup." - "Vymazať súbor?" "Nahrané používateľom" "Nahrané dňa" diff --git a/libraries/mediaviewer/impl/src/main/res/values-sv/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 00000000000..ac1040d60e2 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,21 @@ + + + "Den här filen kommer att tas bort från rummet och medlemmar kommer inte att ha tillgång till den." + "Radera fil?" + "Kontrollera din internetanslutning och försök igen." + "Dokument, ljudfiler och röstmeddelanden som laddas upp till detta rum visas här." + "Inga filer uppladdade än" + "Laddar in filer…" + "Läser in media…" + "Filer" + "Media" + "Bilder och videor som laddas upp till detta rum kommer att visas här." + "Ingen media uppladdad ännu" + "Media och filer" + "Filformat" + "Filnamn" + "Inga fler filer att visa" + "Ingen mer media att visa" + "Uppladdad av" + "Uppladdad på" + diff --git a/libraries/mediaviewer/impl/src/main/res/values-tr/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..30a1d13c4df --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,21 @@ + + + "Bu dosya odadan kaldırılır ve üyeler bu dosyaya erişemez." + "Dosyayı sil?" + "İnternet bağlantınızı kontrol edin ve tekrar deneyin." + "Bu odaya yüklenen belgeler, ses dosyaları ve sesli mesajlar burada gösterilecektir." + "Henüz yüklenen dosya yok" + "Dosyalar yükleniyor…" + "Medya yükleniyor…" + "Dosyalar" + "Medya" + "Bu odaya yüklenen resimler ve videolar burada gösterilecektir." + "Henüz yüklenen medya yok" + "Medya ve dosyalar" + "Dosya biçimi" + "Dosya adı" + "Gösterilecek daha fazla dosya yok" + "Gösterilecek daha fazla medya yok" + "Yükleyen:" + "Yüklendi" + 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 33bb4ed210d..45e1e1703be 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-uk/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-uk/translations.xml @@ -14,8 +14,8 @@ "Медіа та файли" "Формат файлу" "Назва файлу" - "Цей файл буде вилучено з кімнати, і учасники не матимуть доступу до нього." - "Видалити файл?" + "Більше немає файлів для показу" + "Більше немає медіа для показу" "Вивантажено користувачем" "Вивантажено" diff --git a/libraries/mediaviewer/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-zh-rTW/translations.xml index 41c45054193..4cbe579e00e 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-zh-rTW/translations.xml @@ -14,8 +14,8 @@ "媒體與檔案" "檔案格式" "檔案名稱" - "此檔案將會從聊天室中移除,成員將無法存取該檔案。" - "刪除檔案?" + "無可顯示的檔案" + "無可顯示的媒體" "上傳者:" "上傳於" 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 0a8b73eb544..08b33993dc2 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-zh/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-zh/translations.xml @@ -16,8 +16,6 @@ "文件名" "没有更多文件可显示了" "没有更多媒体可显示了" - "此文件将从房间中删除,并且成员将无法访问它。" - "删除文件?" "上传者:" "上传于" diff --git a/libraries/mediaviewer/impl/src/main/res/values/localazy.xml b/libraries/mediaviewer/impl/src/main/res/values/localazy.xml index 52a22183318..2982f8002fc 100644 --- a/libraries/mediaviewer/impl/src/main/res/values/localazy.xml +++ b/libraries/mediaviewer/impl/src/main/res/values/localazy.xml @@ -16,8 +16,6 @@ "File name" "No more files to show" "No more media to show" - "This file will be removed from the room and members won’t have access to it." - "Delete file?" "Uploaded by" "Uploaded on" 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 40e6721c10d..16e6057df64 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 @@ -31,7 +31,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessage import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent -import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType @@ -47,6 +46,7 @@ import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation @@ -67,10 +67,8 @@ class DefaultEventItemFactoryTest { aPollContent(), aProfileChangeMessageContent(), RedactedContent, - RoomMembershipContent( + aRoomMembershipContent( userId = A_USER_ID, - userDisplayName = null, - change = null, ), StateContent("", OtherState.RoomCreate), aStickerContent( diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest.kt index a2ab9b162f6..9edc2d0a75e 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.mediaviewer.impl.datasource import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage import kotlinx.coroutines.test.runTest import org.junit.Test @@ -18,13 +18,14 @@ class DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest { @Test fun `createFor should create a TimelineMediaGalleryDataSource`() = runTest { val sut = DefaultFocusedTimelineMediaGalleryDataSourceFactory( - room = FakeMatrixRoom(), + room = FakeJoinedRoom(), timelineMediaItemsFactory = createTimelineMediaItemsFactory(), mediaItemsPostProcessor = MediaItemsPostProcessor(), ) val result = sut.createFor( eventId = AN_EVENT_ID, mediaItem = aMediaItemImage(), + onlyPinnedEvents = false, ) assertThat(result).isInstanceOf(TimelineMediaGalleryDataSource::class.java) } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedMediaTimelineTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedMediaTimelineTest.kt index 1d4fd3adbef..1f2ab92dfcc 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedMediaTimelineTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FocusedMediaTimelineTest.kt @@ -9,10 +9,11 @@ package io.element.android.libraries.mediaviewer.impl.datasource import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.timeline.Timeline import io.element.android.libraries.matrix.test.AN_EVENT_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.libraries.mediaviewer.impl.model.MediaItem @@ -75,11 +76,11 @@ class FocusedMediaTimelineTest { @Test fun `getTimeline returns the timeline provided by the room`() = runTest { - val mediaTimelineResult = lambdaRecorder> { + val createTimelineResult = lambdaRecorder> { Result.success(FakeTimeline()) } - val room = FakeMatrixRoom( - mediaTimelineResult = mediaTimelineResult, + val room = FakeJoinedRoom( + createTimelineResult = createTimelineResult, ) val sut = createFocusedMediaTimeline( room = room, @@ -87,16 +88,36 @@ class FocusedMediaTimelineTest { ) val timeline = sut.getTimeline() assertThat(timeline.isSuccess).isTrue() - mediaTimelineResult.assertions().isCalledOnce().with(value(AN_EVENT_ID)) + createTimelineResult.assertions().isCalledOnce().with(value(CreateTimelineParams.MediaOnlyFocused(AN_EVENT_ID))) + } + + @Test + fun `getTimeline returns the timeline provided by the room for pinned Events`() = runTest { + val createTimelineResult = lambdaRecorder> { + Result.success(FakeTimeline()) + } + val room = FakeJoinedRoom( + createTimelineResult = createTimelineResult, + ) + val sut = createFocusedMediaTimeline( + room = room, + eventId = AN_EVENT_ID, + onlyPinnedEvent = true, + ) + val timeline = sut.getTimeline() + assertThat(timeline.isSuccess).isTrue() + createTimelineResult.assertions().isCalledOnce().with(value(CreateTimelineParams.PinnedOnly)) } private fun createFocusedMediaTimeline( - room: MatrixRoom = FakeMatrixRoom(), + room: JoinedRoom = FakeJoinedRoom(), eventId: EventId = AN_EVENT_ID, initialMediaItem: MediaItem.Event = aMediaItemImage(), + onlyPinnedEvent: Boolean = false, ) = FocusedMediaTimeline( room = room, eventId = eventId, initialMediaItem = initialMediaItem, + onlyPinnedEvents = onlyPinnedEvent, ) } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/LiveMediaTimelineTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/LiveMediaTimelineTest.kt index 95b62a5824f..4d59b88c09d 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/LiveMediaTimelineTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/LiveMediaTimelineTest.kt @@ -8,10 +8,10 @@ package io.element.android.libraries.mediaviewer.impl.datasource import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.timeline.Timeline -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -28,26 +28,26 @@ class LiveMediaTimelineTest { @Test fun `getTimeline returns the timeline provided by the room, then from cache`() = runTest { - val mediaTimelineResult = lambdaRecorder> { + val createTimelineResult = lambdaRecorder> { Result.success(FakeTimeline()) } - val room = FakeMatrixRoom( - mediaTimelineResult = mediaTimelineResult, + val room = FakeJoinedRoom( + createTimelineResult = createTimelineResult, ) val sut = createLiveMediaTimeline( room = room, ) val timeline = sut.getTimeline() assertThat(timeline.isSuccess).isTrue() - mediaTimelineResult.assertions().isCalledOnce().with(value(null)) + createTimelineResult.assertions().isCalledOnce().with(value(CreateTimelineParams.MediaOnly)) val timeline2 = sut.getTimeline() assertThat(timeline2.isSuccess).isTrue() // No called another time - mediaTimelineResult.assertions().isCalledOnce() + createTimelineResult.assertions().isCalledOnce() } private fun createLiveMediaTimeline( - room: MatrixRoom = FakeMatrixRoom(), + room: JoinedRoom = FakeJoinedRoom(), ) = LiveMediaTimeline( room = room, ) 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 736fd309f36..20208a55dc3 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 @@ -16,7 +16,7 @@ import io.element.android.libraries.dateformatter.test.FakeDateFormatter 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.ThumbnailInfo -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -28,7 +28,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_UNIQUE_ID import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem @@ -56,8 +56,8 @@ class TimelineMediaGalleryDataSourceTest { fun `test - not started TimelineMediaGalleryDataSource emits no events`() = runTest { val fakeTimeline = FakeTimeline() val sut = createTimelineMediaGalleryDataSource( - room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + room = FakeJoinedRoom( + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) @@ -74,8 +74,8 @@ class TimelineMediaGalleryDataSourceTest { val fakeTimeline = FakeTimeline() runTest { val sut = createTimelineMediaGalleryDataSource( - room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + room = FakeJoinedRoom( + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) @@ -111,8 +111,8 @@ class TimelineMediaGalleryDataSourceTest { paginateLambda = paginateLambdaRecorder } val sut = createTimelineMediaGalleryDataSource( - room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + room = FakeJoinedRoom( + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) @@ -134,8 +134,8 @@ class TimelineMediaGalleryDataSourceTest { redactEventLambda = redactEventLambdaRecorder } val sut = createTimelineMediaGalleryDataSource( - room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + room = FakeJoinedRoom( + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) @@ -153,8 +153,8 @@ class TimelineMediaGalleryDataSourceTest { @Test fun `test - failing to load timeline should emit an error`() = runTest { val sut = createTimelineMediaGalleryDataSource( - room = FakeMatrixRoom( - mediaTimelineResult = { Result.failure(AN_EXCEPTION) }, + room = FakeJoinedRoom( + createTimelineResult = { Result.failure(AN_EXCEPTION) }, roomCoroutineScope = backgroundScope, ) ) @@ -175,8 +175,8 @@ class TimelineMediaGalleryDataSourceTest { timelineItems = timelineItems, ) val sut = createTimelineMediaGalleryDataSource( - room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(fakeTimeline) }, + room = FakeJoinedRoom( + createTimelineResult = { Result.success(fakeTimeline) }, roomCoroutineScope = backgroundScope, ) ) @@ -256,7 +256,7 @@ class TimelineMediaGalleryDataSourceTest { } private fun TestScope.createTimelineMediaGalleryDataSource( - room: MatrixRoom = FakeMatrixRoom( + room: JoinedRoom = FakeJoinedRoom( liveTimeline = FakeTimeline(), ), ): TimelineMediaGalleryDataSource { 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 c71cdb8573c..1596104c46d 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 @@ -19,6 +19,7 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -60,7 +61,7 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setSafeContent { MediaDeleteConfirmationBottomSheet( state = state, onDelete = onDelete, 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 b74ae07b8b2..bd4373a4203 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 @@ -18,6 +18,7 @@ import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -30,6 +31,7 @@ class MediaDetailsBottomSheetTest { val rule = createAndroidComposeRule() @Test + @Config(qualifiers = "h1024dp") fun `clicking on View in timeline invokes expected callback`() { val state = aMediaDetailsBottomSheetState() ensureCalledOnceWithParam(state.eventId) { callback -> @@ -42,6 +44,7 @@ class MediaDetailsBottomSheetTest { } @Test + @Config(qualifiers = "h1024dp") fun `clicking on Share invokes expected callback`() { val state = aMediaDetailsBottomSheetState() ensureCalledOnceWithParam(state.eventId) { callback -> @@ -54,6 +57,7 @@ class MediaDetailsBottomSheetTest { } @Test + @Config(qualifiers = "h1024dp") fun `clicking on Save invokes expected callback`() { val state = aMediaDetailsBottomSheetState() ensureCalledOnceWithParam(state.eventId) { callback -> @@ -100,7 +104,7 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setSafeContent { MediaDetailsBottomSheet( state = state, onViewInTimeline = onViewInTimeline, 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 1a43bbacf6d..80005facd40 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 @@ -12,14 +12,16 @@ import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME 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.media.FakeMatrixMediaLoader -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource @@ -50,9 +52,9 @@ class MediaGalleryPresenterTest { mediaGalleryDataSource = FakeMediaGalleryDataSource( startLambda = startLambda, ), - room = FakeMatrixRoom( - displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom(initialRoomInfo = aRoomInfo(name = A_ROOM_NAME)), + createTimelineResult = { Result.success(FakeTimeline()) }, ) ) presenter.test { @@ -69,9 +71,9 @@ class MediaGalleryPresenterTest { @Test fun `present - change mode`() = runTest { val presenter = createMediaGalleryPresenter( - room = FakeMatrixRoom( - displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom(initialRoomInfo = aRoomInfo(name = A_ROOM_NAME)), + createTimelineResult = { Result.success(FakeTimeline()) }, ) ) presenter.test { @@ -98,11 +100,13 @@ class MediaGalleryPresenterTest { private suspend fun `present - bottom sheet state - own message`(canDeleteOwn: Boolean) { val presenter = createMediaGalleryPresenter( - room = FakeMatrixRoom( - sessionId = A_USER_ID, - displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, - canRedactOwnResult = { Result.success(canDeleteOwn) } + room = FakeJoinedRoom( + createTimelineResult = { Result.success(FakeTimeline()) }, + baseRoom = FakeBaseRoom( + sessionId = A_USER_ID, + initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), + canRedactOwnResult = { Result.success(canDeleteOwn) } + ), ) ) presenter.test { @@ -141,11 +145,13 @@ class MediaGalleryPresenterTest { private suspend fun `present - bottom sheet state - other message`(canDeleteOther: Boolean) { val presenter = createMediaGalleryPresenter( - room = FakeMatrixRoom( + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( sessionId = A_USER_ID, - displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, - canRedactOtherResult = { Result.success(canDeleteOther) } + initialRoomInfo = aRoomInfo(name = A_ROOM_NAME), + canRedactOtherResult = { Result.success(canDeleteOther) }, + ), + createTimelineResult = { Result.success(FakeTimeline()) } ) ) presenter.test { @@ -175,9 +181,9 @@ class MediaGalleryPresenterTest { @Test fun `present - delete bottom sheet`() = runTest { val presenter = createMediaGalleryPresenter( - room = FakeMatrixRoom( - displayName = A_ROOM_NAME, - mediaTimelineResult = { Result.success(FakeTimeline()) }, + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom(initialRoomInfo = aRoomInfo(name = A_ROOM_NAME)), + createTimelineResult = { Result.success(FakeTimeline()) }, ) ) presenter.test { @@ -243,8 +249,8 @@ class MediaGalleryPresenterTest { onViewInTimelineClickLambda = onViewInTimelineClickLambda, ) val presenter = createMediaGalleryPresenter( - room = FakeMatrixRoom( - mediaTimelineResult = { Result.success(FakeTimeline()) }, + room = FakeJoinedRoom( + createTimelineResult = { Result.success(FakeTimeline()) }, ), navigator = navigator, ) @@ -283,7 +289,7 @@ class MediaGalleryPresenterTest { localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), navigator: MediaGalleryNavigator = FakeMediaGalleryNavigator(), - room: MatrixRoom = FakeMatrixRoom( + room: JoinedRoom = FakeJoinedRoom( liveTimeline = FakeTimeline(), ), ): MediaGalleryPresenter { diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt index e5e811fb337..e2399eae340 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt @@ -17,9 +17,7 @@ class FileExtensionExtractorWithValidationTest { @Test fun `test FileExtensionExtractor with validation OK`() { val sut = FileExtensionExtractorWithValidation() - // The result should be txt, but with Robolectric, - // MimeTypeMap.getSingleton().hasExtension() always returns false - assertThat(sut.extractFromName("test.txt")).isEqualTo("bin") + assertThat(sut.extractFromName("test.txt")).isEqualTo("txt") } @Test 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 b49c174d21e..6eced261b18 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 @@ -137,7 +137,7 @@ class MediaViewerDataSourceTest { fun `test dataFlow with data galleryMode image`() = runTest { val galleryDataSource = FakeMediaGalleryDataSource() val sut = createMediaViewerDataSource( - mode = MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA), galleryDataSource = galleryDataSource, ) sut.dataFlow().test { @@ -159,7 +159,7 @@ class MediaViewerDataSourceTest { fun `test dataFlow with data galleryMode files`() = runTest { val galleryDataSource = FakeMediaGalleryDataSource() val sut = createMediaViewerDataSource( - mode = MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.MEDIA), galleryDataSource = galleryDataSource, ) sut.dataFlow().test { @@ -265,7 +265,7 @@ class MediaViewerDataSourceTest { } private fun TestScope.createMediaViewerDataSource( - mode: MediaViewerMode = MediaViewerMode.TimelineImagesAndVideos, + mode: MediaViewerMode = MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA), galleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(), mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(), localMediaFactory: LocalMediaFactory = FakeLocalMediaFactory(mockMediaUrl), 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 03d66992bf4..a4f72aa4742 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 @@ -15,7 +15,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncData 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.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom 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 @@ -24,7 +24,8 @@ 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 import io.element.android.libraries.matrix.test.media.aMediaSource -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.anApkMediaInfo @@ -77,10 +78,12 @@ class MediaViewerPresenterTest { @Test fun `present - initial state null Event`() = runTest { val presenter = createMediaViewerPresenter( - room = FakeMatrixRoom( + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, ) ) + ) presenter.test { val initialState = awaitFirstItem() assertThat(initialState.listData).isEmpty() @@ -95,10 +98,12 @@ class MediaViewerPresenterTest { fun `present - initial state cannot show info`() = runTest { val presenter = createMediaViewerPresenter( canShowInfo = false, - room = FakeMatrixRoom( + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, ) ) + ) presenter.test { val initialState = awaitFirstItem() assertThat(initialState.listData).isEmpty() @@ -113,10 +118,12 @@ class MediaViewerPresenterTest { fun `present - initial state Event`() = runTest { val presenter = createMediaViewerPresenter( eventId = AN_EVENT_ID, - room = FakeMatrixRoom( + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, ) ) + ) presenter.test { val initialState = awaitFirstItem() assertThat(initialState.listData).isEmpty() @@ -131,11 +138,13 @@ class MediaViewerPresenterTest { fun `present - initial state Event from other`() = runTest { val presenter = createMediaViewerPresenter( eventId = AN_EVENT_ID, - room = FakeMatrixRoom( + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( sessionId = A_SESSION_ID_2, canRedactOtherResult = { Result.success(false) }, ) ) + ) presenter.test { val initialState = awaitFirstItem() assertThat(initialState.listData).isEmpty() @@ -216,9 +225,9 @@ class MediaViewerPresenterTest { ) val presenter = createMediaViewerPresenter( mediaGalleryDataSource = mediaGalleryDataSource, - room = FakeMatrixRoom( + room = FakeJoinedRoom(baseRoom = FakeBaseRoom( canRedactOwnResult = { Result.success(true) }, - ) + )) ) val anImage = aMediaItemImage( mediaSourceUrl = aUrl, @@ -432,9 +441,9 @@ class MediaViewerPresenterTest { startLambda = { }, ) val presenter = createMediaViewerPresenter( - room = FakeMatrixRoom( + room = FakeJoinedRoom( liveTimeline = timeline, - canRedactOwnResult = { Result.success(true) }, + baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), ), mediaGalleryDataSource = mediaGalleryDataSource, mediaViewerNavigator = FakeMediaViewerNavigator( @@ -519,7 +528,7 @@ class MediaViewerPresenterTest { @Test fun `present - snackbar displayed when there is no more items forward images and videos`() { `present - snackbar displayed when there is no more items forward`( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA), expectedSnackbarResId = R.string.screen_media_details_no_more_media_to_show, ) } @@ -527,7 +536,7 @@ class MediaViewerPresenterTest { @Test fun `present - snackbar displayed when there is no more items forward files and audio`() { `present - snackbar displayed when there is no more items forward`( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.MEDIA), expectedSnackbarResId = R.string.screen_media_details_no_more_files_to_show, ) } @@ -547,7 +556,7 @@ class MediaViewerPresenterTest { awaitFirstItem() mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( - if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), @@ -568,7 +577,7 @@ class MediaViewerPresenterTest { // data source claims that there is no more items to load forward mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( - if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), fileItems = persistentListOf(anImage, aBackwardLoadingIndicator), @@ -590,7 +599,7 @@ class MediaViewerPresenterTest { @Test fun `present - snackbar displayed when there is no more items backward images and videos`() { `present - snackbar displayed when there is no more items backward`( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA), expectedSnackbarResId = R.string.screen_media_details_no_more_media_to_show, ) } @@ -598,7 +607,7 @@ class MediaViewerPresenterTest { @Test fun `present - snackbar displayed when there is no more items backward files and audio`() { `present - snackbar displayed when there is no more items backward`( - mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios, + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.MEDIA), expectedSnackbarResId = R.string.screen_media_details_no_more_files_to_show, ) } @@ -618,7 +627,7 @@ class MediaViewerPresenterTest { awaitFirstItem() mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( - if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), @@ -640,7 +649,7 @@ class MediaViewerPresenterTest { // data source claims that there is no more items to load backward mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( - if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), fileItems = persistentListOf(aForwardLoadingIndicator, anImage), @@ -736,8 +745,8 @@ class MediaViewerPresenterTest { ) val presenter = createMediaViewerPresenter( mediaViewerNavigator = navigator, - room = FakeMatrixRoom( - canRedactOwnResult = { Result.success(true) }, + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), ) ) presenter.test { @@ -766,7 +775,7 @@ class MediaViewerPresenterTest { ), canShowInfo: Boolean = true, mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(), - room: MatrixRoom = FakeMatrixRoom( + room: JoinedRoom = FakeJoinedRoom( liveTimeline = FakeTimeline(), ), ): MediaViewerPresenter { 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 bfc294098b5..6423c3f0b73 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 @@ -26,11 +26,13 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.setSafeContent import io.mockk.mockk 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 MediaViewerViewTest { @@ -107,6 +109,7 @@ class MediaViewerViewTest { } @Test + @Config(qualifiers = "h1024dp") fun `clicking on save emit expected Event`() { val data = aMediaViewerPageData() testBottomSheetAction( @@ -117,6 +120,7 @@ class MediaViewerViewTest { } @Test + @Config(qualifiers = "h1024dp") fun `clicking on share emit expected Event`() { val data = aMediaViewerPageData() testBottomSheetAction( @@ -249,9 +253,11 @@ private fun AndroidComposeTestRule.setMedia state: MediaViewerState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setSafeContent { MediaViewerView( state = state, + audioFocus = null, + textFileViewer = { _, _ -> }, onBackClick = onBackClick, ) } diff --git a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt deleted file mode 100644 index e194f4d509b..00000000000 --- a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.oidc.api - -import android.app.Activity -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node - -interface OidcEntryPoint { - fun canUseCustomTab(): Boolean - fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) - fun openUrlInExternalApp(activity: Activity, url: String) - fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node -} diff --git a/libraries/oidc/impl/build.gradle.kts b/libraries/oidc/impl/build.gradle.kts index 2d4699c2463..c0f8e9d1d62 100644 --- a/libraries/oidc/impl/build.gradle.kts +++ b/libraries/oidc/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { api(projects.libraries.oidc.api) testImplementation(libs.test.junit) + testImplementation(libs.test.robolectric) testImplementation(libs.androidx.test.ext.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt deleted file mode 100644 index 077331dbdf5..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl - -import android.content.Context -import androidx.browser.customtabs.CustomTabsClient -import io.element.android.libraries.di.ApplicationContext -import javax.inject.Inject - -class CustomTabAvailabilityChecker @Inject constructor( - @ApplicationContext private val context: Context, -) { - /** - * Return true if the device supports Custom tab, i.e. there is an third party app with - * CustomTab support (ex: Chrome, Firefox, etc.). - */ - fun supportCustomTab(): Boolean { - val packageName = CustomTabsClient.getPackageName(context, null) - return packageName != null - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt similarity index 95% rename from libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt index 39b67957255..e975cde261b 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oidc.impl.customtab +package io.element.android.libraries.oidc.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt deleted file mode 100644 index c4f533fc9cd..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.oidc.impl - -import android.app.Activity -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab -import io.element.android.libraries.androidutils.system.openUrlInExternalApp -import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.auth.OidcDetails -import io.element.android.libraries.oidc.api.OidcEntryPoint -import io.element.android.libraries.oidc.impl.webview.OidcNode -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultOidcEntryPoint @Inject constructor( - private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, -) : OidcEntryPoint { - override fun canUseCustomTab(): Boolean { - return customTabAvailabilityChecker.supportCustomTab() - } - - override fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) { - assert(canUseCustomTab()) { "Custom tab is not supported in this device." } - activity.openUrlInChromeCustomTab(null, darkTheme, url) - } - - override fun openUrlInExternalApp(activity: Activity, url: String) { - activity.openUrlInExternalApp(url) - } - - override fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node { - assert(!canUseCustomTab()) { "Custom tab should be used instead of the fallback node." } - val inputs = OidcNode.Inputs(OidcDetails(url)) - return parentNode.createNode(buildContext, listOf(inputs)) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt index 80e784264d1..1a41492df63 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt @@ -7,25 +7,34 @@ package io.element.android.libraries.oidc.impl -import io.element.android.libraries.matrix.api.auth.OidcConfig +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider import io.element.android.libraries.oidc.api.OidcAction import javax.inject.Inject +fun interface OidcUrlParser { + fun parse(url: String): OidcAction? +} + /** * Simple parser for oidc url interception. * TODO Find documentation about the format. */ -class OidcUrlParser @Inject constructor() { +@ContributesBinding(AppScope::class) +class DefaultOidcUrlParser @Inject constructor( + private val oidcRedirectUrlProvider: OidcRedirectUrlProvider, +) : OidcUrlParser { /** * Return a OidcAction, or null if the url is not a OidcUrl. * Note: * When user press button "Cancel", we get the url: - * `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO` + * `io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO` * On success, we get: - * `io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB` + * `io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB` */ - fun parse(url: String): OidcAction? { - if (url.startsWith(OidcConfig.REDIRECT_URI).not()) return null + 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) diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt deleted file mode 100644 index 42d1391e9f9..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl.customtab - -import android.app.Activity -import android.content.ComponentName -import android.content.Context -import android.net.Uri -import androidx.browser.customtabs.CustomTabsClient -import androidx.browser.customtabs.CustomTabsServiceConnection -import androidx.browser.customtabs.CustomTabsSession -import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab -import io.element.android.libraries.di.ApplicationContext -import javax.inject.Inject - -class CustomTabHandler @Inject constructor( - @ApplicationContext private val context: Context, -) { - private var customTabsSession: CustomTabsSession? = null - private var customTabsClient: CustomTabsClient? = null - private var customTabsServiceConnection: CustomTabsServiceConnection? = null - - fun prepareCustomTab(url: String) { - val packageName = CustomTabsClient.getPackageName(context, null) - - // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device - if (packageName != null) { - customTabsServiceConnection = object : CustomTabsServiceConnection() { - override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { - customTabsClient = client.apply { warmup(0L) } - prefetchUrl(url) - } - - override fun onServiceDisconnected(name: ComponentName?) { - } - } - .also { - CustomTabsClient.bindCustomTabsService( - context, - // Despite the API, packageName cannot be null - packageName, - it - ) - } - } - } - - private fun prefetchUrl(url: String) { - if (customTabsSession == null) { - customTabsSession = customTabsClient?.newSession(null) - } - - customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) - } - - fun disposeCustomTab() { - customTabsServiceConnection?.let { context.unbindService(it) } - customTabsServiceConnection = null - } - - fun open(activity: Activity, darkTheme: Boolean, url: String) { - activity.openUrlInChromeCustomTab(customTabsSession, darkTheme, url) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt deleted file mode 100644 index 165ef00ebcf..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl.webview - -import io.element.android.libraries.oidc.api.OidcAction - -sealed interface OidcEvents { - data object Cancel : OidcEvents - data class OidcActionEvent(val oidcAction: OidcAction) : OidcEvents - data object ClearError : OidcEvents -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt deleted file mode 100644 index 91c6bec28b2..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl.webview - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.auth.OidcDetails - -@ContributesNode(AppScope::class) -class OidcNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - presenterFactory: OidcPresenter.Factory, -) : Node(buildContext, plugins = plugins) { - data class Inputs( - val oidcDetails: OidcDetails, - ) : NodeInputs - - private val inputs: Inputs = inputs() - private val presenter = presenterFactory.create(inputs.oidcDetails) - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - OidcView( - state = state, - modifier = modifier, - onNavigateBack = ::navigateUp, - ) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt deleted file mode 100644 index f8144d69a6b..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl.webview - -import androidx.compose.runtime.Composable -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 dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.auth.OidcDetails -import io.element.android.libraries.oidc.api.OidcAction -import kotlinx.coroutines.launch - -class OidcPresenter @AssistedInject constructor( - @Assisted private val oidcDetails: OidcDetails, - private val authenticationService: MatrixAuthenticationService, -) : Presenter { - @AssistedFactory - interface Factory { - fun create(oidcDetails: OidcDetails): OidcPresenter - } - - @Composable - override fun present(): OidcState { - var requestState: AsyncAction by remember { - mutableStateOf(AsyncAction.Uninitialized) - } - val localCoroutineScope = rememberCoroutineScope() - - fun handleCancel() { - requestState = AsyncAction.Loading - localCoroutineScope.launch { - authenticationService.cancelOidcLogin() - .fold( - onSuccess = { - // Then go back - requestState = AsyncAction.Success(Unit) - }, - onFailure = { - requestState = AsyncAction.Failure(it) - } - ) - } - } - - fun handleSuccess(url: String) { - requestState = AsyncAction.Loading - localCoroutineScope.launch { - authenticationService.loginWithOidc(url) - .onFailure { - requestState = AsyncAction.Failure(it) - } - // On success, the node tree will be updated, there is nothing to do - } - } - - fun handleAction(action: OidcAction) { - when (action) { - OidcAction.GoBack -> handleCancel() - is OidcAction.Success -> handleSuccess(action.url) - } - } - - fun handleEvents(event: OidcEvents) { - when (event) { - OidcEvents.Cancel -> handleCancel() - is OidcEvents.OidcActionEvent -> handleAction(event.oidcAction) - OidcEvents.ClearError -> requestState = AsyncAction.Uninitialized - } - } - - return OidcState( - oidcDetails = oidcDetails, - requestState = requestState, - eventSink = ::handleEvents - ) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt deleted file mode 100644 index 4358d70179a..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl.webview - -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.auth.OidcDetails - -data class OidcState( - val oidcDetails: OidcDetails, - val requestState: AsyncAction, - val eventSink: (OidcEvents) -> Unit -) diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt deleted file mode 100644 index 52d90a86a57..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl.webview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.auth.OidcDetails - -open class OidcStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aOidcState(), - aOidcState().copy(requestState = AsyncAction.Loading), - ) -} - -fun aOidcState() = OidcState( - oidcDetails = aOidcDetails(), - requestState = AsyncAction.Uninitialized, - eventSink = {} -) - -fun aOidcDetails() = OidcDetails( - url = "aUrl", -) diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt deleted file mode 100644 index 6d429ca088e..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl.webview - -import android.annotation.SuppressLint -import android.webkit.WebView -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.padding -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.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.viewinterop.AndroidView -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.designsystem.components.async.AsyncActionView -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 io.element.android.libraries.oidc.impl.OidcUrlParser - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun OidcView( - state: OidcState, - onNavigateBack: () -> Unit, - modifier: Modifier = Modifier, -) { - val isPreview = LocalInspectionMode.current - val oidcUrlParser = remember { OidcUrlParser() } - var webView by remember { mutableStateOf(null) } - fun shouldOverrideUrl(url: String): Boolean { - val action = oidcUrlParser.parse(url) - if (action != null) { - state.eventSink.invoke(OidcEvents.OidcActionEvent(action)) - return true - } - return false - } - - val oidcWebViewClient = remember { - OidcWebViewClient(::shouldOverrideUrl) - } - - fun onBack() { - if (webView?.canGoBack().orFalse()) { - webView?.goBack() - } else { - // To properly cancel Oidc login - state.eventSink.invoke(OidcEvents.Cancel) - } - } - - BackHandler { onBack() } - - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = {}, - navigationIcon = { - BackButton(onClick = ::onBack) - }, - ) - } - ) { contentPadding -> - AndroidView( - modifier = Modifier.padding(contentPadding), - factory = { context -> - WebView(context).apply { - if (!isPreview) { - webViewClient = oidcWebViewClient - settings.apply { - @SuppressLint("SetJavaScriptEnabled") - javaScriptEnabled = true - allowContentAccess = true - allowFileAccess = true - @Suppress("DEPRECATION") - databaseEnabled = true - domStorageEnabled = true - } - loadUrl(state.oidcDetails.url) - } - }.also { - webView = it - } - } - ) - - AsyncActionView( - async = state.requestState, - onSuccess = { onNavigateBack() }, - onErrorDismiss = { state.eventSink(OidcEvents.ClearError) } - ) - } -} - -@PreviewsDayNight -@Composable -internal fun OidcViewPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) = ElementPreview { - OidcView( - state = state, - onNavigateBack = {}, - ) -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt deleted file mode 100644 index 0d00c790fb5..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl.webview - -import android.webkit.WebResourceRequest -import android.webkit.WebView -import android.webkit.WebViewClient - -class OidcWebViewClient( - private val eventListener: WebViewEventListener, -) : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { - return shouldOverrideUrl(request.url.toString()) - } - - @Deprecated("Deprecated in Java") - override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { - return shouldOverrideUrl(url) - } - - private fun shouldOverrideUrl(url: String): Boolean { - return eventListener.shouldOverrideUrlLoading(url) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt deleted file mode 100644 index def6e54c716..00000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl.webview - -fun interface WebViewEventListener { - /** - * Triggered when a Webview loads an url. - * - * @param url The url about to be rendered. - * @return true if the method needs to manage some custom handling - */ - fun shouldOverrideUrlLoading(url: String): Boolean -} diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt new file mode 100644 index 00000000000..3b56f28c5a6 --- /dev/null +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt @@ -0,0 +1,33 @@ +/* + * 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.impl + +import com.google.common.truth.Truth.assertThat +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 DefaultOidcActionFlowTest { + @Test + fun `collect gets all the posted events`() = runTest { + val data = mutableListOf() + val sut = DefaultOidcActionFlow() + backgroundScope.launch { + sut.collect { action -> + data.add(action) + } + } + sut.post(OidcAction.GoBack) + delay(1) + sut.reset() + delay(1) + assertThat(data).containsExactly(OidcAction.GoBack, null) + } +} diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolverTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolverTest.kt new file mode 100644 index 00000000000..e48e0c2e1ee --- /dev/null +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolverTest.kt @@ -0,0 +1,69 @@ +/* + * 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.impl + +import android.app.Activity +import android.content.Intent +import androidx.core.net.toUri +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider +import io.element.android.libraries.oidc.api.OidcAction +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class DefaultOidcIntentResolverTest { + @Test + 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(OidcAction.GoBack) + } + + @Test + 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( + OidcAction.Success( + url = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" + ) + ) + } + + @Test + 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() + } + assertThrows(IllegalStateException::class.java) { + sut.resolve(intent) + } + } + + private fun createDefaultOidcIntentResolver(): DefaultOidcIntentResolver { + return DefaultOidcIntentResolver( + oidcUrlParser = DefaultOidcUrlParser( + oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), + ), + ) + } +} diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcUrlParserTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcUrlParserTest.kt new file mode 100644 index 00000000000..e40424ca0ef --- /dev/null +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcUrlParserTest.kt @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package io.element.android.libraries.oidc.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.auth.FAKE_REDIRECT_URL +import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider +import io.element.android.libraries.oidc.api.OidcAction +import org.junit.Assert +import org.junit.Test + +class DefaultOidcUrlParserTest { + @Test + fun `test empty url`() { + val sut = createDefaultOidcUrlParser() + assertThat(sut.parse("")).isNull() + } + + @Test + fun `test regular url`() { + val sut = createDefaultOidcUrlParser() + assertThat(sut.parse("https://matrix.org")).isNull() + } + + @Test + fun `test cancel url`() { + val sut = createDefaultOidcUrlParser() + val aCancelUrl = "$FAKE_REDIRECT_URL?error=access_denied&state=IFF1UETGye2ZA8pO" + assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack) + } + + @Test + fun `test success url`() { + val sut = createDefaultOidcUrlParser() + val aSuccessUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" + assertThat(sut.parse(aSuccessUrl)).isEqualTo(OidcAction.Success(aSuccessUrl)) + } + + @Test + fun `test unknown url`() { + val sut = createDefaultOidcUrlParser() + val anUnknownUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&goat=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" + Assert.assertThrows(IllegalStateException::class.java) { + assertThat(sut.parse(anUnknownUrl)) + } + } + + private fun createDefaultOidcUrlParser(): DefaultOidcUrlParser { + return DefaultOidcUrlParser( + oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), + ) + } +} diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt deleted file mode 100644 index 7c467162f9d..00000000000 --- a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.oidc.impl - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.auth.OidcConfig -import io.element.android.libraries.oidc.api.OidcAction -import org.junit.Assert -import org.junit.Test - -class OidcUrlParserTest { - @Test - fun `test empty url`() { - val sut = OidcUrlParser() - assertThat(sut.parse("")).isNull() - } - - @Test - fun `test regular url`() { - val sut = OidcUrlParser() - assertThat(sut.parse("https://matrix.org")).isNull() - } - - @Test - fun `test cancel url`() { - val sut = OidcUrlParser() - val aCancelUrl = OidcConfig.REDIRECT_URI + "?error=access_denied&state=IFF1UETGye2ZA8pO" - assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack) - } - - @Test - fun `test success url`() { - val sut = OidcUrlParser() - val aSuccessUrl = OidcConfig.REDIRECT_URI + "?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" - assertThat(sut.parse(aSuccessUrl)).isEqualTo(OidcAction.Success(aSuccessUrl)) - } - - @Test - fun `test unknown url`() { - val sut = OidcUrlParser() - val anUnknownUrl = OidcConfig.REDIRECT_URI + "?state=IFF1UETGye2ZA8pO&goat=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" - Assert.assertThrows(IllegalStateException::class.java) { - assertThat(sut.parse(anUnknownUrl)) - } - } -} diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt deleted file mode 100644 index 433c77f73a2..00000000000 --- a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package io.element.android.libraries.oidc.impl.webview - -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.test.A_THROWABLE -import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA -import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.tests.testutils.WarmUpRule -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class OidcPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state`() = runTest { - val presenter = OidcPresenter( - A_OIDC_DATA, - FakeMatrixAuthenticationService(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.oidcDetails).isEqualTo(A_OIDC_DATA) - assertThat(initialState.requestState).isEqualTo(AsyncAction.Uninitialized) - } - } - - @Test - fun `present - go back`() = runTest { - val presenter = OidcPresenter( - A_OIDC_DATA, - FakeMatrixAuthenticationService(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.Cancel) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - val finalState = awaitItem() - assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit)) - } - } - - @Test - fun `present - go back with failure`() = runTest { - val authenticationService = FakeMatrixAuthenticationService() - val presenter = OidcPresenter( - A_OIDC_DATA, - authenticationService, - ) - authenticationService.givenOidcCancelError(A_THROWABLE) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.Cancel) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - val finalState = awaitItem() - assertThat(finalState.requestState).isEqualTo(AsyncAction.Failure(A_THROWABLE)) - // Note: in real life I do not think this can happen, and the app should not block the user. - } - } - - @Test - fun `present - user cancels from webview`() = runTest { - val presenter = OidcPresenter( - A_OIDC_DATA, - FakeMatrixAuthenticationService(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.GoBack)) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - val finalState = awaitItem() - assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit)) - } - } - - @Test - fun `present - login success`() = runTest { - val presenter = OidcPresenter( - A_OIDC_DATA, - FakeMatrixAuthenticationService(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL"))) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - // In this case, no success, the session is created and the node get destroyed. - } - } - - @Test - fun `present - login error`() = runTest { - val authenticationService = FakeMatrixAuthenticationService() - val presenter = OidcPresenter( - A_OIDC_DATA, - authenticationService, - ) - authenticationService.givenLoginError(A_THROWABLE) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL"))) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - val errorState = awaitItem() - assertThat(errorState.requestState).isEqualTo(AsyncAction.Failure(A_THROWABLE)) - errorState.eventSink.invoke(OidcEvents.ClearError) - val finalState = awaitItem() - assertThat(finalState.requestState).isEqualTo(AsyncAction.Uninitialized) - } - } -} diff --git a/libraries/oidc/test/build.gradle.kts b/libraries/oidc/test/build.gradle.kts new file mode 100644 index 00000000000..ac9d4cfda83 --- /dev/null +++ b/libraries/oidc/test/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.oidc.test" +} + +dependencies { + implementation(libs.coroutines.core) + api(projects.libraries.oidc.api) + implementation(projects.tests.testutils) +} diff --git a/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/FakeOidcIntentResolver.kt b/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/FakeOidcIntentResolver.kt new file mode 100644 index 00000000000..1fa5947c3f9 --- /dev/null +++ b/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/FakeOidcIntentResolver.kt @@ -0,0 +1,21 @@ +/* + * 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 + +import android.content.Intent +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 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 00000000000..d1673d8bc8a --- /dev/null +++ b/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/customtab/FakeOidcActionFlow.kt @@ -0,0 +1,32 @@ +/* + * 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/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt index 3823b1b8e13..1748a02db47 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.permissions.api import android.Manifest import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter @@ -41,6 +42,7 @@ fun PermissionsView( } @Composable +@ReadOnlyComposable private fun String.toDialogContent(): String { return when (this) { Manifest.permission.POST_NOTIFICATIONS -> stringResource(id = R.string.dialog_permission_notification) diff --git a/libraries/permissions/api/src/main/res/values-cy/translations.xml b/libraries/permissions/api/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..e72a9d09a4e --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-cy/translations.xml @@ -0,0 +1,7 @@ + + + "Er mwyn gadael i\'r rhaglen ddefnyddio\'r camera, rhowch ganiatâd iddo yn y gosodiadau system." + "Rhowch ganiatâd yn y gosodiadau system." + "Er mwyn gadael i\'r cais ddefnyddio\'r meicroffon, rhowch ganiatâd yng ngosodiadau\'r system." + "Er mwyn gadael i\'r ap ddangos hysbysiadau, rhowch ganiatâd yn y gosodiadau system." + diff --git a/libraries/permissions/api/src/main/res/values-da/translations.xml b/libraries/permissions/api/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..8c823f75dd7 --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-da/translations.xml @@ -0,0 +1,7 @@ + + + "For at lade applikationen bruge kameraet, skal du give tilladelsen i systemindstillingerne." + "Giv venligst tilladelsen i systemindstillingerne." + "For at lade applikationen bruge mikrofonen, skal du give tilladelsen i systemindstillingerne." + "For at lade applikationen vise notifikationer, skal du give tilladelsen i systemindstillingerne." + diff --git a/libraries/permissions/api/src/main/res/values-de/translations.xml b/libraries/permissions/api/src/main/res/values-de/translations.xml index fb801cc450e..09c149ddb55 100644 --- a/libraries/permissions/api/src/main/res/values-de/translations.xml +++ b/libraries/permissions/api/src/main/res/values-de/translations.xml @@ -1,7 +1,7 @@ - "Damit die Anwendung die Kamera verwenden kann, erteile bitte die Erlaubnis in den Systemeinstellungen." + "Damit die Anwendung die Kamera verwenden kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen." "Bitte erteile die Erlaubnis in den Systemeinstellungen." - "Damit die Anwendung das Mikrofon verwenden kann, erteile bitte die Erlaubnis in den Systemeinstellungen." - "Damit die Anwendung Benachrichtigungen anzeigen kann, erteile bitte die Erlaubnis in den Systemeinstellungen." + "Damit die Anwendung das Mikrofon verwenden kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen." + "Damit Benachrichtigungen angezeigt werden können, gewähren Sie der App bitte die Erlaubnis in den Systemeinstellungen." diff --git a/libraries/permissions/api/src/main/res/values-eu/translations.xml b/libraries/permissions/api/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..00819b0db22 --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-eu/translations.xml @@ -0,0 +1,7 @@ + + + "Aplikazioak kamera erabiltzeko, eman baimena sistemaren ezarpenetan." + "Eman baimena sistemaren ezarpenetan." + "Aplikazioak mikrofonoa erabiltzeko, eman baimena sistemaren ezarpenetan." + "Aplikazioak jakinarazpenak bistaratzeko, eman baimena sistemaren ezarpenetan." + diff --git a/libraries/permissions/api/src/main/res/values-fa/translations.xml b/libraries/permissions/api/src/main/res/values-fa/translations.xml index 54e7e245bd4..1b014424ec7 100644 --- a/libraries/permissions/api/src/main/res/values-fa/translations.xml +++ b/libraries/permissions/api/src/main/res/values-fa/translations.xml @@ -1,4 +1,7 @@ + "برای اینکه برنامه از دوربین استفاده کند، لطفا در تنظیمات سیستم مجوز بدهید." "لطفاً در تنظیمات سامانه اجازه بدهید." + "برای اینکه برنامه از میکروفون استفاده کند، لطفا اجازه دهید در تنظیمات سیستم مجوز بدهید." + "برای اینکه برنامه اعلان ها را نمایش دهد، لطفا مجوز را در تنظیمات سیستم اعطا کنید." diff --git a/libraries/permissions/api/src/main/res/values-nb/translations.xml b/libraries/permissions/api/src/main/res/values-nb/translations.xml index 40058cedccc..2736c66f8dd 100644 --- a/libraries/permissions/api/src/main/res/values-nb/translations.xml +++ b/libraries/permissions/api/src/main/res/values-nb/translations.xml @@ -2,4 +2,6 @@ "For å la programmet bruke kameraet, vennligst gi tillatelse i systeminnstillingene." "Vennligst gi tillatelse i systeminnstillingene." + "For å la applikasjonen bruke mikrofonen, må du gi tillatelse i systeminnstillingene." + "For å la applikasjonen vise varsler, må du gi tillatelse til dette i systeminnstillingene." diff --git a/libraries/permissions/api/src/main/res/values-tr/translations.xml b/libraries/permissions/api/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..20451d91cff --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-tr/translations.xml @@ -0,0 +1,7 @@ + + + "Uygulamanın kamerayı kullanmasına izin vermek için lütfen sistem ayarlarından izin verin." + "Lütfen sistem ayarlarından izin verin." + "Uygulamanın mikrofonu kullanmasına izin vermek için lütfen sistem ayarlarından izin verin." + "Uygulamanın bildirimleri görüntülemesine izin vermek için lütfen sistem ayarlarından izin verin." + diff --git a/libraries/permissions/api/src/main/res/values-ur/translations.xml b/libraries/permissions/api/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..92925ae749b --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-ur/translations.xml @@ -0,0 +1,7 @@ + + + "اطلاقیے کو تصویرگر استعمال کرنے دینے کے لیے، برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔" + "برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔" + "اطلاقیے کو صوتگر استعمال کرنے دینے کے لیے، برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔" + "اطلاقیے کو اطلاعات ظاہر کرنے دینے کے لیے، برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔" + diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index 3e5e3b74072..e9072e36665 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -13,6 +13,7 @@ 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.saveable.rememberSaveable import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -56,13 +57,13 @@ class DefaultPermissionsPresenter @AssistedInject constructor( // To reset the store: ResetStore() - val isAlreadyDenied: Boolean by permissionsStore - .isPermissionDenied(permission) - .collectAsState(initial = false) + val isAlreadyDenied: Boolean by remember { + permissionsStore.isPermissionDenied(permission) + }.collectAsState(initial = false) - val isAlreadyAsked: Boolean by permissionsStore - .isPermissionAsked(permission) - .collectAsState(initial = false) + val isAlreadyAsked: Boolean by remember { + permissionsStore.isPermissionAsked(permission) + }.collectAsState(initial = false) var permissionState: PermissionState? = null diff --git a/libraries/permissions/impl/src/main/res/values-cy/translations.xml b/libraries/permissions/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..72a037351d0 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,5 @@ + + + "Gwiriwch y gall y rhaglen ddangos hysbysiadau." + "Gwirio caniatâd" + diff --git a/libraries/permissions/impl/src/main/res/values-da/translations.xml b/libraries/permissions/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..871610e35d6 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,5 @@ + + + "Kontroller, at applikationen kan vise underretninger." + "Kontroller tilladelser" + diff --git a/libraries/permissions/impl/src/main/res/values-de/translations.xml b/libraries/permissions/impl/src/main/res/values-de/translations.xml index a0ff0ff417c..180a05f7405 100644 --- a/libraries/permissions/impl/src/main/res/values-de/translations.xml +++ b/libraries/permissions/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,5 @@ - "Stelle sicher, dass die Anwendung Benachrichtigungen anzeigen kann." + "Prüfe, dass die Anwendung Benachrichtigungen anzeigen kann." "Berechtigungen überprüfen" diff --git a/libraries/permissions/impl/src/main/res/values-es/translations.xml b/libraries/permissions/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..45d5eaf9e09 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,5 @@ + + + "Verificar que la aplicación pueda mostrar notificaciones." + "Verificar permisos" + diff --git a/libraries/permissions/impl/src/main/res/values-eu/translations.xml b/libraries/permissions/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..9f5c8edabcb --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,5 @@ + + + "Egiaztatu aplikazioak jakinarazpenak erakutsi ditzakeela." + "Egiaztatu baimenak" + diff --git a/libraries/permissions/impl/src/main/res/values-in/translations.xml b/libraries/permissions/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..dff88d13141 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,5 @@ + + + "Pastikan aplikasi dapat menampilkan notifikasi." + "Periksa izin" + diff --git a/libraries/permissions/impl/src/main/res/values-ka/translations.xml b/libraries/permissions/impl/src/main/res/values-ka/translations.xml new file mode 100644 index 00000000000..2dbb4018f70 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-ka/translations.xml @@ -0,0 +1,5 @@ + + + "შეამოწმეთ რომ აპლიკაციას შეტყობინებების ჩვენება შეუძლია." + "ნებართვების შემოწმება" + diff --git a/libraries/permissions/impl/src/main/res/values-nb/translations.xml b/libraries/permissions/impl/src/main/res/values-nb/translations.xml index b64b4b2b428..b53212a8e40 100644 --- a/libraries/permissions/impl/src/main/res/values-nb/translations.xml +++ b/libraries/permissions/impl/src/main/res/values-nb/translations.xml @@ -1,4 +1,5 @@ + "Kontroller at programmet kan vise varsler." "Sjekk tillatelser" diff --git a/libraries/permissions/impl/src/main/res/values-pt-rBR/translations.xml b/libraries/permissions/impl/src/main/res/values-pt-rBR/translations.xml index c8834ddb23c..e4e2226e9fe 100644 --- a/libraries/permissions/impl/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/permissions/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,4 +1,5 @@ + "Verifique se o aplicativo pode mostrar notificações." "Verifique as permissões" diff --git a/libraries/permissions/impl/src/main/res/values-tr/translations.xml b/libraries/permissions/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..84407bc0301 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,5 @@ + + + "Uygulamanın bildirimleri gösterebildiğini kontrol edin." + "İzinleri kontrol et" + diff --git a/libraries/permissions/impl/src/main/res/values-ur/translations.xml b/libraries/permissions/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..f7148371444 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,5 @@ + + + "پڑتال کریں کہ اطلاقیہ اطلاعات دکھا سکتا ہے" + "اجازتوں کی پڑتال کریں" + diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTestTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTestTest.kt index b50cc16e69d..a530b8dbee4 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTestTest.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTestTest.kt @@ -10,8 +10,8 @@ package io.element.android.libraries.permissions.impl.troubleshoot import android.os.Build import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.permissions.impl.FakePermissionStateProvider import io.element.android.libraries.permissions.impl.action.FakePermissionActions +import io.element.android.libraries.permissions.test.FakePermissionStateProvider import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionStateProvider.kt similarity index 96% rename from libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt rename to libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionStateProvider.kt index ebd945c8e6b..c97057bc7d9 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionStateProvider.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.permissions.impl +package io.element.android.libraries.permissions.test import io.element.android.libraries.permissions.api.PermissionStateProvider import kotlinx.coroutines.flow.Flow 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 865e2992d2e..7e477850880 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 @@ -7,7 +7,9 @@ package io.element.android.libraries.preferences.api.store +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack import kotlinx.coroutines.flow.Flow interface AppPreferencesStore { @@ -20,14 +22,20 @@ interface AppPreferencesStore { suspend fun setTheme(theme: String) fun getThemeFlow(): Flow - suspend fun setSimplifiedSlidingSyncEnabled(enabled: Boolean) - fun isSimplifiedSlidingSyncEnabledFlow(): Flow - - suspend fun setHideImagesAndVideos(value: Boolean) - fun doesHideImagesAndVideosFlow(): Flow + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + suspend fun setHideInviteAvatars(hide: Boolean?) + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + fun getHideInviteAvatarsFlow(): Flow + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + suspend fun setTimelineMediaPreviewValue(mediaPreviewValue: MediaPreviewValue?) + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + fun getTimelineMediaPreviewValueFlow(): Flow suspend fun setTracingLogLevel(logLevel: LogLevel) fun getTracingLogLevelFlow(): Flow + suspend fun setTracingLogPacks(targets: Set) + fun getTracingLogPacksFlow(): Flow> + suspend fun reset() } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt deleted file mode 100644 index 851d6594287..00000000000 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.preferences.api.store - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import javax.inject.Inject - -class EnableNativeSlidingSyncUseCase @Inject constructor( - private val appPreferencesStore: AppPreferencesStore, - private val appCoroutineScope: CoroutineScope, -) { - operator fun invoke() { - appCoroutineScope.launch { - appPreferencesStore.setSimplifiedSlidingSyncEnabled(true) - } - } -} diff --git a/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt b/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt deleted file mode 100644 index e570e7d2263..00000000000 --- a/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 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. - */ - -package io.element.android.libraries.preferences.api.store - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class EnableNativeSlidingSyncUseCaseTest { - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun `ensure that the use case sets the simplified sliding sync enabled flag`() = runTest { - val preferencesStore = InMemoryAppPreferencesStore() - val useCase = EnableNativeSlidingSyncUseCase(preferencesStore, this) - assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse() - - useCase() - advanceUntilIdle() - - assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue() - } -} 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 1874b08b0e3..0dff16ff989 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 @@ -19,7 +19,9 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -30,9 +32,10 @@ private val Context.dataStore: DataStore by preferencesDataStore(na private val developerModeKey = booleanPreferencesKey("developerMode") private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl") private val themeKey = stringPreferencesKey("theme") -private val simplifiedSlidingSyncKey = booleanPreferencesKey("useSimplifiedSlidingSync") -private val hideImagesAndVideosKey = booleanPreferencesKey("hideImagesAndVideos") +private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars") +private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue") private val logLevelKey = stringPreferencesKey("logLevel") +private val traceLogPacksKey = stringPreferencesKey("traceLogPacks") @ContributesBinding(AppScope::class) class DefaultAppPreferencesStore @Inject constructor( @@ -82,27 +85,39 @@ class DefaultAppPreferencesStore @Inject constructor( } } - override suspend fun setSimplifiedSlidingSyncEnabled(enabled: Boolean) { - store.edit { prefs -> - prefs[simplifiedSlidingSyncKey] = enabled + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + override fun getHideInviteAvatarsFlow(): Flow { + return store.data.map { prefs -> + prefs[hideInviteAvatarsKey] } } - override fun isSimplifiedSlidingSyncEnabledFlow(): Flow { - return store.data.map { prefs -> - prefs[simplifiedSlidingSyncKey] ?: true + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + override suspend fun setHideInviteAvatars(hide: Boolean?) { + store.edit { prefs -> + if (hide != null) { + prefs[hideInviteAvatarsKey] = hide + } else { + prefs.remove(hideInviteAvatarsKey) + } } } - override suspend fun setHideImagesAndVideos(value: Boolean) { + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + override suspend fun setTimelineMediaPreviewValue(mediaPreviewValue: MediaPreviewValue?) { store.edit { prefs -> - prefs[hideImagesAndVideosKey] = value + if (mediaPreviewValue != null) { + prefs[timelineMediaPreviewValueKey] = mediaPreviewValue.name + } else { + prefs.remove(timelineMediaPreviewValueKey) + } } } - override fun doesHideImagesAndVideosFlow(): Flow { + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + override fun getTimelineMediaPreviewValueFlow(): Flow { return store.data.map { prefs -> - prefs[hideImagesAndVideosKey] ?: false + prefs[timelineMediaPreviewValueKey]?.let { MediaPreviewValue.valueOf(it) } } } @@ -118,6 +133,23 @@ class DefaultAppPreferencesStore @Inject constructor( } } + override suspend fun setTracingLogPacks(targets: Set) { + val value = targets.joinToString(",") { it.key } + store.edit { prefs -> + prefs[traceLogPacksKey] = value + } + } + + override fun getTracingLogPacksFlow(): Flow> { + return store.data.map { prefs -> + prefs[traceLogPacksKey] + ?.split(",") + ?.mapNotNull { value -> TraceLogPack.entries.find { it.key == value } } + ?.toSet() + ?: emptySet() + } + } + override suspend fun reset() { store.edit { it.clear() } } diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt index 4b9363da0e5..d7f1c368226 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt @@ -80,7 +80,7 @@ class DefaultSessionPreferencesStore( override fun isRenderTypingNotificationsEnabled(): Flow = get(renderTypingNotificationsKey) { true } override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip) - override fun isSessionVerificationSkipped(): Flow = get(skipSessionVerification) { false } + override fun isSessionVerificationSkipped(): Flow = get(skipSessionVerification) { true } override suspend fun setCompressMedia(compress: Boolean) = update(compressMedia, compress) override fun doesCompressMedia(): Flow = get(compressMedia) { true } 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 3191b220736..d0ee298a662 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 @@ -7,25 +7,29 @@ package io.element.android.libraries.preferences.test +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow class InMemoryAppPreferencesStore( isDeveloperModeEnabled: Boolean = false, - hideImagesAndVideos: Boolean = false, customElementCallBaseUrl: String? = null, + hideInviteAvatars: Boolean? = null, + timelineMediaPreviewValue: MediaPreviewValue? = null, theme: String? = null, - simplifiedSlidingSyncEnabled: Boolean = false, logLevel: LogLevel = LogLevel.INFO, + traceLockPacks: Set = emptySet(), ) : AppPreferencesStore { private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) - private val hideImagesAndVideos = MutableStateFlow(hideImagesAndVideos) private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl) private val theme = MutableStateFlow(theme) - private val simplifiedSlidingSyncEnabled = MutableStateFlow(simplifiedSlidingSyncEnabled) private val logLevel = MutableStateFlow(logLevel) + private val tracingLogPacks = MutableStateFlow(traceLockPacks) + private val hideInviteAvatars = MutableStateFlow(hideInviteAvatars) + private val timelineMediaPreviewValue = MutableStateFlow(timelineMediaPreviewValue) override suspend fun setDeveloperModeEnabled(enabled: Boolean) { isDeveloperModeEnabled.value = enabled @@ -51,20 +55,24 @@ class InMemoryAppPreferencesStore( return theme } - override suspend fun setSimplifiedSlidingSyncEnabled(enabled: Boolean) { - simplifiedSlidingSyncEnabled.value = enabled + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + override fun getHideInviteAvatarsFlow(): Flow { + return hideInviteAvatars } - override fun isSimplifiedSlidingSyncEnabledFlow(): Flow { - return simplifiedSlidingSyncEnabled + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + override fun getTimelineMediaPreviewValueFlow(): Flow { + return timelineMediaPreviewValue } - override suspend fun setHideImagesAndVideos(value: Boolean) { - hideImagesAndVideos.value = value + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + override suspend fun setHideInviteAvatars(hide: Boolean?) { + hideInviteAvatars.value = hide } - override fun doesHideImagesAndVideosFlow(): Flow { - return hideImagesAndVideos + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") + override suspend fun setTimelineMediaPreviewValue(mediaPreviewValue: MediaPreviewValue?) { + timelineMediaPreviewValue.value = mediaPreviewValue } override suspend fun setTracingLogLevel(logLevel: LogLevel) { @@ -75,6 +83,14 @@ class InMemoryAppPreferencesStore( return logLevel } + override suspend fun setTracingLogPacks(targets: Set) { + tracingLogPacks.value = targets + } + + override fun getTracingLogPacksFlow(): Flow> { + return tracingLogPacks + } + override suspend fun reset() { // No op } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index 5a82b31fd91..fb1bd144042 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.push.api import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.api.history.PushHistoryItem import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import kotlinx.coroutines.flow.Flow @@ -51,4 +52,24 @@ interface PushService { * Return false in case of early error. */ suspend fun testPush(): Boolean + + /** + * Get a flow of total number of received Push. + */ + val pushCounter: Flow + + /** + * Get a flow of list of [PushHistoryItem]. + */ + fun getPushHistoryItemsFlow(): Flow> + + /** + * Reset the push history, including the push counter. + */ + suspend fun resetPushHistory() + + /** + * Reset the battery optimization state. + */ + suspend fun resetBatteryOptimizationState() } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt new file mode 100644 index 00000000000..ab8d9e92e54 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt @@ -0,0 +1,13 @@ +/* + * 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.push.api.battery + +sealed interface BatteryOptimizationEvents { + data object Dismiss : BatteryOptimizationEvents + data object RequestDisableOptimizations : BatteryOptimizationEvents +} diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt new file mode 100644 index 00000000000..7e7c2b2bd6a --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt @@ -0,0 +1,13 @@ +/* + * 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.push.api.battery + +data class BatteryOptimizationState( + val shouldDisplayBanner: Boolean, + val eventSink: (BatteryOptimizationEvents) -> Unit, +) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt new file mode 100644 index 00000000000..d6d4e5774b3 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt @@ -0,0 +1,16 @@ +/* + * 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.push.api.battery + +fun aBatteryOptimizationState( + shouldDisplayBanner: Boolean = false, + eventSink: (BatteryOptimizationEvents) -> Unit = {}, +) = BatteryOptimizationState( + shouldDisplayBanner = shouldDisplayBanner, + eventSink = eventSink, +) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/history/PushHistoryItem.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/history/PushHistoryItem.kt new file mode 100644 index 00000000000..9606e7e6dc8 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/history/PushHistoryItem.kt @@ -0,0 +1,34 @@ +/* + * 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.push.api.history + +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 + +/** + * Data class representing a push history item. + * @property pushDate Date (timestamp). + * @property formattedDate Formatted date. + * @property providerInfo Push provider name / info + * @property eventId EventId from the push, can be null if the received data are not correct. + * @property roomId RoomId from the push, can be null if the received data are not correct. + * @property sessionId The session Id, can be null if the session cannot be retrieved + * @property hasBeenResolved Result of resolving the event + * @property comment Comment. Can contains an error message if the event could not be resolved, or other any information. + */ +data class PushHistoryItem( + val pushDate: Long, + val formattedDate: String, + val providerInfo: String, + val eventId: EventId?, + val roomId: RoomId?, + val sessionId: SessionId?, + val hasBeenResolved: Boolean, + val comment: String?, +) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt index 651403dadde..541e7ae2e7e 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.push.api.notifications import android.graphics.Bitmap import androidx.core.graphics.drawable.IconCompat -import coil.ImageLoader +import coil3.ImageLoader interface NotificationBitmapLoader { /** diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt deleted file mode 100644 index 2d37548722c..00000000000 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.push.api.store - -import kotlinx.coroutines.flow.Flow - -interface PushDataStore { - val pushCounterFlow: Flow -} diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index ef85740f68a..dbf3bbcf5aa 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -9,6 +9,7 @@ import extension.setupAnvil plugins { id("io.element.android-compose-library") alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.sqldelight) } android { @@ -27,15 +28,21 @@ dependencies { implementation(libs.dagger) implementation(libs.androidx.corektx) implementation(libs.androidx.datastore.preferences) - implementation(libs.androidx.security.crypto) implementation(platform(libs.network.retrofit.bom)) implementation(libs.network.retrofit) implementation(libs.serialization.json) implementation(libs.coil) + implementation(libs.sqldelight.driver.android) + implementation(libs.sqlcipher) + implementation(libs.sqlite) + implementation(libs.sqldelight.coroutines) + implementation(projects.libraries.encryptedDb) + implementation(projects.appconfig) implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.di) implementation(projects.libraries.androidutils) @@ -77,3 +84,11 @@ dependencies { testImplementation(projects.libraries.featureflag.test) testImplementation(libs.kotlinx.collections.immutable) } + +sqldelight { + databases { + create("PushDatabase") { + schemaOutputDirectory = File("src/main/sqldelight/databases") + } + } +} diff --git a/libraries/push/impl/src/main/AndroidManifest.xml b/libraries/push/impl/src/main/AndroidManifest.xml index 49ca55decc9..c08c16ed5f3 100644 --- a/libraries/push/impl/src/main/AndroidManifest.xml +++ b/libraries/push/impl/src/main/AndroidManifest.xml @@ -7,6 +7,9 @@ + + + = pushDataStore.pushCounterFlow + + override fun getPushHistoryItemsFlow(): Flow> { + return pushDataStore.getPushHistoryItemsFlow() + } + + override suspend fun resetPushHistory() { + pushDataStore.reset() + } + + override suspend fun resetBatteryOptimizationState() { + mutableBatteryOptimizationStore.reset() + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt new file mode 100644 index 00000000000..4087a2e7988 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt @@ -0,0 +1,85 @@ +/* + * 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.push.impl.battery + +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.os.PowerManager +import android.provider.Settings +import androidx.core.content.getSystemService +import androidx.core.net.toUri +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher +import timber.log.Timber +import javax.inject.Inject + +interface BatteryOptimization { + /** + * Tells if the application ignores battery optimizations. + * + * Ignoring them allows the app to run in background to make background sync with the homeserver. + * This user option appears on Android M but Android O enforces its usage and kills apps not + * authorised by the user to run in background. + * + * @return true if battery optimisations are ignored + */ + fun isIgnoringBatteryOptimizations(): Boolean + + /** + * Request the user to disable battery optimizations for this app. + * This will open the system settings where the user can disable battery optimizations. + * See https://developer.android.com/training/monitoring-device-state/doze-standby#exemption-cases + * + * @return true if the intent was successfully started, false if the activity was not found + */ + fun requestDisablingBatteryOptimization(): Boolean +} + +@ContributesBinding(AppScope::class) +class AndroidBatteryOptimization @Inject constructor( + @ApplicationContext + private val context: Context, + private val externalIntentLauncher: ExternalIntentLauncher, +) : BatteryOptimization { + override fun isIgnoringBatteryOptimizations(): Boolean { + return context.getSystemService() + ?.isIgnoringBatteryOptimizations(context.packageName) == true + } + + @SuppressLint("BatteryLife") + override fun requestDisablingBatteryOptimization(): Boolean { + val ignoreBatteryOptimizationsResult = launchAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, withData = true) + if (ignoreBatteryOptimizationsResult) { + return true + } + // Open settings as a fallback if the first attempt fails + return launchAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS, withData = false) + } + + private fun launchAction( + action: String, + withData: Boolean, + ): Boolean { + val intent = Intent() + intent.action = action + if (withData) { + intent.data = ("package:" + context.packageName).toUri() + } + return try { + externalIntentLauncher.launch(intent) + true + } catch (exception: ActivityNotFoundException) { + Timber.w(exception, "Cannot launch intent with action $action.") + false + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt new file mode 100644 index 00000000000..9fa17f05449 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt @@ -0,0 +1,71 @@ +/* + * 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.push.impl.battery + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.LifecycleResumeEffect +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.push.api.battery.BatteryOptimizationEvents +import io.element.android.libraries.push.api.battery.BatteryOptimizationState +import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore +import io.element.android.libraries.push.impl.store.PushDataStore +import kotlinx.coroutines.launch +import javax.inject.Inject + +class BatteryOptimizationPresenter @Inject constructor( + private val pushDataStore: PushDataStore, + private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore, + private val batteryOptimization: BatteryOptimization, +) : Presenter { + @Composable + override fun present(): BatteryOptimizationState { + val coroutineScope = rememberCoroutineScope() + var isRequestSent by remember { mutableStateOf(false) } + var localShouldDisplayBanner by remember { mutableStateOf(true) } + val storeShouldDisplayBanner by pushDataStore.shouldDisplayBatteryOptimizationBannerFlow.collectAsState(initial = false) + var isSystemIgnoringBatteryOptimizations by remember { + mutableStateOf(batteryOptimization.isIgnoringBatteryOptimizations()) + } + + LifecycleResumeEffect(Unit) { + isSystemIgnoringBatteryOptimizations = batteryOptimization.isIgnoringBatteryOptimizations() + if (isRequestSent) { + localShouldDisplayBanner = false + } + onPauseOrDispose {} + } + + fun handleEvents(event: BatteryOptimizationEvents) { + when (event) { + BatteryOptimizationEvents.Dismiss -> coroutineScope.launch { + mutableBatteryOptimizationStore.onOptimizationBannerDismissed() + } + BatteryOptimizationEvents.RequestDisableOptimizations -> { + isRequestSent = true + if (batteryOptimization.requestDisablingBatteryOptimization().not()) { + // If not able to perform the request, ensure that we do not display the banner again + coroutineScope.launch { + mutableBatteryOptimizationStore.onOptimizationBannerDismissed() + } + } + } + } + } + + return BatteryOptimizationState( + shouldDisplayBanner = localShouldDisplayBanner && storeShouldDisplayBanner && !isSystemIgnoringBatteryOptimizations, + eventSink = ::handleEvents, + ) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt index 3e7c7ddae3f..bb8b7e1624b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt @@ -10,16 +10,25 @@ package io.element.android.libraries.push.impl.di import android.content.Context import androidx.core.app.NotificationManagerCompat import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds import dagger.Module import dagger.Provides +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.push.api.battery.BatteryOptimizationState +import io.element.android.libraries.push.impl.battery.BatteryOptimizationPresenter @Module @ContributesTo(AppScope::class) -object PushModule { - @Provides - fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat { - return NotificationManagerCompat.from(context) +interface PushModule { + companion object { + @Provides + fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat { + return NotificationManagerCompat.from(context) + } } + + @Binds + fun bindBatteryOptimizationPresenter(presenter: BatteryOptimizationPresenter): Presenter } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/DefaultPushHistoryService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/DefaultPushHistoryService.kt new file mode 100644 index 00000000000..23ae48ea31a --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/DefaultPushHistoryService.kt @@ -0,0 +1,76 @@ +/* + * 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.push.impl.history + +import android.content.Context +import android.os.Build +import android.os.PowerManager +import androidx.core.content.getSystemService +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +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.push.impl.PushDatabase +import io.element.android.libraries.push.impl.db.PushHistory +import io.element.android.services.toolbox.api.systemclock.SystemClock +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultPushHistoryService @Inject constructor( + private val pushDatabase: PushDatabase, + private val systemClock: SystemClock, + @ApplicationContext context: Context, +) : PushHistoryService { + private val powerManager = context.getSystemService() + private val packageName = context.packageName + + override fun onPushReceived( + providerInfo: String, + eventId: EventId?, + roomId: RoomId?, + sessionId: SessionId?, + hasBeenResolved: Boolean, + includeDeviceState: Boolean, + comment: String?, + ) { + val finalComment = buildString { + append(comment.orEmpty()) + if (includeDeviceState && powerManager != null) { + // Add info about device state + append("\n") + append(" - Idle: ${powerManager.isDeviceIdleMode}\n") + append(" - Power Save Mode: ${powerManager.isPowerSaveMode}\n") + append(" - Ignoring Battery Optimizations: ${powerManager.isIgnoringBatteryOptimizations(packageName)}\n") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + append(" - Device Light Idle Mode: ${powerManager.isDeviceLightIdleMode}\n") + append(" - Low Power Standby Enabled: ${powerManager.isLowPowerStandbyEnabled}\n") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + append(" - Exempt from Low Power Standby: ${powerManager.isExemptFromLowPowerStandby}\n") + } + } + }.takeIf { it.isNotEmpty() } + + pushDatabase.pushHistoryQueries.insertPushHistory( + PushHistory( + pushDate = systemClock.epochMillis(), + providerInfo = providerInfo, + eventId = eventId?.value, + roomId = roomId?.value, + sessionId = sessionId?.value, + hasBeenResolved = if (hasBeenResolved) 1 else 0, + comment = finalComment, + ) + ) + + // Keep only the last 1_000 events + pushDatabase.pushHistoryQueries.removeOldest(1_000) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/PushHistoryService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/PushHistoryService.kt new file mode 100644 index 00000000000..31d431bb2c9 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/PushHistoryService.kt @@ -0,0 +1,105 @@ +/* + * 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.push.impl.history + +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 + +interface PushHistoryService { + /** + * Create a new push history entry. + * Do not use directly, prefer using the extension functions. + */ + fun onPushReceived( + providerInfo: String, + eventId: EventId?, + roomId: RoomId?, + sessionId: SessionId?, + hasBeenResolved: Boolean, + includeDeviceState: Boolean, + comment: String?, + ) +} + +fun PushHistoryService.onInvalidPushReceived( + providerInfo: String, + data: String, +) = onPushReceived( + providerInfo = providerInfo, + eventId = null, + roomId = null, + sessionId = null, + hasBeenResolved = false, + includeDeviceState = false, + comment = "Invalid or ignored push data:\n$data", +) + +fun PushHistoryService.onUnableToRetrieveSession( + providerInfo: String, + eventId: EventId, + roomId: RoomId, + reason: String, +) = onPushReceived( + providerInfo = providerInfo, + eventId = eventId, + roomId = roomId, + sessionId = null, + hasBeenResolved = false, + includeDeviceState = true, + comment = "Unable to retrieve session: $reason", +) + +fun PushHistoryService.onUnableToResolveEvent( + providerInfo: String, + eventId: EventId, + roomId: RoomId, + sessionId: SessionId, + reason: String, +) = onPushReceived( + providerInfo = providerInfo, + eventId = eventId, + roomId = roomId, + sessionId = sessionId, + hasBeenResolved = false, + includeDeviceState = true, + comment = "Unable to resolve event: $reason", +) + +fun PushHistoryService.onSuccess( + providerInfo: String, + eventId: EventId, + roomId: RoomId, + sessionId: SessionId, + comment: String?, +) = onPushReceived( + providerInfo = providerInfo, + eventId = eventId, + roomId = roomId, + sessionId = sessionId, + hasBeenResolved = true, + includeDeviceState = false, + comment = buildString { + append("Success") + if (comment.isNullOrBlank().not()) { + append(" - $comment") + } + }, +) + +fun PushHistoryService.onDiagnosticPush( + providerInfo: String, +) = onPushReceived( + providerInfo = providerInfo, + eventId = null, + roomId = null, + sessionId = null, + hasBeenResolved = true, + includeDeviceState = false, + comment = "Diagnostic push", +) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt new file mode 100644 index 00000000000..248ba8182d8 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt @@ -0,0 +1,43 @@ +/* + * 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.push.impl.history.di + +import android.content.Context +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.push.impl.PushDatabase +import io.element.encrypteddb.SqlCipherDriverFactory +import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider + +@Module +@ContributesTo(AppScope::class) +object PushHistoryModule { + @Provides + @SingleIn(AppScope::class) + fun providePushDatabase( + @ApplicationContext context: Context, + ): PushDatabase { + val name = "push_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(PushDatabase.Schema, "$name.db", context) + return PushDatabase(driver) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt index ecbee498109..119eda3423e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt @@ -8,16 +8,24 @@ package io.element.android.libraries.push.impl.notifications import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.notification.CallNotifyType 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.timeline.item.event.EventType import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent +import io.element.android.services.appnavstate.api.AppForegroundStateService import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withTimeoutOrNull +import timber.log.Timber import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds /** * Helper to resolve a valid [NotifiableEvent] from a [NotificationData]. @@ -30,17 +38,53 @@ interface CallNotificationEventResolver { * @param forceNotify `true` to force the notification to be non-ringing, `false` to use the default behaviour. Default is `false`. * @return a [NotifiableEvent] if the notification data is a call notification, null otherwise */ - fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean = false): NotifiableEvent? + suspend fun resolveEvent( + sessionId: SessionId, + notificationData: NotificationData, + forceNotify: Boolean = false, + ): Result } @ContributesBinding(AppScope::class) class DefaultCallNotificationEventResolver @Inject constructor( private val stringProvider: StringProvider, + private val appForegroundStateService: AppForegroundStateService, + private val clientProvider: MatrixClientProvider, ) : CallNotificationEventResolver { - override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? { - val content = notificationData.content as? NotificationContent.MessageLike.CallNotify ?: return null - return notificationData.run { - if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) { + override suspend fun resolveEvent( + sessionId: SessionId, + notificationData: NotificationData, + forceNotify: Boolean + ): Result = runCatchingExceptions { + val content = notificationData.content as? NotificationContent.MessageLike.CallNotify + ?: throw ResolvingException("content is not a call notify") + + val previousRingingCallStatus = appForegroundStateService.hasRingingCall.value + // We need the sync service working to get the updated room info + val isRoomCallActive = runCatchingExceptions { + if (content.type == CallNotifyType.RING) { + appForegroundStateService.updateHasRingingCall(true) + + val client = clientProvider.getOrRestore(sessionId).getOrNull() ?: throw ResolvingException("Session $sessionId not found") + val room = client.getRoom(notificationData.roomId) ?: throw ResolvingException("Room ${notificationData.roomId} not found") + // Give a few seconds for the room info flow to catch up with the sync, if needed - this is usually instant + val isActive = withTimeoutOrNull(3.seconds) { room.roomInfoFlow.firstOrNull { it.hasRoomCall } }?.hasRoomCall ?: false + + // We no longer need the sync service to be active because of a call notification. + appForegroundStateService.updateHasRingingCall(previousRingingCallStatus) + + isActive + } else { + // If the call notification is not of ringing type, we don't need to check if the call is active + false + } + }.onFailure { + // Make sure to reset the hasRingingCall state in case of failure + appForegroundStateService.updateHasRingingCall(previousRingingCallStatus) + }.getOrDefault(false) + + notificationData.run { + if (content.type == CallNotifyType.RING && isRoomCallActive && !forceNotify) { NotifiableRingingCallEvent( sessionId = sessionId, roomId = roomId, @@ -59,6 +103,7 @@ class DefaultCallNotificationEventResolver @Inject constructor( senderAvatarUrl = senderAvatarUrl, ) } else { + Timber.d("Event $eventId is call notify but should not ring: $isRoomCallActive, notify: ${content.type}") // Create a simple message notification event buildNotifiableMessageEvent( sessionId = sessionId, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index c8e8ba44314..7e7dbe3fb4a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -11,9 +11,12 @@ import android.content.Context import android.net.Uri import androidx.core.content.FileProvider import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.mapCatchingExceptions +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.EventId @@ -21,6 +24,8 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.api.media.getMediaPreviewValue 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.permalink.PermalinkParser @@ -37,7 +42,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy 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.ui.messages.toPlainText -import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent @@ -46,7 +50,6 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock -import kotlinx.coroutines.flow.first import timber.log.Timber import javax.inject.Inject @@ -59,10 +62,14 @@ private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.No * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk. */ interface NotifiableEventResolver { - suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? + suspend fun resolveEvents( + sessionId: SessionId, + notificationEventRequests: List + ): Result>> } @ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) class DefaultNotifiableEventResolver @Inject constructor( private val stringProvider: StringProvider, private val clock: SystemClock, @@ -71,53 +78,69 @@ class DefaultNotifiableEventResolver @Inject constructor( @ApplicationContext private val context: Context, private val permalinkParser: PermalinkParser, private val callNotificationEventResolver: CallNotificationEventResolver, - private val appPreferencesStore: AppPreferencesStore, ) : NotifiableEventResolver { - override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? { - // Restore session - val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null - val notificationService = client.notificationService() - val notificationData = notificationService.getNotification( - roomId = roomId, - eventId = eventId, - ).onFailure { - Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.") - }.getOrNull() + override suspend fun resolveEvents( + sessionId: SessionId, + notificationEventRequests: List + ): Result>> { + Timber.d("Queueing notifications: $notificationEventRequests") + val client = matrixClientProvider.getOrRestore(sessionId).getOrElse { + return Result.failure(IllegalStateException("Couldn't get or restore client for session $sessionId")) + } + val ids = notificationEventRequests.groupBy { it.roomId }.mapValues { (_, value) -> value.map { it.eventId } } // TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event - return notificationData?.asNotifiableEvent(client, sessionId) + val notifications = client.notificationService().getNotifications(ids).mapCatchingExceptions { map -> + map.mapValues { (_, notificationData) -> + notificationData.asNotifiableEvent(client, sessionId) + } + } + + return Result.success( + notificationEventRequests.associate { + val notificationData = notifications.getOrNull()?.get(it.eventId) + if (notificationData != null) { + it to notificationData + } else { + // TODO once the SDK can actually return what went wrong, we should return it here instead of this generic error + it to Result.failure(ResolvingException("No notification data for ${it.roomId} - ${it.eventId}")) + } + } + ) } private suspend fun NotificationData.asNotifiableEvent( client: MatrixClient, userId: SessionId, - ): ResolvedPushEvent? { - val content = this.content - val notifiableEvent = when (content) { + ): Result = runCatchingExceptions { + when (val content = this.content) { is NotificationContent.MessageLike.RoomMessage -> { + val showMediaPreview = client.mediaPreviewService().getMediaPreviewValue() == MediaPreviewValue.On val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId) val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName) - buildNotifiableMessageEvent( + val notifiableMessageEvent = buildNotifiableMessageEvent( sessionId = userId, senderId = content.senderId, roomId = roomId, eventId = eventId, + threadId = threadId, noisy = isNoisy, timestamp = this.timestamp, senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, body = messageBody, - imageUriString = content.fetchImageIfPresent(client)?.toString(), - imageMimeType = content.getImageMimetype(), + imageUriString = if (showMediaPreview) content.fetchImageIfPresent(client)?.toString() else null, + imageMimeType = if (showMediaPreview) content.getImageMimetype() else null, roomName = roomDisplayName, roomIsDm = isDm, roomAvatarPath = roomAvatarUrl, senderAvatarPath = senderAvatarUrl, hasMentionOrReply = hasMention, ) + ResolvedPushEvent.Event(notifiableMessageEvent) } is NotificationContent.Invite -> { val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId) - InviteNotifiableEvent( + val inviteNotifiableEvent = InviteNotifiableEvent( sessionId = userId, roomId = roomId, eventId = eventId, @@ -135,15 +158,16 @@ class DefaultNotifiableEventResolver @Inject constructor( // TODO check if title is needed anymore title = null, ) + ResolvedPushEvent.Event(inviteNotifiableEvent) } NotificationContent.MessageLike.CallAnswer, NotificationContent.MessageLike.CallCandidates, NotificationContent.MessageLike.CallHangup -> { Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}") - null + throw ResolvingException("Ignoring notification for call ${content.javaClass.simpleName}") } is NotificationContent.MessageLike.CallInvite -> { - buildNotifiableMessageEvent( + val notifiableMessageEvent = buildNotifiableMessageEvent( sessionId = userId, senderId = content.senderId, roomId = roomId, @@ -157,9 +181,11 @@ class DefaultNotifiableEventResolver @Inject constructor( roomAvatarPath = roomAvatarUrl, senderAvatarPath = senderAvatarUrl, ) + ResolvedPushEvent.Event(notifiableMessageEvent) } is NotificationContent.MessageLike.CallNotify -> { - callNotificationEventResolver.resolveEvent(userId, this) + val notifiableEvent = callNotificationEventResolver.resolveEvent(userId, this).getOrThrow() + ResolvedPushEvent.Event(notifiableEvent) } NotificationContent.MessageLike.KeyVerificationAccept, NotificationContent.MessageLike.KeyVerificationCancel, @@ -167,11 +193,12 @@ class DefaultNotifiableEventResolver @Inject constructor( NotificationContent.MessageLike.KeyVerificationKey, NotificationContent.MessageLike.KeyVerificationMac, NotificationContent.MessageLike.KeyVerificationReady, - NotificationContent.MessageLike.KeyVerificationStart -> null.also { + NotificationContent.MessageLike.KeyVerificationStart -> { Timber.tag(loggerTag.value).d("Ignoring notification for verification ${content.javaClass.simpleName}") + throw ResolvingException("Ignoring notification for verification ${content.javaClass.simpleName}") } is NotificationContent.MessageLike.Poll -> { - buildNotifiableMessageEvent( + val notifiableEventMessage = buildNotifiableMessageEvent( sessionId = userId, senderId = content.senderId, roomId = roomId, @@ -186,19 +213,40 @@ class DefaultNotifiableEventResolver @Inject constructor( roomAvatarPath = roomAvatarUrl, senderAvatarPath = senderAvatarUrl, ) + ResolvedPushEvent.Event(notifiableEventMessage) } - is NotificationContent.MessageLike.ReactionContent -> null.also { + is NotificationContent.MessageLike.ReactionContent -> { Timber.tag(loggerTag.value).d("Ignoring notification for reaction") + throw ResolvingException("Ignoring notification for reaction") } - NotificationContent.MessageLike.RoomEncrypted -> fallbackNotifiableEvent(userId, roomId, eventId).also { + NotificationContent.MessageLike.RoomEncrypted -> { Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback") + val fallbackNotifiableEvent = fallbackNotifiableEvent(userId, roomId, eventId) + ResolvedPushEvent.Event(fallbackNotifiableEvent) + } + NotificationContent.MessageLike.UnableToResolve -> { + Timber.tag(loggerTag.value).w("Unable to resolve notification -> fallback") + val fallbackNotifiableEvent = fallbackNotifiableEvent(userId, roomId, eventId) + ResolvedPushEvent.Event(fallbackNotifiableEvent) } is NotificationContent.MessageLike.RoomRedaction -> { // Note: this case will be handled below - null + val redactedEventId = content.redactedEventId + if (redactedEventId == null) { + Timber.tag(loggerTag.value).d("redactedEventId is null.") + throw ResolvingException("redactedEventId is null") + } else { + ResolvedPushEvent.Redaction( + sessionId = userId, + roomId = roomId, + redactedEventId = redactedEventId, + reason = content.reason, + ) + } } - NotificationContent.MessageLike.Sticker -> null.also { + NotificationContent.MessageLike.Sticker -> { Timber.tag(loggerTag.value).d("Ignoring notification for sticker") + throw ResolvingException("Ignoring notification for reaction") } is NotificationContent.StateEvent.RoomMemberContent, NotificationContent.StateEvent.PolicyRuleRoom, @@ -218,31 +266,13 @@ class DefaultNotifiableEventResolver @Inject constructor( NotificationContent.StateEvent.RoomServerAcl, NotificationContent.StateEvent.RoomThirdPartyInvite, NotificationContent.StateEvent.RoomTombstone, - NotificationContent.StateEvent.RoomTopic, + is NotificationContent.StateEvent.RoomTopic, NotificationContent.StateEvent.SpaceChild, - NotificationContent.StateEvent.SpaceParent -> null.also { + NotificationContent.StateEvent.SpaceParent -> { Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}") + throw ResolvingException("Ignoring notification for state event ${content.javaClass.simpleName}") } } - - return if (notifiableEvent != null) { - ResolvedPushEvent.Event(notifiableEvent) - } else if (content is NotificationContent.MessageLike.RoomRedaction) { - val redactedEventId = content.redactedEventId - if (redactedEventId == null) { - Timber.tag(loggerTag.value).d("redactedEventId is null.") - null - } else { - ResolvedPushEvent.Redaction( - sessionId = userId, - roomId = roomId, - redactedEventId = redactedEventId, - reason = content.reason, - ) - } - } else { - null - } } private fun fallbackNotifiableEvent( @@ -292,9 +322,6 @@ class DefaultNotifiableEventResolver @Inject constructor( } private suspend fun NotificationContent.MessageLike.RoomMessage.fetchImageIfPresent(client: MatrixClient): Uri? { - if (appPreferencesStore.doesHideImagesAndVideosFlow().first()) { - return null - } val fileResult = when (val messageType = messageType) { is ImageMessageType -> notificationMediaRepoFactory.create(client) .getMediaFile( @@ -318,10 +345,7 @@ class DefaultNotifiableEventResolver @Inject constructor( .getOrNull() } - private suspend fun NotificationContent.MessageLike.RoomMessage.getImageMimetype(): String? { - if (appPreferencesStore.doesHideImagesAndVideosFlow().first()) { - return null - } + private fun NotificationContent.MessageLike.RoomMessage.getImageMimetype(): String? { return when (val messageType = messageType) { is ImageMessageType -> messageType.info?.mimetype is VideoMessageType -> null // Use the thumbnail here? diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt index 5f1458f2adf..7aef28df67c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt @@ -11,10 +11,11 @@ import android.content.Context import android.graphics.Bitmap import android.os.Build import androidx.core.graphics.drawable.IconCompat -import androidx.core.graphics.drawable.toBitmap -import coil.ImageLoader -import coil.request.ImageRequest -import coil.transform.CircleCropTransformation +import coil3.ImageLoader +import coil3.request.ImageRequest +import coil3.request.transformations +import coil3.toBitmap +import coil3.transform.CircleCropTransformation import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -50,7 +51,7 @@ class DefaultNotificationBitmapLoader @Inject constructor( .transformations(CircleCropTransformation()) .build() val result = imageLoader.execute(imageRequest) - result.drawable?.toBitmap() + result.image?.toBitmap() } catch (e: Throwable) { Timber.e(e, "Unable to load room bitmap") null @@ -78,7 +79,7 @@ class DefaultNotificationBitmapLoader @Inject constructor( .transformations(CircleCropTransformation()) .build() val result = imageLoader.execute(imageRequest) - val bitmap = result.drawable?.toBitmap() + val bitmap = result.image?.toBitmap() return bitmap?.let { IconCompat.createWithBitmap(it) } } catch (e: Throwable) { Timber.e(e, "Unable to load user bitmap") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 93e4bd85e5d..e50536b3132 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.EventId @@ -48,6 +49,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private val notificationManager: NotificationManagerCompat, private val notificationRenderer: NotificationRenderer, private val appNavigationStateService: AppNavigationStateService, + @AppCoroutineScope coroutineScope: CoroutineScope, private val matrixClientProvider: MatrixClientProvider, private val imageLoaderHolder: ImageLoaderHolder, @@ -113,6 +115,11 @@ class DefaultNotificationDrawerManager @Inject constructor( renderEvents(listOf(notifiableEvent)) } + suspend fun onNotifiableEventsReceived(notifiableEvents: List) { + val eventsToNotify = notifiableEvents.filter { !it.shouldIgnoreEventInRoom(appNavigationStateService.appNavigationState.value) } + renderEvents(eventsToNotify) + } + /** * Clear all known message events for a [sessionId]. */ @@ -203,7 +210,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private suspend fun MatrixClient.getSafeUserProfile(): MatrixUser { return tryOrNull( - onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") }, + onException = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") }, operation = { val profile = getUserProfile().getOrNull() // displayName cannot be empty else NotificationCompat.MessagingStyle() will crash diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt index aff02a221ff..1efe57ce065 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt @@ -30,8 +30,9 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor( // Resolve the event and add a notification for it, at this point it should no longer be a ringing one val notificationData = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?.notificationService() - ?.getNotification(roomId, eventId) + ?.getNotifications(mapOf(roomId to listOf(eventId))) ?.getOrNull() + ?.get(eventId) ?: return val notifiableEvent = callNotificationEventResolver.resolveEvent( @@ -39,7 +40,7 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor( notificationData = notificationData, // Make sure the notifiable event is not a ringing one forceNotify = true, - ) + ).getOrNull() notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) } } } 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 070f8769733..fab6e3a53a7 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 @@ -9,20 +9,22 @@ package io.element.android.libraries.push.impl.notifications import android.content.Intent import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.libraries.matrix.api.core.asEventId -import io.element.android.libraries.matrix.api.room.MatrixRoom +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.message.replyInThread import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived +import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope @@ -35,6 +37,7 @@ import javax.inject.Inject private val loggerTag = LoggerTag("NotificationBroadcastReceiverHandler", LoggerTag.NotificationLoggerTag) class NotificationBroadcastReceiverHandler @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val matrixClientProvider: MatrixClientProvider, private val sessionPreferencesStore: SessionPreferencesStoreFactory, @@ -44,6 +47,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( private val onNotifiableEventReceived: OnNotifiableEventReceived, private val stringProvider: StringProvider, private val replyMessageExtractor: ReplyMessageExtractor, + private val activeRoomsHolder: ActiveRoomsHolder, ) { fun onReceive(intent: Intent) { val sessionId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return @@ -54,7 +58,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}") when (intent.action) { actionIds.smartReply -> if (roomId != null) { - handleSmartReply(sessionId, roomId, threadId, intent) + handleSmartReply(sessionId, roomId, eventId, threadId, intent) } actionIds.dismissRoom -> if (roomId != null) { notificationCleaner.clearMessagesForRoom(sessionId, roomId) @@ -106,6 +110,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( private fun handleSmartReply( sessionId: SessionId, roomId: RoomId, + replyToEventId: EventId?, threadId: ThreadId?, intent: Intent, ) = appCoroutineScope.launch { @@ -116,12 +121,15 @@ class NotificationBroadcastReceiverHandler @Inject constructor( return@launch } val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch - client.getRoom(roomId)?.let { room -> + val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId) ?: client.getJoinedRoom(roomId) + + room?.let { sendMatrixEvent( sessionId = sessionId, roomId = roomId, + replyToEventId = replyToEventId, threadId = threadId, - room = room, + room = it, message = message, ) } @@ -131,7 +139,8 @@ class NotificationBroadcastReceiverHandler @Inject constructor( sessionId: SessionId, roomId: RoomId, threadId: ThreadId?, - room: MatrixRoom, + replyToEventId: EventId?, + room: JoinedRoom, message: String, ) { // Create a new event to be displayed in the notification drawer, right now @@ -152,19 +161,19 @@ class NotificationBroadcastReceiverHandler @Inject constructor( imageUriString = null, imageMimeType = null, threadId = threadId, - roomName = room.displayName, - roomIsDm = room.isDm, + roomName = room.info().name, + roomIsDm = room.isDm(), outGoingMessage = true, ) - onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent) + onNotifiableEventReceived.onNotifiableEventsReceived(listOf(notifiableMessageEvent)) - if (threadId != null) { + if (threadId != null && replyToEventId != null) { room.liveTimeline.replyMessage( - eventId = threadId.asEventId(), body = message, htmlBody = null, intentionalMentions = emptyList(), fromNotification = true, + replyParameters = replyInThread(replyToEventId), ) } else { room.liveTimeline.sendMessage( @@ -174,9 +183,11 @@ class NotificationBroadcastReceiverHandler @Inject constructor( ) }.onFailure { Timber.e(it, "Failed to send smart reply message") - onNotifiableEventReceived.onNotifiableEventReceived( - notifiableMessageEvent.copy( - outGoingMessageFailed = true + onNotifiableEventReceived.onNotifiableEventsReceived( + listOf( + notifiableMessageEvent.copy( + outGoingMessageFailed = true + ) ) ) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt index ab82f3c6437..52c633820bf 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt @@ -12,7 +12,7 @@ import android.graphics.Typeface import android.text.style.StyleSpan import androidx.core.text.buildSpannedString import androidx.core.text.inSpans -import coil.ImageLoader +import coil3.ImageLoader import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.RoomId diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt index 09bfbb51ba4..88e6021cc43 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt @@ -37,6 +37,7 @@ class DefaultNotificationDisplayer @Inject constructor( return false } notificationManager.notify(tag, id, notification) + Timber.d("Notifying with tag: $tag, id: $id") return true } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt index 41dd3914707..5483c765786 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.matrix.api.MatrixClient @@ -85,7 +86,7 @@ class DefaultNotificationMediaRepo @AssistedInject constructor( source = mediaSource, mimeType = mimeType, filename = filename, - ).mapCatching { + ).mapCatchingExceptions { it.use { mediaFile -> val dest = cachedFile.apply { parentFile?.mkdirs() } if (mediaFile.persist(dest.path)) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index f68dc1e4dc2..79d127b0097 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.push.impl.notifications -import coil.ImageLoader +import coil3.ImageLoader import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.api.notifications.NotificationIdProvider diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt new file mode 100644 index 00000000000..a3ba36cfe85 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt @@ -0,0 +1,103 @@ +/* + * 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.push.impl.notifications + +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope +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.push.impl.notifications.model.ResolvedPushEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds + +/** + * This class is responsible for periodically batching notification requests and resolving them in a single call, + * so that we can avoid having to resolve each notification individually in the SDK. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SingleIn(AppScope::class) +class NotificationResolverQueue @Inject constructor( + private val notifiableEventResolver: NotifiableEventResolver, + @AppCoroutineScope + private val appCoroutineScope: CoroutineScope, +) { + companion object { + private const val BATCH_WINDOW_MS = 250L + } + private val requestQueue = Channel(capacity = 100) + + private var currentProcessingJob: Job? = null + + /** + * A flow that emits pairs of a list of notification event requests and a map of the resolved events. + * The map contains the original request as the key and the resolved event as the value. + */ + val results: SharedFlow, Map>>> = MutableSharedFlow() + + /** + * Enqueues a notification event request to be resolved. + * The request will be processed in batches, so it may not be resolved immediately. + * + * @param request The notification event request to enqueue. + */ + suspend fun enqueue(request: NotificationEventRequest) { + // Cancel previous processing job if it exists, acting as a debounce operation + Timber.d("Cancelling job: $currentProcessingJob") + currentProcessingJob?.cancel() + + // Enqueue the request and start a delayed processing job + requestQueue.send(request) + currentProcessingJob = processQueue() + Timber.d("Starting processing job for request: $request") + } + + private fun processQueue() = appCoroutineScope.launch(SupervisorJob()) { + delay(BATCH_WINDOW_MS.milliseconds) + + // If this job is still active (so this is the latest job), we launch a separate one that won't be cancelled when enqueueing new items + // to process the existing queued items. + appCoroutineScope.launch { + val groupedRequestsById = buildList { + while (!requestQueue.isEmpty) { + requestQueue.receiveCatching().getOrNull()?.let(this::add) + } + }.groupBy { it.sessionId } + + val sessionIds = groupedRequestsById.keys + for (sessionId in sessionIds) { + val requests = groupedRequestsById[sessionId].orEmpty() + Timber.d("Fetching notifications for $sessionId: $requests. Pending requests: ${!requestQueue.isEmpty}") + // Resolving the events in parallel should improve performance since each session id will query a different Client + launch { + // No need for a Mutex since the SDK already has one internally + val notifications = notifiableEventResolver.resolveEvents(sessionId, requests).getOrNull().orEmpty() + (results as MutableSharedFlow).emit(requests to notifications) + } + } + } + } +} + +data class NotificationEventRequest( + val sessionId: SessionId, + val roomId: RoomId, + val eventId: EventId, + val providerInfo: String, +) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ResolvingException.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ResolvingException.kt new file mode 100644 index 00000000000..11c655ec1c9 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ResolvingException.kt @@ -0,0 +1,10 @@ +/* + * 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.push.impl.notifications + +class ResolvingException(message: String) : Exception(message) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index b78f7280748..3e8ddb87988 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import android.graphics.Bitmap -import coil.ImageLoader +import coil3.ImageLoader import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.RoomId diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt index 4f814fafd4f..e707f3ad51f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt @@ -7,18 +7,16 @@ package io.element.android.libraries.push.impl.notifications.channels -import android.content.Context import android.media.AudioAttributes import android.media.AudioManager -import android.media.RingtoneManager import android.os.Build +import android.provider.Settings import androidx.annotation.ChecksSdkIntAtLeast import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationManagerCompat import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.NotificationConfig import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn import io.element.android.libraries.push.impl.R import io.element.android.services.toolbox.api.strings.StringProvider @@ -60,7 +58,6 @@ private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSI @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultNotificationChannels @Inject constructor( - @ApplicationContext private val context: Context, private val notificationManager: NotificationManagerCompat, private val stringProvider: StringProvider, ) : NotificationChannels { @@ -153,8 +150,6 @@ class DefaultNotificationChannels @Inject constructor( ) // Register a channel for incoming call notifications which will ring the device when received - // TODO use a fallback ringtone if the default ringtone is not available - val ringtoneUri = runCatching { RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) }.getOrNull() notificationManager.createNotificationChannel( NotificationChannelCompat.Builder( RINGING_CALL_NOTIFICATION_CHANNEL_ID, @@ -162,18 +157,14 @@ class DefaultNotificationChannels @Inject constructor( ) .setName(stringProvider.getString(R.string.notification_channel_ringing_calls).ifEmpty { "Ringing calls" }) .setVibrationEnabled(true) - .apply { - if (ringtoneUri != null) { - setSound( - ringtoneUri, - AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setLegacyStreamType(AudioManager.STREAM_RING) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) - .build() - ) - } - } + .setSound( + Settings.System.DEFAULT_RINGTONE_URI, + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setLegacyStreamType(AudioManager.STREAM_RING) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build() + ) .setDescription(stringProvider.getString(R.string.notification_channel_ringing_calls)) .setLightsEnabled(true) .setLightColor(accentColor) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index 1ecf45921d9..0b07b62ea9f 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -16,7 +16,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.Person import androidx.core.content.res.ResourcesCompat -import coil.ImageLoader +import coil3.ImageLoader import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.NotificationConfig import io.element.android.libraries.core.meta.BuildMeta @@ -119,7 +119,7 @@ class DefaultNotificationCreator @Inject constructor( else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId) } - val smallIcon = CommonDrawables.ic_notification_small + val smallIcon = CommonDrawables.ic_notification val containsMissedCall = events.any { it.type == EventType.CALL_NOTIFY } val channelId = if (containsMissedCall) { @@ -197,7 +197,8 @@ class DefaultNotificationCreator @Inject constructor( addAction(markAsReadActionFactory.create(roomInfo)) // Quick reply if (!roomInfo.hasSmartReplyError) { - addAction(quickReplyActionFactory.create(roomInfo, threadId)) + val latestEventId = events.lastOrNull()?.eventId + addAction(quickReplyActionFactory.create(roomInfo, latestEventId, threadId)) } if (openIntent != null) { setContentIntent(openIntent) @@ -219,7 +220,7 @@ class DefaultNotificationCreator @Inject constructor( override fun createRoomInvitationNotification( inviteNotifiableEvent: InviteNotifiableEvent ): Notification { - val smallIcon = CommonDrawables.ic_notification_small + val smallIcon = CommonDrawables.ic_notification val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) @@ -261,7 +262,7 @@ class DefaultNotificationCreator @Inject constructor( override fun createSimpleEventNotification( simpleNotifiableEvent: SimpleNotifiableEvent, ): Notification { - val smallIcon = CommonDrawables.ic_notification_small + val smallIcon = CommonDrawables.ic_notification val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) @@ -294,7 +295,7 @@ class DefaultNotificationCreator @Inject constructor( override fun createFallbackNotification( fallbackNotifiableEvent: FallbackNotifiableEvent, ): Notification { - val smallIcon = CommonDrawables.ic_notification_small + val smallIcon = CommonDrawables.ic_notification val channelId = notificationChannels.getChannelIdForMessage(false) return NotificationCompat.Builder(context, channelId) @@ -330,7 +331,7 @@ class DefaultNotificationCreator @Inject constructor( noisy: Boolean, lastMessageTimestamp: Long ): Notification { - val smallIcon = CommonDrawables.ic_notification_small + val smallIcon = CommonDrawables.ic_notification val channelId = notificationChannels.getChannelIdForMessage(noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) @@ -367,7 +368,7 @@ class DefaultNotificationCreator @Inject constructor( return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest()) .setContentTitle(buildMeta.applicationName) .setContentText(stringProvider.getString(R.string.notification_test_push_notification_content)) - .setSmallIcon(CommonDrawables.ic_notification_small) + .setSmallIcon(CommonDrawables.revivalist_logo_shot) .setLargeIcon(getBitmap(io.element.android.libraries.designsystem.R.drawable.revivalist_logo_shot)) .setColor(accentColor) .setPriority(NotificationCompat.PRIORITY_MAX) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt index f8efa9b0e76..6a590aeda84 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt @@ -16,6 +16,7 @@ import androidx.core.app.RemoteInput import io.element.android.appconfig.NotificationConfig import io.element.android.libraries.androidutils.uri.createIgnoredUri import io.element.android.libraries.di.ApplicationContext +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.core.ThreadId @@ -33,11 +34,11 @@ class QuickReplyActionFactory @Inject constructor( private val stringProvider: StringProvider, private val clock: SystemClock, ) { - fun create(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): NotificationCompat.Action? { + fun create(roomInfo: RoomEventGroupInfo, eventId: EventId?, threadId: ThreadId?): NotificationCompat.Action? { if (!NotificationConfig.SHOW_QUICK_REPLY_ACTION) return null val sessionId = roomInfo.sessionId val roomId = roomInfo.roomId - val replyPendingIntent = buildQuickReplyIntent(sessionId, roomId, threadId) + val replyPendingIntent = buildQuickReplyIntent(sessionId, roomId, eventId, threadId) val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) .setLabel(stringProvider.getString(R.string.notification_room_action_quick_reply)) .build() @@ -63,6 +64,7 @@ class QuickReplyActionFactory @Inject constructor( private fun buildQuickReplyIntent( sessionId: SessionId, roomId: RoomId, + eventId: EventId?, threadId: ThreadId?, ): PendingIntent { val intent = Intent(context, NotificationBroadcastReceiver::class.java) @@ -70,9 +72,8 @@ class QuickReplyActionFactory @Inject constructor( intent.data = createIgnoredUri("quickReply/$sessionId/$roomId" + threadId?.let { "/$it" }.orEmpty()) intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value) - threadId?.let { - intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value) - } + eventId?.let { intent.putExtra(NotificationBroadcastReceiver.KEY_EVENT_ID, it.value) } + threadId?.let { intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value) } return PendingIntent.getBroadcast( context, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt index 21feeb1cd3f..ba7cd2f78b5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.push.impl.notifications.model import android.net.Uri +import androidx.core.net.toUri 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 @@ -52,7 +53,7 @@ data class NotifiableMessageEvent( // Example of value: // content://io.element.android.x.debug.notifications.fileprovider/downloads/temp/notif/matrix.org/XGItzSDOnSyXjYtOPfiKexDJ val imageUri: Uri? - get() = imageUriString?.let { Uri.parse(it) } + get() = imageUriString?.toUri() } /** diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt index bf2de5c32d3..07e85ab2bfc 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt @@ -12,8 +12,6 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.CallNotifyType -import java.time.Instant -import kotlin.time.Duration.Companion.seconds data class NotifiableRingingCallEvent( override val sessionId: SessionId, @@ -31,13 +29,4 @@ data class NotifiableRingingCallEvent( val roomAvatarUrl: String? = null, val callNotifyType: CallNotifyType, val timestamp: Long, -) : NotifiableEvent { - companion object { - fun shouldRing(callNotifyType: CallNotifyType, timestamp: Long): Boolean { - val timeout = 10.seconds.inWholeMilliseconds - val elapsed = Instant.now().toEpochMilli() - timestamp - // Only ring if the type is RING and the elapsed time is less than the timeout - return callNotifyType == CallNotifyType.RING && elapsed < timeout - } - } -} +) : NotifiableEvent diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/ResolvedPushEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/ResolvedPushEvent.kt index 48e2af6e980..2363e38e1d7 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/ResolvedPushEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/ResolvedPushEvent.kt @@ -12,12 +12,22 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId sealed interface ResolvedPushEvent { - data class Event(val notifiableEvent: NotifiableEvent) : ResolvedPushEvent + val sessionId: SessionId + val roomId: RoomId + val eventId: EventId + + data class Event(val notifiableEvent: NotifiableEvent) : ResolvedPushEvent { + override val sessionId: SessionId = notifiableEvent.sessionId + override val roomId: RoomId = notifiableEvent.roomId + override val eventId: EventId = notifiableEvent.eventId + } data class Redaction( - val sessionId: SessionId, - val roomId: RoomId, + override val sessionId: SessionId, + override val roomId: RoomId, val redactedEventId: EventId, val reason: String?, - ) : ResolvedPushEvent + ) : ResolvedPushEvent { + override val eventId: EventId = redactedEventId + } } 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 1e5066a5c86..4d9e3906900 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 @@ -13,9 +13,20 @@ import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver +import io.element.android.libraries.push.impl.history.PushHistoryService +import io.element.android.libraries.push.impl.history.onDiagnosticPush +import io.element.android.libraries.push.impl.history.onInvalidPushReceived +import io.element.android.libraries.push.impl.history.onSuccess +import io.element.android.libraries.push.impl.history.onUnableToResolveEvent +import io.element.android.libraries.push.impl.history.onUnableToRetrieveSession +import io.element.android.libraries.push.impl.notifications.NotificationEventRequest +import io.element.android.libraries.push.impl.notifications.NotificationResolverQueue import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels +import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.test.DefaultTestPush @@ -24,18 +35,23 @@ import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag) +@SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultPushHandler @Inject constructor( private val onNotifiableEventReceived: OnNotifiableEventReceived, private val onRedactedEventReceived: OnRedactedEventReceived, - private val notifiableEventResolver: NotifiableEventResolver, private val incrementPushDataStore: IncrementPushDataStore, + private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore, private val userPushStoreFactory: UserPushStoreFactory, private val pushClientSecret: PushClientSecret, private val buildMeta: BuildMeta, @@ -43,13 +59,119 @@ class DefaultPushHandler @Inject constructor( private val diagnosticPushHandler: DiagnosticPushHandler, private val elementCallEntryPoint: ElementCallEntryPoint, private val notificationChannels: NotificationChannels, + private val pushHistoryService: PushHistoryService, + private val resolverQueue: NotificationResolverQueue, + @AppCoroutineScope + private val appCoroutineScope: CoroutineScope, ) : PushHandler { + init { + processPushEventResults() + } + + /** + * Process the push notification event results emitted by the [resolverQueue]. + */ + private fun processPushEventResults() { + resolverQueue.results + .map { (requests, resolvedEvents) -> + for (request in requests) { + // Log the result of the push notification event + val result = resolvedEvents[request] + if (result == null) { + pushHistoryService.onUnableToResolveEvent( + providerInfo = request.providerInfo, + eventId = request.eventId, + roomId = request.roomId, + sessionId = request.sessionId, + reason = "Push not handled: no result found for request", + ) + } else { + result.fold( + onSuccess = { + if (it is ResolvedPushEvent.Event && it.notifiableEvent is FallbackNotifiableEvent) { + pushHistoryService.onUnableToResolveEvent( + providerInfo = request.providerInfo, + eventId = request.eventId, + roomId = request.roomId, + sessionId = request.sessionId, + reason = "Showing fallback notification", + ) + mutableBatteryOptimizationStore.showBatteryOptimizationBanner() + } else { + pushHistoryService.onSuccess( + providerInfo = request.providerInfo, + eventId = request.eventId, + roomId = request.roomId, + sessionId = request.sessionId, + comment = "Push handled successfully", + ) + } + }, + onFailure = { exception -> + pushHistoryService.onUnableToResolveEvent( + providerInfo = request.providerInfo, + eventId = request.eventId, + roomId = request.roomId, + sessionId = request.sessionId, + reason = exception.message ?: exception.javaClass.simpleName, + ) + mutableBatteryOptimizationStore.showBatteryOptimizationBanner() + } + ) + } + } + + val events = mutableListOf() + val redactions = mutableListOf() + + @Suppress("LoopWithTooManyJumpStatements") + for (result in resolvedEvents.values) { + val event = result.getOrNull() ?: continue + val userPushStore = userPushStoreFactory.getOrCreate(event.sessionId) + val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first() + // If notifications are disabled for this session and device, we don't want to show the notification + // But if it's a ringing call, we want to show it anyway + val isRingingCall = (event as? ResolvedPushEvent.Event)?.notifiableEvent is NotifiableRingingCallEvent + if (!areNotificationsEnabled && !isRingingCall) continue + + // We categorise each result into either a NotifiableEvent or a Redaction + when (event) { + is ResolvedPushEvent.Event -> { + events.add(event.notifiableEvent) + } + is ResolvedPushEvent.Redaction -> { + redactions.add(event) + } + } + } + + // Process redactions of messages + if (redactions.isNotEmpty()) { + onRedactedEventReceived.onRedactedEventsReceived(redactions) + } + + // Find and process ringing call notifications separately + val (ringingCallEvents, nonRingingCallEvents) = events.partition { it is NotifiableRingingCallEvent } + for (ringingCallEvent in ringingCallEvents) { + Timber.tag(loggerTag.value).d("Ringing call event: $ringingCallEvent") + handleRingingCallEvent(ringingCallEvent as NotifiableRingingCallEvent) + } + + // Finally, process other notifications (messages, invites, generic notifications, etc.) + if (nonRingingCallEvents.isNotEmpty()) { + onNotifiableEventReceived.onNotifiableEventsReceived(nonRingingCallEvents) + } + } + .launchIn(appCoroutineScope) + } + /** * Called when message is received. * * @param pushData the data received in the push. + * @param providerInfo the provider info. */ - override suspend fun handle(pushData: PushData) { + override suspend fun handle(pushData: PushData, providerInfo: String) { Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}") if (buildMeta.lowPrivacyLoggingEnabled) { Timber.tag(loggerTag.value).d("## pushData: $pushData") @@ -57,18 +179,25 @@ class DefaultPushHandler @Inject constructor( incrementPushDataStore.incrementPushCounter() // Diagnostic Push if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) { + pushHistoryService.onDiagnosticPush(providerInfo) diagnosticPushHandler.handlePush() } else { - handleInternal(pushData) + handleInternal(pushData, providerInfo) } } + override suspend fun handleInvalid(providerInfo: String, data: String) { + incrementPushDataStore.incrementPushCounter() + pushHistoryService.onInvalidPushReceived(providerInfo, data) + } + /** * Internal receive method. * * @param pushData Object containing message data. + * @param providerInfo the provider info. */ - private suspend fun handleInternal(pushData: PushData) { + private suspend fun handleInternal(pushData: PushData, providerInfo: String) { try { if (buildMeta.lowPrivacyLoggingEnabled) { Timber.tag(loggerTag.value).d("## handleInternal() : $pushData") @@ -77,48 +206,50 @@ class DefaultPushHandler @Inject constructor( } val clientSecret = pushData.clientSecret // clientSecret should not be null. If this happens, restore default session - val userId = clientSecret - ?.let { - // Get userId from client secret - pushClientSecret.getUserIdFromSecret(clientSecret) + var reason = if (clientSecret == null) "No client secret" else "" + val userId = clientSecret?.let { + // Get userId from client secret + pushClientSecret.getUserIdFromSecret(clientSecret).also { + if (it == null) { + reason = "Unable to get userId from client secret" + } } + } ?: run { - matrixAuthenticationService.getLatestSessionId() + matrixAuthenticationService.getLatestSessionId().also { + if (it == null) { + if (reason.isNotEmpty()) reason += " - " + reason += "Unable to get latest sessionId" + } + } } if (userId == null) { Timber.w("Unable to get a session") + pushHistoryService.onUnableToRetrieveSession( + providerInfo = providerInfo, + eventId = pushData.eventId, + roomId = pushData.roomId, + reason = reason, + ) return } - val resolvedPushEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId) - when (resolvedPushEvent) { - null -> Timber.tag(loggerTag.value).w("Unable to get a notification data") - is ResolvedPushEvent.Event -> { - when (val notifiableEvent = resolvedPushEvent.notifiableEvent) { - is NotifiableRingingCallEvent -> { - onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent) - handleRingingCallEvent(notifiableEvent) - } - else -> { - val userPushStore = userPushStoreFactory.getOrCreate(userId) - val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first() - if (areNotificationsEnabled) { - onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent) - } else { - Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") - } - } - } - } - is ResolvedPushEvent.Redaction -> { - onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent) - } + + appCoroutineScope.launch { + val notificationEventRequest = NotificationEventRequest( + sessionId = userId, + roomId = pushData.roomId, + eventId = pushData.eventId, + providerInfo = providerInfo, + ) + Timber.d("Queueing notification: $notificationEventRequest") + resolverQueue.enqueue(notificationEventRequest) } } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") } } - private fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) { + private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) { Timber.i("## handleInternal() : Incoming call.") elementCallEntryPoint.handleIncomingCall( callType = CallType.RoomCall(notifiableEvent.sessionId, notifiableEvent.roomId), @@ -129,6 +260,7 @@ class DefaultPushHandler @Inject constructor( avatarUrl = notifiableEvent.roomAvatarUrl, timestamp = notifiableEvent.timestamp, notificationChannelId = notificationChannels.getChannelForIncomingCall(ring = true), + textContent = notifiableEvent.description, ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt new file mode 100644 index 00000000000..14e2cfd97b8 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt @@ -0,0 +1,36 @@ +/* + * 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.push.impl.push + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.impl.store.DefaultPushDataStore +import javax.inject.Inject + +interface MutableBatteryOptimizationStore { + suspend fun showBatteryOptimizationBanner() + suspend fun onOptimizationBannerDismissed() + suspend fun reset() +} + +@ContributesBinding(AppScope::class) +class DefaultMutableBatteryOptimizationStore @Inject constructor( + private val defaultPushDataStore: DefaultPushDataStore, +) : MutableBatteryOptimizationStore { + override suspend fun showBatteryOptimizationBanner() { + defaultPushDataStore.setBatteryOptimizationBannerState(DefaultPushDataStore.BATTERY_OPTIMIZATION_BANNER_STATE_SHOW) + } + + override suspend fun onOptimizationBannerDismissed() { + defaultPushDataStore.setBatteryOptimizationBannerState(DefaultPushDataStore.BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED) + } + + override suspend fun reset() { + defaultPushDataStore.setBatteryOptimizationBannerState(DefaultPushDataStore.BATTERY_OPTIMIZATION_BANNER_STATE_INIT) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt index 7269dbcff77..b311aab4e2c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.push import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent @@ -17,21 +18,20 @@ import kotlinx.coroutines.launch import javax.inject.Inject interface OnNotifiableEventReceived { - fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) + fun onNotifiableEventsReceived(notifiableEvents: List) } @ContributesBinding(AppScope::class) class DefaultOnNotifiableEventReceived @Inject constructor( private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, + @AppCoroutineScope private val coroutineScope: CoroutineScope, private val syncOnNotifiableEvent: SyncOnNotifiableEvent, ) : OnNotifiableEventReceived { - override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { + override fun onNotifiableEventsReceived(notifiableEvents: List) { coroutineScope.launch { - launch { syncOnNotifiableEvent(notifiableEvent) } - if (notifiableEvent !is NotifiableRingingCallEvent) { - defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent) - } + launch { syncOnNotifiableEvent(notifiableEvents) } + defaultNotificationDrawerManager.onNotifiableEventsReceived(notifiableEvents.filter { it !is NotifiableRingingCallEvent }) } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt index bf452bd3b7b..991baba4041 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt @@ -17,6 +17,7 @@ import androidx.core.text.inSpans import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.push.impl.notifications.ActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.NotificationDisplayer import io.element.android.libraries.push.impl.notifications.factories.DefaultNotificationCreator @@ -29,59 +30,66 @@ import timber.log.Timber import javax.inject.Inject interface OnRedactedEventReceived { - fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) + fun onRedactedEventsReceived(redactions: List) } @ContributesBinding(AppScope::class) class DefaultOnRedactedEventReceived @Inject constructor( private val activeNotificationsProvider: ActiveNotificationsProvider, private val notificationDisplayer: NotificationDisplayer, + @AppCoroutineScope private val coroutineScope: CoroutineScope, @ApplicationContext private val context: Context, private val stringProvider: StringProvider, ) : OnRedactedEventReceived { - override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) { + override fun onRedactedEventsReceived(redactions: List) { coroutineScope.launch { - val notifications = activeNotificationsProvider.getMessageNotificationsForRoom( - redaction.sessionId, - redaction.roomId, - ) - if (notifications.isEmpty()) { - Timber.d("No notifications found for redacted event") + val redactionsBySessionIdAndRoom = redactions.groupBy { redaction -> + redaction.sessionId to redaction.roomId } - notifications.forEach { statusBarNotification -> - val notification = statusBarNotification.notification - val messagingStyle = MessagingStyle.extractMessagingStyleFromNotification(notification) - if (messagingStyle == null) { - Timber.w("Unable to retrieve messaging style from notification") - return@forEach - } - val messageToRedactIndex = messagingStyle.messages.indexOfFirst { message -> - message.extras.getString(DefaultNotificationCreator.MESSAGE_EVENT_ID) == redaction.redactedEventId.value - } - if (messageToRedactIndex == -1) { - Timber.d("Unable to find the message to remove from notification") - return@forEach + for ((keys, roomRedactions) in redactionsBySessionIdAndRoom) { + val (sessionId, roomId) = keys + val notifications = activeNotificationsProvider.getMessageNotificationsForRoom( + sessionId, + roomId, + ) + if (notifications.isEmpty()) { + Timber.d("No notifications found for redacted event") } - val oldMessage = messagingStyle.messages[messageToRedactIndex] - val content = buildSpannedString { - inSpans(StyleSpan(Typeface.ITALIC)) { - append(stringProvider.getString(CommonStrings.common_message_removed)) + notifications.forEach { statusBarNotification -> + val notification = statusBarNotification.notification + val messagingStyle = MessagingStyle.extractMessagingStyleFromNotification(notification) + if (messagingStyle == null) { + Timber.w("Unable to retrieve messaging style from notification") + return@forEach + } + val messageToRedactIndex = messagingStyle.messages.indexOfFirst { message -> + roomRedactions.any { it.redactedEventId.value == message.extras.getString(DefaultNotificationCreator.MESSAGE_EVENT_ID) } } + if (messageToRedactIndex == -1) { + Timber.d("Unable to find the message to remove from notification") + return@forEach + } + val oldMessage = messagingStyle.messages[messageToRedactIndex] + val content = buildSpannedString { + inSpans(StyleSpan(Typeface.ITALIC)) { + append(stringProvider.getString(CommonStrings.common_message_removed)) + } + } + val newMessage = MessagingStyle.Message( + content, + oldMessage.timestamp, + oldMessage.person + ) + messagingStyle.messages[messageToRedactIndex] = newMessage + notificationDisplayer.showNotificationMessage( + statusBarNotification.tag, + statusBarNotification.id, + NotificationCompat.Builder(context, notification) + .setStyle(messagingStyle) + .build() + ) } - val newMessage = MessagingStyle.Message( - content, - oldMessage.timestamp, - oldMessage.person - ) - messagingStyle.messages[messageToRedactIndex] = newMessage - notificationDisplayer.showNotificationMessage( - statusBarNotification.tag, - statusBarNotification.id, - NotificationCompat.Builder(context, notification) - .setStyle(messagingStyle) - .build() - ) } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEvent.kt index 58e148c184a..57c004ce234 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEvent.kt @@ -11,19 +11,12 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.sync.SyncService -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent -import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.services.appnavstate.api.AppForegroundStateService -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull -import java.util.concurrent.atomic.AtomicInteger +import timber.log.Timber import javax.inject.Inject -import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds class SyncOnNotifiableEvent @Inject constructor( @@ -32,65 +25,31 @@ class SyncOnNotifiableEvent @Inject constructor( private val appForegroundStateService: AppForegroundStateService, private val dispatchers: CoroutineDispatchers, ) { - private var syncCounter = AtomicInteger(0) - - suspend operator fun invoke(notifiableEvent: NotifiableEvent) = withContext(dispatchers.io) { - val isRingingCallEvent = notifiableEvent is NotifiableRingingCallEvent - if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush) && !isRingingCallEvent) { + suspend operator fun invoke(notifiableEvents: List) = withContext(dispatchers.io) { + if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush)) { return@withContext } - val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@withContext - client.getRoom(notifiableEvent.roomId)?.use { room -> - room.subscribeToSync() - // If the app is in foreground, sync is already running, so just add the subscription. - if (!appForegroundStateService.isInForeground.value) { - val syncService = client.syncService() - syncService.startSyncIfNeeded() - if (isRingingCallEvent) { - room.waitsUntilUserIsInTheCall(timeout = 60.seconds) - } else { - room.waitsUntilEventIsKnown(eventId = notifiableEvent.eventId, timeout = 10.seconds) - } - syncService.stopSyncIfNeeded() - } - } - } + try { + val eventsBySession = notifiableEvents.groupBy { it.sessionId } - /** - * User can be in the call if they answer using another session. - * If the user does not join the call, the timeout will be reached. - */ - private suspend fun MatrixRoom.waitsUntilUserIsInTheCall(timeout: Duration) { - withTimeoutOrNull(timeout) { - roomInfoFlow.first { - sessionId in it.activeRoomCallParticipants - } - } - } + appForegroundStateService.updateIsSyncingNotificationEvent(true) + Timber.d("Starting opportunistic room list sync | In foreground: ${appForegroundStateService.isInForeground.value}") - private suspend fun MatrixRoom.waitsUntilEventIsKnown(eventId: EventId, timeout: Duration) { - withTimeoutOrNull(timeout) { - liveTimeline.timelineItems.first { timelineItems -> - timelineItems.any { timelineItem -> - when (timelineItem) { - is MatrixTimelineItem.Event -> timelineItem.eventId == eventId - else -> false - } - } - } - } - } + for ((sessionId, events) in eventsBySession) { + val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: continue + val eventsByRoomId = events.groupBy { it.roomId } - private suspend fun SyncService.startSyncIfNeeded() { - if (syncCounter.getAndIncrement() == 0) { - startSync() - } - } + client.roomListService.subscribeToVisibleRooms(eventsByRoomId.keys.toList()) - private suspend fun SyncService.stopSyncIfNeeded() { - if (syncCounter.decrementAndGet() == 0 && !appForegroundStateService.isInForeground.value) { - stopSync() + if (!appForegroundStateService.isInForeground.value) { + // Give the sync some time to complete in background + delay(10.seconds) + } + } + } finally { + Timber.d("Finished opportunistic room list sync") + appForegroundStateService.updateIsSyncingNotificationEvent(false) } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt index e5e472722e8..a4d9413cb14 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt @@ -13,11 +13,20 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import app.cash.sqldelight.coroutines.asFlow +import app.cash.sqldelight.coroutines.mapToList import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.dateformatter.api.DateFormatter +import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.push.api.store.PushDataStore +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.push.api.history.PushHistoryItem +import io.element.android.libraries.push.impl.PushDatabase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -28,17 +37,83 @@ private val Context.dataStore: DataStore by preferencesDataStore(na @ContributesBinding(AppScope::class) class DefaultPushDataStore @Inject constructor( @ApplicationContext private val context: Context, + private val pushDatabase: PushDatabase, + private val dateFormatter: DateFormatter, + private val dispatchers: CoroutineDispatchers, ) : PushDataStore { private val pushCounter = intPreferencesKey("push_counter") + /** + * Integer preference to track the state of the battery optimization banner. + * Possible values: + * [BATTERY_OPTIMIZATION_BANNER_STATE_INIT]: Should not show the banner + * [BATTERY_OPTIMIZATION_BANNER_STATE_SHOW]: Should show the banner + * [BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED]: Banner has been shown and user has dismissed it + */ + private val batteryOptimizationBannerState = intPreferencesKey("battery_optimization_banner_state") + override val pushCounterFlow: Flow = context.dataStore.data.map { preferences -> preferences[pushCounter] ?: 0 } + @Suppress("UnnecessaryParentheses") + override val shouldDisplayBatteryOptimizationBannerFlow: Flow = context.dataStore.data.map { preferences -> + (preferences[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT) == BATTERY_OPTIMIZATION_BANNER_STATE_SHOW + } + suspend fun incrementPushCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[pushCounter] ?: 0 settings[pushCounter] = currentCounterValue + 1 } } + + suspend fun setBatteryOptimizationBannerState(newState: Int) { + context.dataStore.edit { settings -> + val currentValue = settings[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT + settings[batteryOptimizationBannerState] = when (currentValue) { + BATTERY_OPTIMIZATION_BANNER_STATE_INIT, + BATTERY_OPTIMIZATION_BANNER_STATE_SHOW -> newState + BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED -> currentValue + else -> error("Invalid value for showBatteryOptimizationBanner: $currentValue") + } + } + } + + override fun getPushHistoryItemsFlow(): Flow> { + return pushDatabase.pushHistoryQueries.selectAll() + .asFlow() + .mapToList(dispatchers.io) + .map { items -> + items.map { pushHistory -> + PushHistoryItem( + pushDate = pushHistory.pushDate, + formattedDate = dateFormatter.format( + timestamp = pushHistory.pushDate, + mode = DateFormatterMode.Full, + useRelative = false, + ), + providerInfo = pushHistory.providerInfo, + eventId = pushHistory.eventId?.let { EventId(it) }, + roomId = pushHistory.roomId?.let { RoomId(it) }, + sessionId = pushHistory.sessionId?.let { SessionId(it) }, + hasBeenResolved = pushHistory.hasBeenResolved == 1L, + comment = pushHistory.comment, + ) + } + } + } + + override suspend fun reset() { + pushDatabase.pushHistoryQueries.removeAll() + context.dataStore.edit { + it.clear() + } + } + + companion object { + const val BATTERY_OPTIMIZATION_BANNER_STATE_INIT = 0 + const val BATTERY_OPTIMIZATION_BANNER_STATE_SHOW = 1 + const val BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED = 2 + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/PushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/PushDataStore.kt new file mode 100644 index 00000000000..de8f6dbbc1e --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/PushDataStore.kt @@ -0,0 +1,26 @@ +/* + * 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.push.impl.store + +import io.element.android.libraries.push.api.history.PushHistoryItem +import kotlinx.coroutines.flow.Flow + +interface PushDataStore { + val shouldDisplayBatteryOptimizationBannerFlow: Flow + val pushCounterFlow: Flow + + /** + * Get a flow of list of [PushHistoryItem]. + */ + fun getPushHistoryItemsFlow(): Flow> + + /** + * Reset the push counter to 0, and clear the database. + */ + suspend fun reset() +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt index 01f728d23c6..ee140e7f6cf 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt @@ -63,6 +63,7 @@ class NotificationTest @Inject constructor( notificationClickHandler.state.first() Timber.d("Notification clicked!") } + @Suppress("RunCatchingNotAllowed") runCatching { withTimeout(30.seconds) { job.join() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt index 3285ecea100..3f392a3a178 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt @@ -76,6 +76,7 @@ class PushLoopbackTest @Inject constructor( job.cancel() return } + @Suppress("RunCatchingNotAllowed") runCatching { withTimeout(10.seconds) { completable.await() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTest.kt index d75c4f4d590..5a30db6f59e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTest.kt @@ -38,10 +38,8 @@ class PushProvidersTest @Inject constructor( val result = sortedPushProvider.isNotEmpty() if (result) { delegate.updateState( - description = stringProvider.getQuantityString( - resId = R.plurals.troubleshoot_notifications_test_detect_push_provider_success, - quantity = sortedPushProvider.size, - sortedPushProvider.size, + description = stringProvider.getString( + resId = R.string.troubleshoot_notifications_test_detect_push_provider_success_2, sortedPushProvider.joinToString { it.name } ), status = NotificationTroubleshootTestState.Status.Success diff --git a/libraries/push/impl/src/main/res/values-be/translations.xml b/libraries/push/impl/src/main/res/values-be/translations.xml index 0dc2611e58f..7a30be6f011 100644 --- a/libraries/push/impl/src/main/res/values-be/translations.xml +++ b/libraries/push/impl/src/main/res/values-be/translations.xml @@ -15,8 +15,7 @@ "%d апавяшчэнні" "%d апавяшчэнняў" - "Апавяшчэнне" - "Уваходны званок" + "📹 Уваходны выклік" "** Не атрымалася даслаць - калі ласка, адкрыйце пакой" "Далучыцца" "Адхіліць" @@ -40,6 +39,7 @@ "Запрасіў(-ла) вас далучыцца да пакоя" "%1$s запрасіў(-ла) вас далучыцца да пакоя" "Я" + "%1$s згадаў ці адказаў" "Вы праглядаеце апавяшчэнне! Націсніце мяне!" "%1$s: %2$s" "%1$s: %2$s %3$s" diff --git a/libraries/push/impl/src/main/res/values-bg/translations.xml b/libraries/push/impl/src/main/res/values-bg/translations.xml index 821ca2f5d8a..ca1d5c08c86 100644 --- a/libraries/push/impl/src/main/res/values-bg/translations.xml +++ b/libraries/push/impl/src/main/res/values-bg/translations.xml @@ -10,7 +10,6 @@ "%d известие" "%d известия" - "Известие" "** Неуспешно изпращане - моля, отворете стаята" "Присъединяване" diff --git a/libraries/push/impl/src/main/res/values-cs/translations.xml b/libraries/push/impl/src/main/res/values-cs/translations.xml index 15a5d22de92..d072a1c3b16 100644 --- a/libraries/push/impl/src/main/res/values-cs/translations.xml +++ b/libraries/push/impl/src/main/res/values-cs/translations.xml @@ -16,7 +16,7 @@ "%d oznámení" "Oznámení" - "Příchozí hovor" + "📹 Příchozí hovor" "** Nepodařilo se odeslat - otevřete prosím místnost" "Vstoupit" "Odmítnout" @@ -71,6 +71,7 @@ "Nalezeni %1$d push poskytovatelé: %2$s" "Nalezeno %1$d push poskytovatelů: %2$s" + "Aplikace byla vytvořena s podporou: %1$s" "Zjistit push poskytovatele" "Zkontrolujte, zda aplikace může zobrazit oznámení." "Na oznámení nebylo kliknuto." diff --git a/libraries/push/impl/src/main/res/values-cy/translations.xml b/libraries/push/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..d2e6a2810e7 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,110 @@ + + + "Galw" + "Gwrando am ddigwyddiadau" + "Hysbysiadau swnllyd" + "Galwadau\'n canu" + "Hysbysiadau tawel" + + "%1$s: %2$d negeseuon" + "%1$s: %2$d neges" + "%1$s: %2$d neges" + "%1$s: %2$d neges" + "%1$s: %2$d neges" + "%1$s: %2$d neges" + + + "%d hysbysiadau" + "%d hysbysiad" + "%d hysbysiad" + "%d hysbysiad" + "%d hysbysiad" + "%d hysbysiad" + + "Mae gennych chi negeseuon newydd." + "📹 Galwad i mewn" + "** Wedi methu anfon - agorwch yr ystafell" + "Ymuno" + "Gwrthod" + + "%d gwahoddiadau" + "%d gwahoddiadau" + "%d gwahoddiadau" + "%d gwahoddiadau" + "%d gwahoddiadau" + "%d gwahoddiadau" + + "Wedi eich gwahodd i sgwrsio" + "Mae %1$s wedi eich gwahodd i sgwrsio" + "Wedi eich crybwyll: %1$s" + "Negeseuon Newydd" + + "%d negeseuon newydd" + "%d neges newydd" + "%d neges newydd" + "%d neges newydd" + "%d neges newydd" + "%d neges newydd" + + "Wedi ymateb gyda %1$s" + "Marcio fel wedi\'i ddarllen" + "Ymateb cyflym" + "Wedi\'ch gwahodd i ymuno â\'r ystafell" + "Mae %1$s wedi eich gwahodd i ymuno â\'r ystafell" + "Fi" + "Crybwyllodd neu atebodd %1$s" + "Rydych chi\'n edrych ar yr hysbysiad! Cliciwch fi!" + "%1$s: %2$s" + "%1$s : %2$s %3$s" + + "%d negeseuon hysbyswyd heb eu darllen" + "%d neges hysbyswyd heb ei ddarllen" + "%d neges hysbyswyd heb eu darllen" + "%d neges hysbyswyd heb eu darllen" + "%d neges hysbyswyd heb eu darllen" + "%d neges hysbyswyd heb eu darllen" + + "%1$s a %2$s" + "%1$s yn %2$s" + "%1$s yn %2$s a %3$s" + + "%d ystafelloedd" + "%d ystafell" + "%d ystafell" + "%d ystafell" + "%d ystafell" + "%d ystafell" + + "Cydweddu\'n y cefndir" + "Gwasanaethau Google" + "Heb ganfod Google Play Services dilys. Efallai fydd hysbysiadau ddim yn gweithio\'n iawn." + "Cael enw\'r darparwr presennol." + "Dim darparwyr gwthio wedi\'u dewis." + "Darparwr gwthio presennol: %1$s." + "Darparwr gwthio presennol" + "Gwnewch yn siŵr fod y cais yn cefnogi o leiaf un darparwr gwthio." + "Heb ganfod cefnogaeth darparwr gwthio." + + "Wedi canfod %1$d darparwyr gwthio: %2$s" + "Wedi canfod %1$d darparwr gwthio: %2$s" + "Wedi canfod %1$d darparwr gwthio: %2$s" + "Wedi canfod %1$d darparwr gwthio: %2$s" + "Wedi canfod %1$d darparwr gwthio: %2$s" + "Wedi canfod %1$d darparwr gwthio: %2$s" + + "Adeiladwyd y rhaglen gyda chefnogaeth ar gyfer: %1$s" + "Cefnogaeth darparwr gwthio" + "Gwiriwch y gall y cais ddangos hysbysiad." + "Nid yw\'r hysbysiad wedi\'i glicio." + "Methu dangos yr hysbysiad." + "Mae\'r hysbysiad wedi\'i glicio!" + "Hysbysiad dangos" + "Cliciwch ar yr hysbysiad i barhau â\'r prawf." + "Gwnewch yn siŵr fod y cais yn cael ei wthio." + "Gwall: mae\'r gwthiwr wedi gwrthod y cais." + "Gwall: %1$s." + "Gwall, methu profi\'r gwthio." + "Gwall, terfyn amser wrth aros am y gwthio." + "Cymerodd dolen gwthio nôl %1$d ms." + "Prawf dolen gwthio\'n ôl" + diff --git a/libraries/push/impl/src/main/res/values-da/translations.xml b/libraries/push/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..facfc0eb3e8 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,82 @@ + + + "Opkald" + "Lytter efter begivenheder" + "Lyd på notifikationer" + "Ringende opkald" + "Lydløse notifikationer" + + "%1$s: %2$d besked" + "%1$s: %2$d beskeder" + + + "%d notifikation" + "%d notifikationer" + + "Du har nye beskeder." + "📹 Indgående opkald" + "** Kunne ikke sende - åbn venligst rummet" + "Deltag" + "Afvis" + + "%d invitation" + "%d invitationer" + + "Inviterede dig til at samtale" + "%1$s inviterede dig til at samtale" + "Nævnte dig: %1$s" + "Nye beskeder" + + "%d ny besked" + "%d nye beskeder" + + "Reagerede med%1$s" + "Marker som læst" + "Hurtigt svar" + "Inviterede dig til at deltage i rummet" + "%1$s inviterede dig til at deltage i rummet" + "Mig" + "%1$s nævnt eller besvaret" + "Du ser notifikationen! Klik på mig!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "%d ulæst besked" + "%d ulæste beskeder" + + "%1$s og %2$s" + "%1$s i %2$s" + "%1$s i %2$s og %3$s" + + "%d rum" + "%d rum" + + "Synkronisering i baggrunden" + "Google-tjenester" + "Der blev ikke fundet nogen gyldige Google Play-tjenester. Notifikationer fungerer muligvis ikke korrekt." + "Få navnet på den aktuelle udbyder." + "Ingen push-udbydere valgt." + "Nuværende push-udbyder: %1$s." + "Nuværende push-udbyder" + "Sørg for, at programmet understøtter mindst én push-udbyder." + "Ingen push-udbyder understøttelse fundet." + + "Fandt %1$d push-udbyder: %2$s" + "Fandt %1$d push-udbydere: %2$s" + + "Applikationen blev bygget med støtte til: %1$s" + "Understøttelse af push-udbydere" + "Kontrollér, at appen kan vise notifikationer." + "Der er ikke blevet klikket på meddelelsen." + "Kan ikke vise notifikationen." + "Der er blevet klikket på notifikationen!" + "Vis notifikation" + "Klik venligst på notifikationen for at fortsætte testen." + "Sørg for, at applikationen modtager push." + "Fejl: pusher har afvist anmodningen." + "Fejl: %1$s." + "Fejl, kan ikke teste push." + "Fejl, timeout venter på push." + "Push loop back tog %1$d ms." + "Afprøv Push loop back" + 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 f16303f87c8..e2ef9109bab 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -13,7 +13,7 @@ "%d Mitteilung" "%d Mitteilungen" - "Mitteilung" + "Du hast neue Nachrichten." "Eingehender Anruf" "** Fehler beim Senden - bitte Raum öffnen" "Beitreten" @@ -22,7 +22,7 @@ "%d Einladung" "%d Einladungen" - "Du wurdest zu einem Chat eingeladen" + "Sie wurden zu einem Chat eingeladen" "%1$s hat dich zum Chatten eingeladen" "Hat Dich erwähnt: %1$s" "Neue Nachrichten" @@ -37,7 +37,7 @@ "%1$s hat dich eingeladen, dem Chatroom beizutreten" "Ich" "%1$s hat Dich erwähnt oder geantwortet" - "Du siehst dir die Benachrichtigung an! Klicke hier!" + "Sie sehen die Benachrichtigung! Klicken Sie hier!" "%1$s: %2$s" "%1$s: %2$s %3$s" @@ -55,16 +55,17 @@ "Google-Dienste" "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig." "Ermittele den Namen des aktuellen Anbieters." - "Kein Push-Anbieter ausgewählt." - "Aktueller Push-Anbieter:%1$s." - "Aktueller Push-Anbieter" - "Stelle sicher, dass die Anwendung mindestens einen Push-Anbieter hat." - "Keine Push-Anbieter gefunden." + "Kein Pushanbieter ausgewählt." + "Aktueller Pushanbieter: %1$s." + "Aktueller Pushanbieter" + "Stellen Sie sicher, dass die Anwendung mindestens einen Pushanbieter unterstützt." + "Keine Unterstützung für Pushanbieter gefunden." "%1$d Push-Anbieter gefunden: %2$s" "%1$d Push-Anbieter gefunden: %2$s" - "Push-Anbieter erkennen" + "Die Anwendung bietet Unterstützung für: %1$s" + "Unterstützung für Pushanbieter" "Prüfe, ob die Anwendung Benachrichtigungen anzeigen kann." "Die Benachrichtigung wurde nicht angeklickt." "Die Benachrichtigung kann nicht angezeigt werden." diff --git a/libraries/push/impl/src/main/res/values-el/translations.xml b/libraries/push/impl/src/main/res/values-el/translations.xml index ad88f0f3b3e..da21ccfce72 100644 --- a/libraries/push/impl/src/main/res/values-el/translations.xml +++ b/libraries/push/impl/src/main/res/values-el/translations.xml @@ -13,9 +13,9 @@ "%d ειδοποίηση" "%d ειδοποιήσεις" - "Γνωστοποίηση" - "Εισερχόμενη κλήση" - "** Αποτυχία αποστολής - παρακαλώ άνοιξε το δωμάτιο" + "Έχεις νέο(α) μήνυμα(τα)." + "📹 Εισερχόμενη κλήση" + "** Αποτυχία αποστολής - παρακαλώ ανοίξτε την αίθουσα" "Συμμετοχή" "Απόρριψη" @@ -33,8 +33,8 @@ "Αντέδρασε με %1$s" "Επισήμανση ως αναγνωσμένου" "Γρήγορη απάντηση" - "Σέ προσκάλεσε να συμμετάσχεις στο δωμάτιο" - "Ο χρήστης %1$s σε προσκάλεσε να συμμετάσχεις στο δωμάτιο" + "Σας προσκάλεσε να ενταχθείτε στην αίθουσα" + "%1$s σας προσκάλεσε να συμμετάσχετε στην αίθουσα" "Εγώ" "Ο χρήστης %1$s αναφέρθηκε ή απάντησε" "Βλέπεις την ειδοποίηση! Κάνε μου κλικ!" @@ -48,8 +48,8 @@ "%1$s σε %2$s" "%1$s σε %2$s και %3$s" - "%d δωμάτιο" - "%d δωμάτια" + "%d αίθουσα" + "%d αίθουσες" "Συγχρονισμός στο παρασκήνιο" "Υπηρεσίες Google" @@ -64,6 +64,7 @@ "Βρέθηκε %1$d πάροχος push: %2$s" "Βρέθηκαν %1$d πάροχοι push: %2$s" + "Η εφαρμογή δημιουργήθηκε με υποστήριξη για: %1$s" "Εντοπισμός παρόχων push" "Έλεγξε ότι η εφαρμογή μπορεί να εμφανίσει ειδοποίηση." "Δεν έχει γίνει κλικ στην ειδοποίηση." diff --git a/libraries/push/impl/src/main/res/values-es/translations.xml b/libraries/push/impl/src/main/res/values-es/translations.xml index 27533f254c0..4200cb9baf7 100644 --- a/libraries/push/impl/src/main/res/values-es/translations.xml +++ b/libraries/push/impl/src/main/res/values-es/translations.xml @@ -3,6 +3,7 @@ "Llamada" "Esperando eventos" "Notificaciones ruidosas" + "Llamadas entrantes" "Notificaciones silenciosas" "%1$s: %2$d mensaje" @@ -12,14 +13,16 @@ "%d notificación" "%d notificaciones" - "Notificación" + "📹 Llamada entrante" "** No se ha podido enviar - por favor, abre la sala" "Unirse" + "Rechazar" "%d invitación" "%d invitaciones" "Te invitó a chatear" + "%1$s te invitó a chatear" "Te mencionó: %1$s" "Mensajes nuevos" @@ -30,7 +33,9 @@ "Marcar como leído" "Respuesta rápida" "Te invitó a unirte a la sala" + "%1$s te invitó a unirte a la sala" "Yo" + "%1$s mencionó o respondió" "¡Estás viendo la notificación! ¡Haz clic en mí!" "%1$s: %2$s" "%1$s: %2$s %3$s" @@ -48,4 +53,29 @@ "Sincronización en segundo plano" "Servicios de Google" "No se han encontrado Servicios de Google Play válidos. Es posible que las notificaciones no funcionen correctamente." + "Obtener el nombre del proveedor actual." + "No se ha seleccionado ningún proveedor de push." + "Proveedor de push actual: %1$s." + "Proveedor de push actual" + "Asegurarse de que la aplicación tiene al menos un proveedor de push." + "No se encontró ningún proveedor de push." + + "Se encontró %1$d proveedor de push: %2$s" + "Se encontraron %1$d proveedores de push: %2$s" + + "La aplicación se compiló con compatibilidad para: %1$s" + "Detectar proveedores de push" + "Verificar que la aplicación pueda mostrar notificaciones." + "No se ha hecho clic en la notificación." + "No se puede mostrar la notificación." + "¡Se ha hecho clic en la notificación!" + "Mostrar notificación" + "Haz clic en la notificación para continuar la prueba." + "Asegurarse de que la aplicación esté recibiendo notificaciones push." + "Error: el servicio de push ha rechazado la solicitud." + "Error: %1$s." + "Error, no se puede probar el push." + "Error, tiempo de espera agotado para push." + "Envío y recepción de notificación push tomó %1$d ms." + "Probar envío y recepción Push" diff --git a/libraries/push/impl/src/main/res/values-et/translations.xml b/libraries/push/impl/src/main/res/values-et/translations.xml index 6a0c1cf5c05..0484f0008c4 100644 --- a/libraries/push/impl/src/main/res/values-et/translations.xml +++ b/libraries/push/impl/src/main/res/values-et/translations.xml @@ -13,7 +13,7 @@ "%d teavitus" "%d teavitust" - "Teavitus" + "Sulle on uusi sõnumeid." "📹 Sissetulev kõne" "** Saatmine ei õnnestunud - palun ava jututoa täisvaade" "Liitu" @@ -64,6 +64,7 @@ "Leidsime %1$d tõuketeenuse pakkuja: %2$s" "Leidsime %1$d tõuketeenuse pakkujat: %2$s" + "Rakendus on kompileeritud kaasneva toega teenusele: %1$s" "Tuvasta tõuketeenuste pakkujad" "Palun kontrolli, et rakendus saaks kuvada teavitusi." "Sa pole teavitust klõpsinud." diff --git a/libraries/push/impl/src/main/res/values-eu/translations.xml b/libraries/push/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..f548b40c343 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,74 @@ + + + "Deia" + "Gertaerei adi" + "Jakinarazpen zaratatsuak" + "Jakinarazpen isilak" + + "%1$s: mezu %2$d" + "%1$s: %2$d mezu" + + + "jakinarazpen %d" + "%d jakinarazpen" + + "Deia jasotzen" + "** Huts egin du bidalketak - ireki gela" + "Elkartu" + "Baztertu" + + "Gonbidapen %d" + "%d gonbidapen" + + "Txatetzera gonbidatu zaitu" + "%1$s(e)k txatera gonbidatu zaitu" + "Aipatu zaitu: %1$s" + "Mezu berriak" + + "Mezu berri %d" + "%d mezu berri" + + "%1$s (r)ekin erreakzionatu du" + "Markatu irakurritzat" + "Erantzun azkarra" + "Gelan sartzera gonbidatu zaitu" + "%1$s(e)k gelan sartzera gonbidatu zaitu" + "Neu" + "%1$s(e)k aipatu zaitu edo erantzun dizu" + "Jakinarazpena ikusten ari zara! Klikatu!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "Irakurri gabeko mezu %den jakinarazpena" + "Irakurri gabeko %d mezuren jakinarazpena" + + "%1$s eta %2$s" + "%1$s %2$s gelan" + "%1$s %2$s gelan eta %3$s" + + "Gela %d" + "%d gela" + + "Atzeko planoko sinkronizazioa" + "Google Services" + "Ez da baliozko Google Play Servicerik aurkitu. Litekeena da jakinarazpenak behar bezala ez ibiltzea." + "Lortu uneko hornitzailearen izena." + "Ez da push hornitzailerik hautatu." + "Uneko push hornitzailea: %1$s." + "Uneko push hornitzailea" + "Ez da push hornitzailerik aurkitu." + + "Push hornitzaile %1$d aurkitu da: %2$s" + "%1$d push hornitzaile aurkitu dira: %2$s" + + "Detektatu push hornitzaileak" + "Egiaztatu aplikazioak jakinarazpena bistaratu dezakeela." + "Ez da klikik egin jakinarazpenean." + "Ezin da jakinarazpena bistaratu." + "Klik egin da jakinarazpenean!" + "Bistaratu jakinarazpena" + "Klikatu jakinarazpenean probarekin jarraitzeko." + "Errorea: %1$s." + "Errorea, ezin da push-a probatu." + "Errorea, denbora agortu da push-aren zain." + diff --git a/libraries/push/impl/src/main/res/values-fa/translations.xml b/libraries/push/impl/src/main/res/values-fa/translations.xml index a4bd043071b..972ee549a28 100644 --- a/libraries/push/impl/src/main/res/values-fa/translations.xml +++ b/libraries/push/impl/src/main/res/values-fa/translations.xml @@ -5,15 +5,31 @@ "اعلان‌های پرصدا" "زنگ خوردن تماس" "اعلان‌های صامت" + + "%1$s:%2$d پیام" + "%1$s:%2$d پیام‌" + + + "%dاعلان" + "%dاعلان‌" + "آگاهی" - "تماس ورودی" + "📹 تماس ورودی" "**‌شکست در فرستادن - لطفاً اتاق را بگشایید" "پیوستن" "رد کردن" + + "%d دعوت" + "%d دعوت" + "به گپ دعوتتان کرد" "%1$s به گپ دعوتتان کرد" "به شما اشاره کرد: %1$s" "پیام جدید" + + "%d پیام جدید" + "%d پیام جدید" + "با %1$s واکنش داد" "علامت‌گذاری به عنوان خوانده شده" "پاسخ سریع" @@ -24,9 +40,17 @@ "دارید آگاهی را مشاهده می‌کنید! کلیک کنید!" "%1$s: %2$s" "%1$s: %2$s %3$s" + + "%d پیام اعلان نشده" + "%d پیام اعلان نشده" + "%1$s و %2$s" "%1$s در %2$s" "%1$s در %2$s و %3$s" + + "%d اتاق" + "%d اتاق" + "همگام سازی پس‌زمینه" "خدمات گوگل" "خدمت پلی گوگل معتبری پیدا نشد. ممکن است آگاهی‌ها به درستی کار نکنند." diff --git a/libraries/push/impl/src/main/res/values-fi/translations.xml b/libraries/push/impl/src/main/res/values-fi/translations.xml index 61c0890fd3c..95e1b340b14 100644 --- a/libraries/push/impl/src/main/res/values-fi/translations.xml +++ b/libraries/push/impl/src/main/res/values-fi/translations.xml @@ -13,8 +13,7 @@ "%d ilmoitus" "%d ilmoitusta" - "Ilmoitus" - "Saapuva puhelu" + "📹 Saapuva puhelu" "** Lähetys epäonnistui - avaa huone" "Liity" "Hylkää" @@ -64,6 +63,7 @@ "Löytyi %1$d push-palveluntarjoaja: %2$s" "Löytyi %1$d push-palveluntarjoajaa: %2$s" + "Sovellus on rakennettu tukemaan: %1$s" "Push-palveluntarjoajien havaitseminen" "Tarkistaa, että sovellus voi näyttää ilmoituksen." "Ilmoitusta ei ole klikattu." diff --git a/libraries/push/impl/src/main/res/values-fr/translations.xml b/libraries/push/impl/src/main/res/values-fr/translations.xml index eb386ed459f..0069c1e5a78 100644 --- a/libraries/push/impl/src/main/res/values-fr/translations.xml +++ b/libraries/push/impl/src/main/res/values-fr/translations.xml @@ -13,8 +13,8 @@ "%d notification" "%d notifications" - "Notification" - "Appel entrant" + "Vous avez de nouveau(x) message(s)." + "📹 Appel entrant" "** Échec de l’envoi - veuillez ouvrir le salon" "Rejoindre" "Rejeter" @@ -64,6 +64,7 @@ "%1$d fournisseur de Push détecté : %2$s" "%1$d fournisseurs de Push détectés : %2$s" + "L’application a été compilée avec la prise en charge de : %1$s" "Détecter les fournisseurs de Push" "Vérifier que l’application peut afficher des notifications." "Vous n’avez pas cliqué sur la notification." diff --git a/libraries/push/impl/src/main/res/values-hu/translations.xml b/libraries/push/impl/src/main/res/values-hu/translations.xml index 6d478516f69..c0aad928d4d 100644 --- a/libraries/push/impl/src/main/res/values-hu/translations.xml +++ b/libraries/push/impl/src/main/res/values-hu/translations.xml @@ -64,6 +64,7 @@ "%1$d leküldéses szolgáltató találva: %2$s" "%1$d leküldéses szolgáltató találva: %2$s" + "Az alkalmazás úgy lett összeállítva, hogy támogatja a következőket: %1$s" "Leküldéses értesítési szolgáltatók észlelése" "Ellenőrizze, hogy az alkalmazás képes-e megjeleníteni az értesítést." "Az értesítésre nem kattintottak rá." diff --git a/libraries/push/impl/src/main/res/values-in/translations.xml b/libraries/push/impl/src/main/res/values-in/translations.xml index 4c12681fbec..760d507e08e 100644 --- a/libraries/push/impl/src/main/res/values-in/translations.xml +++ b/libraries/push/impl/src/main/res/values-in/translations.xml @@ -11,8 +11,8 @@ "%d pemberitahuan" - "Notifikasi" - "Panggilan masuk" + "Anda memiliki pesan baru." + "📹 Panggilan masuk" "** Gagal mengirim — silakan buka ruangan" "Gabung" "Tolak" @@ -57,6 +57,7 @@ "Ditemukan %1$d penyedia notifikasi dorongan: %2$s" + "Aplikasi ini dibangun dengan dukungan untuk: %1$s" "Deteksi penyedia notifikasi dorongan" "Periksa apakah aplikasi dapat menampilkan notifikasi." "Notifikasi belum diklik." diff --git a/libraries/push/impl/src/main/res/values-it/translations.xml b/libraries/push/impl/src/main/res/values-it/translations.xml index a5ad1a76b3a..919d8924408 100644 --- a/libraries/push/impl/src/main/res/values-it/translations.xml +++ b/libraries/push/impl/src/main/res/values-it/translations.xml @@ -13,7 +13,7 @@ "%d notifica" "%d notifiche" - "Notifica" + "Hai nuovi messaggi." "📹 Chiamata in arrivo" "** Invio fallito - si prega di aprire la stanza" "Entra" @@ -64,6 +64,7 @@ "Provider %1$d push trovato: %2$s" "Provider %1$d push trovati: %2$s" + "L\'applicazione è stata creata con il supporto per: %1$s" "Rileva i provider di servizi push" "Verifica che l\'applicazione possa mostrare una notifica." "La notifica non è stata cliccata." diff --git a/libraries/push/impl/src/main/res/values-ka/translations.xml b/libraries/push/impl/src/main/res/values-ka/translations.xml index 8296075572c..3a0d6260168 100644 --- a/libraries/push/impl/src/main/res/values-ka/translations.xml +++ b/libraries/push/impl/src/main/res/values-ka/translations.xml @@ -12,7 +12,6 @@ "%d შეტყობინება" "%d შეტყობინება" - "შეტყობინება" "** გაგზავნა ვერ მოხერხდა - გთხოვთ, გახსნათ ოთახი" "გაწევრიანება" @@ -27,6 +26,7 @@ "%d ახალი მესიჯი" "რეაგირება მოხდა: %1$s" + "წაკითხულად მონიშვნა" "Სწრაფი პასუხი" "მოგიწვიათ ოთახში" "მე" @@ -47,4 +47,28 @@ "ფონის სინქრონიზაცია" "Google სერვისები" "მოქმედი Google Play სერვისები ვერ მოიძებნა. შეტყობინებები შეიძლება ვერ იმუშაოს სწორად." + "მიმდინარე პროვაიდერის სახელის გაგება" + "push პროვაიდერები არაა არჩეული." + "მიმდინარე push პროვაიდერი: %1$s." + "მიმდინარე push პროვაიდერი" + "დარწმუნდით რომ აპლიკაციას მინიმუმ ერთი push პროვაიდერი." + "push პროვაიდერები ვერ მოიძებნა." + + "მოიძებნა %1$d push პროვაიდერი: %2$s" + "მოიძებნა %1$d push პროვაიდერი: %2$s" + + "push პროვაიდერების აღმოჩენა" + "შეამოწმეთ აპლიკაციას თუ შეუძლია შეტყობინებების ჩვენება" + "შეტყობინება არ იქნა დაჭერილი." + "შეტყობინების ჩვენება შეუძლებელია." + "შეტყობინება იყო დაჭერილი!" + "შეტყობინებების ჩვენება" + "გთხოვთ დააჭიროთ შეტყობინებაზე ტესტის გასაგრძელებლად." + "დარწმუნდით რომ აპლიკაცია იღებს push-შეტყობინებას." + "შეცდომა: გამგზავნმა უარყო მოთხოვნა." + "შეცდომა: %1$s." + "შეცდომა, push-ის ტესტირება შეუძლებელია." + "შეცდომა, push-ისთვის ლოდინის დრო გავიდა." + "Push-შეტყობინების ციკლმა დაიკავა %1$d ms." + "push-შეტყობინების ციკლის ტესტირება" diff --git a/libraries/push/impl/src/main/res/values-lt/translations.xml b/libraries/push/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..b894a9cf999 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,51 @@ + + + "Skambinti" + "Klausomasi įvykių" + "Triukšmingi pranešimai" + "Tylūs pranešimai" + + "%1$s: %2$d žinutė" + "%1$s: %2$d žinutės" + "%1$s: %2$d žinučių" + + + "%d pranešimas" + "%d pranešimai" + "%d pranešimų" + + "** Nepavyko išsiųsti - prašome atidaryti kambarį" + + "%d kvietimas" + "%d kvietimai" + "%d kvietimų" + + "Pakvietė jus bendrauti" + "Naujos žinutės" + + "%d nauja žinutė" + "%d naujos žinutės" + "%d naujų žinučių" + + "Sparčiai atsakyti" + "Aš" + "Jūs žiūrite pranešimą! Spustelėkite mane!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "%d neperskaityta žinutė" + "%d neperskaitytos žinutės" + "%d neperskaitytų žinučių" + + "%1$s ir %2$s" + "%1$s (%2$s)" + "%1$s (%2$s) ir %3$s" + + "%d kambaryje" + "%d kambariuose" + "%d kambarių" + + "Sinchronizavimas fone" + "Google Services" + "Nerasta veikiančių \"Google Play Services\". Pranešimai gali veikti netinkamai." + diff --git a/libraries/push/impl/src/main/res/values-nb/translations.xml b/libraries/push/impl/src/main/res/values-nb/translations.xml index 7e4eacb48b4..1be74244aa3 100644 --- a/libraries/push/impl/src/main/res/values-nb/translations.xml +++ b/libraries/push/impl/src/main/res/values-nb/translations.xml @@ -1,7 +1,9 @@ "Anrop" + "Lytter etter hendelser" "Støyende varsler" + "Ringende anrop" "Stille varsler" "%1$s: %2$d melding" @@ -11,8 +13,8 @@ "%d varsel" "%d varsler" - "Varsel" - "Innkommende anrop" + "Du har nye meldinger." + "📹 Innkommende anrop" "** Kunne ikke sende - vennligst åpne rommet" "Bli med" "Avvis" @@ -22,11 +24,13 @@ "Inviterte deg til å chatte" "%1$s inviterte deg til å chatte" + "Nevnte deg: %1$s" "Nye meldinger" "%d ny melding" "%d nye meldinger" + "Reagerte med %1$s" "Marker som lest" "Raskt svar" "Invitert deg til å bli med i rommet" @@ -50,9 +54,29 @@ "Bakgrunnssynkronisering" "Google Services" "Ingen gyldige Google Play-tjenester funnet. Det kan hende at varsler ikke fungerer som de skal." + "Få navnet på den nåværende tilbyderen." "Ingen push-leverandører er valgt." "Gjeldende push-leverandør: %1$s." "Nåværende push-leverandør" + "Påse at applikasjonen har minst én push-leverandør." "Ingen push-leverandører funnet." + + "Fant %1$d push-leverandør: %2$s" + "Fant %1$d push-leverandører: %2$s" + + "Applikasjonen ble bygget med støtte for: %1$s" + "Oppdag push-leverandører" + "Kontroller at programmet kan vise varsler." + "Det er ikke klikket på varselet." + "Kan ikke vise varselet." + "Varselet har blitt klikket på!" + "Vis varsel" + "Klikk på varselet for å fortsette testen." + "Kontroller at applikasjonen mottar push." + "Feil: pusheren har avvist forespørselen." "Feil: %1$s." + "Feil, kan ikke teste push." + "Feil, tidsavbrudd i påvente av push." + "Push loop back tok %1$d ms." + "Test Push loop back" diff --git a/libraries/push/impl/src/main/res/values-nl/translations.xml b/libraries/push/impl/src/main/res/values-nl/translations.xml index 6954296e4ac..833c6e7107c 100644 --- a/libraries/push/impl/src/main/res/values-nl/translations.xml +++ b/libraries/push/impl/src/main/res/values-nl/translations.xml @@ -13,8 +13,7 @@ "%d melding" "%d meldingen" - "Melding" - "Inkomende oproep" + "📹 Inkomende oproep" "** Verzenden is mislukt - open de kamer" "Deelnemen" "Weiger" 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 1cd641e6f21..818cd3edc49 100644 --- a/libraries/push/impl/src/main/res/values-pl/translations.xml +++ b/libraries/push/impl/src/main/res/values-pl/translations.xml @@ -15,8 +15,7 @@ "%d powiadomienia" "%d powiadomień" - "Powiadomienie" - "Przychodzące połączenie" + "📹 Połączenie przychodzące" "** Nie udało się wysłać - proszę otworzyć pokój" "Dołącz" "Odrzuć" @@ -71,6 +70,7 @@ "Znaleziono %1$d dostawców push: %2$s" "Znaleziono %1$d dostawców push: %2$s" + "Aplikacja została zbudowana ze wsparciem dla: %1$s" "Wykryj dostawców powiadomień push" "Sprawdź, czy aplikacja może wyświetlać powiadomienie." "Powiadomienie nie zostało kliknięte." diff --git a/libraries/push/impl/src/main/res/values-pt-rBR/translations.xml b/libraries/push/impl/src/main/res/values-pt-rBR/translations.xml index b44e78a0c30..fb577b0dd4e 100644 --- a/libraries/push/impl/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/push/impl/src/main/res/values-pt-rBR/translations.xml @@ -3,6 +3,7 @@ "Chamada" "Ouvindo eventos" "Notificações barulhentas" + "Chamadas tocando" "Notificações silenciosas" "%1$s: %2$d mensagem" @@ -12,7 +13,8 @@ "%d notificação" "%d notificações" - "Notificação" + "Você tem mensagens novas." + "📹 Chamada recebida" "** Falha ao enviar - por favor, abra a sala" "Entrar" "Recusar" @@ -34,6 +36,7 @@ "Convidou você para entrar na sala" "%1$s te convidou para entrar na sala" "Eu" + "%1$s mencionado ou respondido" "Você está visualizando a notificação! Clique em mim!" "%1$s: %2$s" "%1$s: %2$s %3$s" @@ -51,5 +54,29 @@ "Sincronização em segundo plano" "Serviços do Google" "O Google Play Services não foi encontrado. As notificações podem não funcionar corretamente." + "Obtenha o nome do provedor atual." + "Nenhum provedor de push foi selecionado." + "Provedor de push atual: %1$s." + "Provedor de push atual" + "Certifique-se de que o aplicativo seja compatível com pelo menos um provedor de push." + "Nenhum suporte de provedor de push foi encontrado." + + "Encontrado %1$d provedor de push: %2$s" + "Encontrados %1$d provedores de push: %2$s" + + "O aplicativo foi desenvolvido com suporte para: %1$s" + "Suporte ao provedor de push" + "Verifique se o aplicativo pode exibir a notificação." + "A notificação não foi clicada." + "Não é possível exibir a notificação." + "A notificação foi clicada!" + "Exibir notificação" + "Clique na notificação para continuar o teste." + "Certifique-se de que o aplicativo está recebendo push." + "Erro: o pusher rejeitou a solicitação." "Erro: %1$s." + "Erro, não é possível testar o push." + "Erro, tempo limite de espera pelo push." + "O push loop back levou %1$d ms." + "Teste o loopback do push" diff --git a/libraries/push/impl/src/main/res/values-pt/translations.xml b/libraries/push/impl/src/main/res/values-pt/translations.xml index 49fa8271313..0a55a1dd7b9 100644 --- a/libraries/push/impl/src/main/res/values-pt/translations.xml +++ b/libraries/push/impl/src/main/res/values-pt/translations.xml @@ -13,8 +13,8 @@ "%d notificação" "%d notificações" - "Notificação" - "Chamada recebida" + "Tens novas mensagens." + "📹 A receber chamada" "** Falha no envio - por favor abre a sala" "Entrar" "Rejeitar" @@ -64,6 +64,7 @@ "%1$d fornecedor de envio encontrado: %2$s" "%1$d fornecedores de envio encontrados: %2$s" + "A aplicação suporta: %1$s" "Detetar fornecedores de envio" "Verificar se a aplicação consegue mostrar notificações." "Não clicaste na notificação." 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 48b1b19dff2..14240a8d592 100644 --- a/libraries/push/impl/src/main/res/values-ro/translations.xml +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -13,7 +13,6 @@ "%d notificare" "%d notificări" - "Notificare" "Apel primit" "** Trimiterea eșuată - vă rugăm să deschideți camera" "Alăturați-vă" diff --git a/libraries/push/impl/src/main/res/values-ru/translations.xml b/libraries/push/impl/src/main/res/values-ru/translations.xml index 0387c42f2d8..7345664ce0e 100644 --- a/libraries/push/impl/src/main/res/values-ru/translations.xml +++ b/libraries/push/impl/src/main/res/values-ru/translations.xml @@ -15,8 +15,7 @@ "%d уведомления" "%d уведомлений" - "Уведомление" - "Входящий вызов" + "📹 Входящий вызов" "** Не удалось отправить - пожалуйста, откройте комнату" "Присоединиться" "Отклонить" @@ -71,6 +70,7 @@ "Найдено %1$d push-провайдеров: %2$s" "Найдено %1$d push-провайдеров: %2$s" + "Приложение было создано с поддержкой: %1$s" "Обнаружение поставщиков push-уведомлений" "Убедитесь, что приложение может отображать уведомление." "Уведомление не было нажато." diff --git a/libraries/push/impl/src/main/res/values-sk/translations.xml b/libraries/push/impl/src/main/res/values-sk/translations.xml index 5c1b2febf48..74ba29d78ad 100644 --- a/libraries/push/impl/src/main/res/values-sk/translations.xml +++ b/libraries/push/impl/src/main/res/values-sk/translations.xml @@ -15,7 +15,7 @@ "%d oznámenia" "%d oznámení" - "Oznámenie" + "Máte nové správy." "📹 Prichádzajúci hovor" "** Nepodarilo sa odoslať - prosím otvorte miestnosť" "Pripojiť sa" @@ -71,6 +71,7 @@ "Nájdení %1$d poskytovatelia služby push: %2$s" "Nájdených %1$d poskytovateľov služby push: %2$s" + "Aplikácia bola vytvorená s podporou pre: %1$s" "Zistiť poskytovateľov push" "Skontrolujte, či aplikácia dokáže zobraziť upozornenie." "Na oznámenie nebolo kliknuté." diff --git a/libraries/push/impl/src/main/res/values-sv/translations.xml b/libraries/push/impl/src/main/res/values-sv/translations.xml index d9c096af3d9..fd3739915f9 100644 --- a/libraries/push/impl/src/main/res/values-sv/translations.xml +++ b/libraries/push/impl/src/main/res/values-sv/translations.xml @@ -13,8 +13,7 @@ "%d avisering" "%d aviseringar" - "notis" - "Inkommande samtal" + "📹 Inkommande samtal" "** Misslyckades att skicka - vänligen öppna rummet" "Gå med" "Avvisa" @@ -64,6 +63,7 @@ "Hittade %1$d push-leverantör: %2$s" "Hittade %1$d push-leverantörer: %2$s" + "Applikationen byggdes med stöd för: %1$s" "Upptäck push-leverantörer" "Kontrollera att applikationen kan visa avisering." "Aviseringen har inte klickats på." diff --git a/libraries/push/impl/src/main/res/values-tr/translations.xml b/libraries/push/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..183950f3b9b --- /dev/null +++ b/libraries/push/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,80 @@ + + + "Çağrı" + "Etkinlikleri dinlemek" + "Gürültülü bildirimler" + "Çalan aramalar" + "Sessiz bildirimler" + + "%1$s: %2$d mesaj" + "%1$s: %2$d mesaj" + + + "%d bildirim" + "%d bildirim" + + "📹 Gelen çağrı" + "** Gönderilemedi - lütfen odayı açın" + "Katıl" + "Reddet" + + "%d davet" + "%d davet" + + "Sizi sohbete davet etti" + "%1$s sizi sohbete davet etti" + "Senden bahsettim: %1$s" + "Yeni Mesajlar" + + "%d yeni mesaj" + "%d yeni mesaj" + + "%1$s ile tepki verildi" + "Okundu olarak işaretle" + "Hızlı cevap" + "Sizi odaya katılmaya davet etti" + "%1$s sizi odaya katılmaya davet etti" + "Ben" + "%1$s belirtildi veya yanıtlandı" + "Bildirimi görüntülüyorsunuz! Beni tıklayın!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "%d okunmamış mesaj bildirimi" + "%d okunmamış mesaj bildirimi" + + "%1$s ve %2$s" + "%1$s içinde %2$s" + "%1$s içinde %2$s ve %3$s" + + "%d oda" + "%d oda" + + "Arkaplan senkronizasyonu" + "Google Hizmetleri" + "Geçerli bir Google Play Hizmeti bulunamadı. Bildirimler düzgün çalışmayabilir." + "Geçerli sağlayıcının adını al." + "Hiçbir gönderme sağlayıcısı seçilmedi." + "Geçerli gönderme sağlayıcısı: %1$s." + "Geçerli gönderme sağlayıcısı" + "Uygulamanın en az bir gönderme sağlayıcısına sahip olduğundan emin olun." + "Gönderim sağlayıcısı bulunamadı." + + "%1$d gönderme sağlayıcısı bulundu: %2$s" + "%1$d gönderme sağlayıcısı bulundu: %2$s" + + "Gönderme sağlayıcılarını tespit et" + "Uygulamanın bildirim görüntüleyebildiğini kontrol edin." + "Bildirime tıklanmadı." + "Bildirim görüntülenemiyor." + "Bildirime tıklandı!" + "Bildirimi görüntüle" + "Teste devam etmek için lütfen bildirime tıklayın." + "Uygulamanın push aldığından emin olun." + "Hata: itici isteği reddetti." + "Hata: %1$s." + "Hata, itme test edilemiyor." + "Hata, push beklenirken zaman aşımı oluştu." + "Geri itme döngüsü %1$d ms sürdü." + "Test Geri İtme döngüsü" + diff --git a/libraries/push/impl/src/main/res/values-uk/translations.xml b/libraries/push/impl/src/main/res/values-uk/translations.xml index 71e8f14d613..1aa535b1e27 100644 --- a/libraries/push/impl/src/main/res/values-uk/translations.xml +++ b/libraries/push/impl/src/main/res/values-uk/translations.xml @@ -15,8 +15,8 @@ "%d сповіщення" "%d сповіщень" - "Сповіщення" - "Вхідний виклик" + "У вас є нові повідомлення." + "📹 Вхідний виклик" "** Не вдалося надіслати - відкрийте кімнату" "Доєднатися" "Відхилити" @@ -71,6 +71,7 @@ "Виявлено %1$d постачальники: %2$s" "Виявлено %1$d постачальників: %2$s" + "Застосунок створено за підтримки: %1$s" "Виявлення постачальників push-сповіщень" "Перевірте, чи може застосунок показувати сповіщення." "Ви не натиснули на сповіщення." diff --git a/libraries/push/impl/src/main/res/values-ur/translations.xml b/libraries/push/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..adcf03099b9 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,77 @@ + + + "مکالمہ" + "واقعات کیلئے سن رہا ہے" + "صاخب اطلاعات" + "بجنے والے مکالمات" + "خاموش اطلاعات" + + "%1$s: %2$d پیغام" + "%1$s: %2$d پیغامات" + + + "%d اطلاع" + "%d اطلاعات" + + "📹 آنے والا مکالمہ" + "** بھیجنے میں ناکام - براہ کرم کمرہ کھولیں" + "شامل ہوں" + "مسترد کریں" + + "%d دعوت نامہ" + "%d دعوت نامے" + + "آپ کو گفتگو کیلئے مدعو کیا" + "آپ کا تذکرہ کیا: %1$s" + "نئے پیغامات" + + "%d نیا پیغام" + "%d نئے پیغامات" + + "%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 کمرے" + + "پس منظر مطابقت پذیری" + "گوگل سروسز" + "کوئی درست گوگل پلے سروسز نہیں ملی۔ ہو سکتا ہے اطلاعات ٹھیک سے کام نہ کریں۔" + "موجودہ فراہم کنندہ کا نام حاصل کریں۔" + "کوئی دھکا فراہم کنندہ منتخب نہیں کیا گیا" + "موجودہ دھکا فراہم کنندہ: %1$s۔" + "موجودہ دھکا فراہم کنندہ" + "یقینی بنائیں کہ اطلاقیہ کم از کم ایک دھکا فراہم کنندہ کی حمایت کرتا ہے۔" + "کوئی دھکا فراہم کنندہ حمایت نہیں ملی۔" + + "پایا %1$d دھکا فراہم کنندہ: %2$s" + "پائے %1$d دھکا فراہم کنندگان: %2$s" + + "دھکا فراہم کنندہ حمایت" + "پڑتال کریں کہ اطلاقیہ اطلاع دکھا سکتا ہے" + "یہ اطلاع نہیں دبائی گئی" + "اطلاع ظاہر نہیں کرسکتا" + "یہ اطلاع دبا دی گئی ہے!" + "اطلاع دکھائیں" + "برائے مہربانی جانچ جاری رکھنے کیلئے اطلاع پر دبائیں" + "اس بات کو یقینی بنائیں کہ اطلاقیے کو دھکا موصول ہو رہا ہے۔" + "نقص: دھکا کنندہ نے درخواست مسترد کردی ہے۔" + "نقص: %1$s۔" + "نقص، دھکا جانچ نہیں سکتا" + "نقص، دھکے کے انتظار میں نفذ الوقت" + "دھکئی حلقۂ رجوعیہ نے %1$d مث لیا" + "دھکئی حلقۂ رجوعیہ جانچیں" + 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 badfd6d421c..486b668b3e0 100644 --- a/libraries/push/impl/src/main/res/values-uz/translations.xml +++ b/libraries/push/impl/src/main/res/values-uz/translations.xml @@ -12,7 +12,6 @@ "%dbildirishnoma" "%dbildirishnomalar" - "Bildirishnoma" "** Yuborilmadi - iltimos, xonani oching" "Qo\'shilish" 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 90717e73a13..61178f61127 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 @@ -11,8 +11,7 @@ "%d 個通知" - "通知" - "來電" + "📹 來電" "** 無法傳送,請開啟聊天室" "加入" "拒絕" @@ -57,6 +56,7 @@ "找到 %1$d 個推播提供者:%2$s" + "該應用程式支援以下功能:%1$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 d4e904ca67c..94d156396af 100644 --- a/libraries/push/impl/src/main/res/values-zh/translations.xml +++ b/libraries/push/impl/src/main/res/values-zh/translations.xml @@ -11,8 +11,7 @@ "%d 条通知" - "通知" - "来电" + "📹 来电" "** 无法发送——请打开聊天室" "加入" "拒绝" diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index 5c6c852f145..069183a3a25 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -13,7 +13,7 @@ "%d notification" "%d notifications" - "Notification" + "You have new messages." "📹 Incoming call" "** Failed to send - please open room" "Join" @@ -58,13 +58,14 @@ "No push providers selected." "Current push provider: %1$s." "Current push provider" - "Ensure that the application has at least one push provider." - "No push providers found." + "Ensure that the application supports at least one push provider." + "No push provider support found." "Found %1$d push provider: %2$s" "Found %1$d push providers: %2$s" - "Detect push providers" + "The application was built with support for: %1$s" + "Push provider support" "Check that the application can display notification." "The notification has not been clicked." "Cannot display the notification." diff --git a/libraries/push/impl/src/main/sqldelight/io/element/android/libraries/push/impl/db/PushHistory.sq b/libraries/push/impl/src/main/sqldelight/io/element/android/libraries/push/impl/db/PushHistory.sq new file mode 100644 index 00000000000..7a355baeb5d --- /dev/null +++ b/libraries/push/impl/src/main/sqldelight/io/element/android/libraries/push/impl/db/PushHistory.sq @@ -0,0 +1,22 @@ +CREATE TABLE PushHistory ( + pushDate INTEGER NOT NULL, + providerInfo TEXT NOT NULL, + eventId TEXT, + roomId TEXT, + sessionId TEXT, + hasBeenResolved INTEGER NOT NULL, + comment TEXT +); + +selectAll: +SELECT * FROM PushHistory ORDER BY pushDate DESC; + +insertPushHistory: +INSERT INTO PushHistory VALUES ?; + +removeAll: +DELETE FROM PushHistory; + +-- add query to keep only the last x entries +removeOldest: +DELETE FROM PushHistory WHERE rowid NOT IN (SELECT rowid FROM PushHistory ORDER BY pushDate DESC LIMIT ?); diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt index 8c76ed3259c..5ae8ab261e4 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt @@ -7,13 +7,21 @@ package io.element.android.libraries.push.impl +import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId +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_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.push.api.GetCurrentPushProvider +import io.element.android.libraries.push.api.history.PushHistoryItem +import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore +import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore +import io.element.android.libraries.push.impl.store.InMemoryPushDataStore +import io.element.android.libraries.push.impl.store.PushDataStore import io.element.android.libraries.push.impl.test.FakeTestPush import io.element.android.libraries.push.impl.test.TestPush import io.element.android.libraries.push.test.FakeGetCurrentPushProvider @@ -281,6 +289,53 @@ class DefaultPushServiceTest { assertThat(userPushStore.getPushProviderName()).isEqualTo(aPushProvider.name) } + @Test + fun `resetBatteryOptimizationState invokes the store method`() = runTest { + val resetResult = lambdaRecorder { } + val defaultPushService = createDefaultPushService( + mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore( + resetResult = resetResult, + ), + ) + defaultPushService.resetBatteryOptimizationState() + resetResult.assertions().isCalledOnce() + } + + @Test + fun `resetPushHistory invokes the store method`() = runTest { + val resetResult = lambdaRecorder { } + val defaultPushService = createDefaultPushService( + pushDataStore = InMemoryPushDataStore( + resetResult = resetResult + ), + ) + defaultPushService.resetPushHistory() + resetResult.assertions().isCalledOnce() + } + + @Test + fun `getPushHistoryItemsFlow invokes the store method`() = runTest { + val store = InMemoryPushDataStore() + val aPushHistoryItem = PushHistoryItem( + pushDate = 0L, + formattedDate = "formattedDate", + providerInfo = "providerInfo", + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + sessionId = A_SESSION_ID, + hasBeenResolved = false, + comment = null, + ) + val defaultPushService = createDefaultPushService( + pushDataStore = store, + ) + defaultPushService.getPushHistoryItemsFlow().test { + assertThat(awaitItem().isEmpty()).isTrue() + store.emitPushHistoryItems(listOf(aPushHistoryItem)) + assertThat(awaitItem().first()).isEqualTo(aPushHistoryItem) + } + } + private fun createDefaultPushService( testPush: TestPush = FakeTestPush(), userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(), @@ -288,6 +343,8 @@ class DefaultPushServiceTest { getCurrentPushProvider: GetCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = null), sessionObserver: SessionObserver = NoOpSessionObserver(), pushClientSecretStore: PushClientSecretStore = InMemoryPushClientSecretStore(), + pushDataStore: PushDataStore = InMemoryPushDataStore(), + mutableBatteryOptimizationStore: MutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(), ): DefaultPushService { return DefaultPushService( testPush = testPush, @@ -296,6 +353,8 @@ class DefaultPushServiceTest { getCurrentPushProvider = getCurrentPushProvider, sessionObserver = sessionObserver, pushClientSecretStore = pushClientSecretStore, + pushDataStore = pushDataStore, + mutableBatteryOptimizationStore = mutableBatteryOptimizationStore, ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt new file mode 100644 index 00000000000..e3a28550ca6 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt @@ -0,0 +1,113 @@ +/* + * 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.push.impl.battery + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.provider.Settings +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher +import io.element.android.services.toolbox.test.intent.FakeExternalIntentLauncher +import io.element.android.tests.testutils.lambda.lambdaRecorder +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AndroidBatteryOptimizationTest { + @Test + fun `isIgnoringBatteryOptimizations should return false`() { + val sut = createAndroidBatteryOptimization() + assertThat(sut.isIgnoringBatteryOptimizations()).isFalse() + } + + @Test + fun `requestDisablingBatteryOptimization is called once with expected intent`() { + val launchLambda = lambdaRecorder { intent -> + assertThat(intent.action).isEqualTo(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + assertThat(intent.data.toString()).isEqualTo("package:${InstrumentationRegistry.getInstrumentation().context.packageName}") + } + val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda) + val sut = createAndroidBatteryOptimization( + externalIntentLauncher = externalIntentLauncher, + ) + val result = sut.requestDisablingBatteryOptimization() + launchLambda.assertions().isCalledOnce() + assertThat(result).isTrue() + } + + @Test + fun `in case of 1 error, requestDisablingBatteryOptimization returns true`() { + var callNumber = 0 + val launchLambda = lambdaRecorder { intent -> + callNumber++ + when (callNumber) { + 1 -> { + assertThat(intent.action).isEqualTo(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + assertThat(intent.data.toString()).isEqualTo("package:${InstrumentationRegistry.getInstrumentation().context.packageName}") + throw ActivityNotFoundException("Test exception") + } + 2 -> { + assertThat(intent.action).isEqualTo(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + assertThat(intent.data).isNull() + // No error + } + else -> { + throw AssertionError("Unexpected call number: $callNumber") + } + } + } + val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda) + val sut = createAndroidBatteryOptimization( + externalIntentLauncher = externalIntentLauncher, + ) + val result = sut.requestDisablingBatteryOptimization() + launchLambda.assertions().isCalledExactly(2) + assertThat(result).isTrue() + } + + @Test + fun `in case of 2 errors, requestDisablingBatteryOptimization returns false`() { + var callNumber = 0 + val launchLambda = lambdaRecorder { intent -> + callNumber++ + when (callNumber) { + 1 -> { + assertThat(intent.action).isEqualTo(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + assertThat(intent.data.toString()).isEqualTo("package:${InstrumentationRegistry.getInstrumentation().context.packageName}") + throw ActivityNotFoundException("Test exception") + } + 2 -> { + assertThat(intent.action).isEqualTo(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + assertThat(intent.data).isNull() + throw ActivityNotFoundException("Test exception") + } + else -> { + throw AssertionError("Unexpected call number: $callNumber") + } + } + } + val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda) + val sut = createAndroidBatteryOptimization( + externalIntentLauncher = externalIntentLauncher, + ) + val result = sut.requestDisablingBatteryOptimization() + launchLambda.assertions().isCalledExactly(2) + assertThat(result).isFalse() + } + + private fun createAndroidBatteryOptimization( + externalIntentLauncher: ExternalIntentLauncher = FakeExternalIntentLauncher(), + ): AndroidBatteryOptimization { + return AndroidBatteryOptimization( + context = InstrumentationRegistry.getInstrumentation().context, + externalIntentLauncher = externalIntentLauncher, + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt new file mode 100644 index 00000000000..d5dd7c48d45 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt @@ -0,0 +1,170 @@ +/* + * 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.push.impl.battery + +import androidx.lifecycle.Lifecycle +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.push.api.battery.BatteryOptimizationEvents +import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore +import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore +import io.element.android.libraries.push.impl.store.InMemoryPushDataStore +import io.element.android.libraries.push.impl.store.PushDataStore +import io.element.android.tests.testutils.FakeLifecycleOwner +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.testWithLifecycleOwner +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class BatteryOptimizationPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter( + pushDataStore = InMemoryPushDataStore( + initialShouldDisplayBatteryOptimizationBanner = false, + ), + batteryOptimization = FakeBatteryOptimization( + isIgnoringBatteryOptimizationsResult = false, + ), + ) + val lifeCycleOwner = FakeLifecycleOwner() + presenter.testWithLifecycleOwner(lifeCycleOwner) { + val initialState = awaitItem() + assertThat(initialState.shouldDisplayBanner).isFalse() + lifeCycleOwner.givenState(Lifecycle.State.RESUMED) + } + } + + @Test + fun `present - should display banner`() = runTest { + val presenter = createPresenter( + pushDataStore = InMemoryPushDataStore( + initialShouldDisplayBatteryOptimizationBanner = true, + ), + batteryOptimization = FakeBatteryOptimization( + isIgnoringBatteryOptimizationsResult = false, + ), + ) + presenter.testWithLifecycleOwner { + val initialState = awaitItem() + assertThat(initialState.shouldDisplayBanner).isFalse() + assertThat(awaitItem().shouldDisplayBanner).isTrue() + } + } + + @Test + fun `present - should display banner, but setting already performed`() = runTest { + val presenter = createPresenter( + pushDataStore = InMemoryPushDataStore( + initialShouldDisplayBatteryOptimizationBanner = true, + ), + batteryOptimization = FakeBatteryOptimization( + isIgnoringBatteryOptimizationsResult = true, + ), + ) + presenter.testWithLifecycleOwner { + val initialState = awaitItem() + assertThat(initialState.shouldDisplayBanner).isFalse() + assertThat(awaitItem().shouldDisplayBanner).isFalse() + } + } + + @Test + fun `present - should display banner, user dismisses`() = runTest { + val onOptimizationBannerDismissedResult = lambdaRecorder { } + val presenter = createPresenter( + pushDataStore = InMemoryPushDataStore( + initialShouldDisplayBatteryOptimizationBanner = true, + ), + batteryOptimization = FakeBatteryOptimization( + isIgnoringBatteryOptimizationsResult = false, + ), + mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore( + onOptimizationBannerDismissedResult = onOptimizationBannerDismissedResult, + ), + ) + presenter.testWithLifecycleOwner { + val initialState = awaitItem() + assertThat(initialState.shouldDisplayBanner).isFalse() + val displayedItem = awaitItem() + assertThat(displayedItem.shouldDisplayBanner).isTrue() + displayedItem.eventSink(BatteryOptimizationEvents.Dismiss) + onOptimizationBannerDismissedResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - should display banner, user continue, error case`() = runTest { + val onOptimizationBannerDismissedResult = lambdaRecorder { } + val requestDisablingBatteryOptimizationResult = lambdaRecorder { false } + val presenter = createPresenter( + pushDataStore = InMemoryPushDataStore( + initialShouldDisplayBatteryOptimizationBanner = true, + ), + batteryOptimization = FakeBatteryOptimization( + isIgnoringBatteryOptimizationsResult = false, + requestDisablingBatteryOptimizationResult = requestDisablingBatteryOptimizationResult + ), + mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore( + onOptimizationBannerDismissedResult = onOptimizationBannerDismissedResult, + ), + ) + presenter.testWithLifecycleOwner { + val initialState = awaitItem() + assertThat(initialState.shouldDisplayBanner).isFalse() + val displayedItem = awaitItem() + assertThat(displayedItem.shouldDisplayBanner).isTrue() + displayedItem.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) + requestDisablingBatteryOptimizationResult.assertions().isCalledOnce() + onOptimizationBannerDismissedResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - should display banner, user continue, nominal case`() = runTest { + val requestDisablingBatteryOptimizationResult = lambdaRecorder { true } + val batteryOptimization = FakeBatteryOptimization( + isIgnoringBatteryOptimizationsResult = false, + requestDisablingBatteryOptimizationResult = requestDisablingBatteryOptimizationResult + ) + val presenter = createPresenter( + pushDataStore = InMemoryPushDataStore( + initialShouldDisplayBatteryOptimizationBanner = true, + ), + batteryOptimization = batteryOptimization, + mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(), + ) + val lifeCycleOwner = FakeLifecycleOwner() + presenter.testWithLifecycleOwner(lifeCycleOwner) { + val initialState = awaitItem() + assertThat(initialState.shouldDisplayBanner).isFalse() + val displayedItem = awaitItem() + assertThat(displayedItem.shouldDisplayBanner).isTrue() + displayedItem.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) + requestDisablingBatteryOptimizationResult.assertions().isCalledOnce() + batteryOptimization.isIgnoringBatteryOptimizationsResult = true + lifeCycleOwner.givenState(Lifecycle.State.RESUMED) + assertThat(awaitItem().shouldDisplayBanner).isFalse() + assertThat(awaitItem().shouldDisplayBanner).isFalse() + } + } + + private fun createPresenter( + pushDataStore: PushDataStore = InMemoryPushDataStore(), + mutableBatteryOptimizationStore: MutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(), + batteryOptimization: BatteryOptimization = FakeBatteryOptimization(), + ) = BatteryOptimizationPresenter( + pushDataStore = pushDataStore, + mutableBatteryOptimizationStore = mutableBatteryOptimizationStore, + batteryOptimization = batteryOptimization + ) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt new file mode 100644 index 00000000000..0adb3d25202 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt @@ -0,0 +1,23 @@ +/* + * 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.push.impl.battery + +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeBatteryOptimization( + var isIgnoringBatteryOptimizationsResult: Boolean = false, + private val requestDisablingBatteryOptimizationResult: () -> Boolean = { lambdaError() } +) : BatteryOptimization { + override fun isIgnoringBatteryOptimizations(): Boolean { + return isIgnoringBatteryOptimizationsResult + } + + override fun requestDisablingBatteryOptimization(): Boolean { + return requestDisablingBatteryOptimizationResult() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/history/FakePushHistoryService.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/history/FakePushHistoryService.kt new file mode 100644 index 00000000000..50a06756dc5 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/history/FakePushHistoryService.kt @@ -0,0 +1,45 @@ +/* + * 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.push.impl.history + +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.tests.testutils.lambda.lambdaError + +class FakePushHistoryService( + private val onPushReceivedResult: ( + String, + EventId?, + RoomId?, + SessionId?, + Boolean, + Boolean, + String? + ) -> Unit = { _, _, _, _, _, _, _ -> lambdaError() } +) : PushHistoryService { + override fun onPushReceived( + providerInfo: String, + eventId: EventId?, + roomId: RoomId?, + sessionId: SessionId?, + hasBeenResolved: Boolean, + includeDeviceState: Boolean, + comment: String?, + ) { + onPushReceivedResult( + providerInfo, + eventId, + roomId, + sessionId, + hasBeenResolved, + includeDeviceState, + comment + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt new file mode 100644 index 00000000000..ba4e1657a72 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt @@ -0,0 +1,231 @@ +/* + * 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. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.NotificationConfig +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_TIMESTAMP +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL +import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.push.impl.notifications.factories.MARK_AS_READ_ACTION_TITLE +import io.element.android.libraries.push.impl.notifications.factories.QUICK_REPLY_ACTION_TITLE +import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator +import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.libraries.push.test.notifications.FakeImageLoader +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider +import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +private const val A_ROOM_AVATAR = "mxc://roomAvatar" +private const val A_USER_AVATAR_1 = "mxc://userAvatar1" +private const val A_USER_AVATAR_2 = "mxc://userAvatar2" + +@RunWith(RobolectricTestRunner::class) +class DefaultBaseRoomGroupMessageCreatorTest { + @Test + fun `test createRoomMessage with one Event`() = runTest { + val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + imageUriString = "aUri", + ) + ), + roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), + existingNotification = null, + ) + assertThat(result.number).isEqualTo(1) + @Suppress("DEPRECATION") + assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_LOW) + assertThat(result.`when`).isEqualTo(A_TIMESTAMP) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + } + + @Test + fun `test createRoomMessage with one noisy Event`() = runTest { + val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + noisy = true, + ) + ), + roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), + existingNotification = null, + ) + @Suppress("DEPRECATION") + assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + } + + @Test + fun `test createRoomMessage with room avatar and sender avatar android O`() { + `test createRoomMessage with room avatar and sender avatar`( + api = Build.VERSION_CODES.O, + // Only the Room avatar is loaded + expectedCoilRequests = listOf( + MediaRequestData( + source = MediaSource(url = A_ROOM_AVATAR), + kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) + ) + ) + ) + } + + @Test + fun `test createRoomMessage with room avatar and sender avatar android P`() = runTest { + `test createRoomMessage with room avatar and sender avatar`( + api = Build.VERSION_CODES.P, + // Room and user avatar are loaded + expectedCoilRequests = listOf( + MediaRequestData( + source = MediaSource(url = A_USER_AVATAR_1), + kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) + ), + MediaRequestData( + source = MediaSource(url = A_USER_AVATAR_2), + kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) + ), + MediaRequestData( + source = MediaSource(url = A_ROOM_AVATAR), + kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) + ), + ) + ) + } + + private fun `test createRoomMessage with room avatar and sender avatar`( + api: Int, + expectedCoilRequests: List, + ) = runTest { + val fakeImageLoader = FakeImageLoader() + val sut = createRoomGroupMessageCreator( + sdkIntProvider = FakeBuildVersionSdkIntProvider(api) + ) + val result = sut.createRoomMessage( + currentUser = aMatrixUser( + // Some user avatar + avatarUrl = A_USER_AVATAR_1, + ), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + roomAvatarPath = A_ROOM_AVATAR, + senderAvatarPath = A_USER_AVATAR_2, + ) + ), + roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), + existingNotification = null, + ) + assertThat(result.number).isEqualTo(1) + assertThat(fakeImageLoader.getCoilRequests()).containsExactlyElementsIn(expectedCoilRequests) + } + + @Test + fun `test createRoomMessage with two Events`() = runTest { + val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP), + aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10), + ), + roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), + existingNotification = null, + ) + assertThat(result.number).isEqualTo(2) + assertThat(result.`when`).isEqualTo(A_TIMESTAMP + 10) + val actionTitles = result.actions?.map { it.title } + assertThat(actionTitles).isEqualTo( + listOfNotNull( + MARK_AS_READ_ACTION_TITLE.takeIf { NotificationConfig.SHOW_MARK_AS_READ_ACTION }, + QUICK_REPLY_ACTION_TITLE.takeIf { NotificationConfig.SHOW_QUICK_REPLY_ACTION }, + ) + ) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + } + + @Test + fun `test createRoomMessage with smart reply error`() = runTest { + val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + outGoingMessage = true, + outGoingMessageFailed = true, + ), + ), + roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), + existingNotification = null, + ) + val actionTitles = result.actions?.map { it.title } + assertThat(actionTitles).isEqualTo( + listOfNotNull( + MARK_AS_READ_ACTION_TITLE.takeIf { NotificationConfig.SHOW_MARK_AS_READ_ACTION } + ) + ) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + } + + @Test + fun `test createRoomMessage for DM`() = runTest { + val sut = createRoomGroupMessageCreator() + val fakeImageLoader = FakeImageLoader() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + roomIsDm = true, + ), + ), + roomId = A_ROOM_ID, + imageLoader = fakeImageLoader.getImageLoader(), + existingNotification = null, + ) + assertThat(result.number).isEqualTo(1) + assertThat(result.`when`).isEqualTo(A_TIMESTAMP) + assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + } +} + +fun createRoomGroupMessageCreator( + sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O), +): RoomGroupMessageCreator { + val context = RuntimeEnvironment.getApplication() as Context + val bitmapLoader = DefaultNotificationBitmapLoader( + context = RuntimeEnvironment.getApplication(), + sdkIntProvider = sdkIntProvider, + ) + return DefaultRoomGroupMessageCreator( + notificationCreator = createNotificationCreator(bitmapLoader = bitmapLoader), + bitmapLoader = bitmapLoader, + stringProvider = AndroidStringProvider(context.resources) + ) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultCallNotificationEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultCallNotificationEventResolverTest.kt new file mode 100644 index 00000000000..c9ebbb72b32 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultCallNotificationEventResolverTest.kt @@ -0,0 +1,171 @@ +/* + * 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.push.impl.notifications + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.notification.CallNotifyType +import io.element.android.libraries.matrix.api.notification.NotificationContent +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_NAME_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.test.notification.aNotificationData +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent +import io.element.android.services.appnavstate.test.FakeAppForegroundStateService +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultCallNotificationEventResolverTest { + @Test + fun `resolve CallNotify - RING when call is still ongoing`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + // The call is still ongoing + initialRoomInfo = aRoomInfo(hasRoomCall = true), + ) + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + + val resolver = createDefaultNotifiableEventResolver( + clientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }), + ) + val expectedResult = NotifiableRingingCallEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + senderId = A_USER_ID_2, + roomName = A_ROOM_NAME, + editedEventId = null, + description = "📹 Incoming call", + timestamp = 567L, + canBeReplaced = true, + isRedacted = false, + isUpdated = false, + senderDisambiguatedDisplayName = A_USER_NAME_2, + senderAvatarUrl = null, + callNotifyType = CallNotifyType.RING, + ) + + val notificationData = aNotificationData( + content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.RING) + ) + val result = resolver.resolveEvent(A_SESSION_ID, notificationData) + assertThat(result.getOrNull()).isEqualTo(expectedResult) + } + + @Test + fun `resolve CallNotify - NOTIFY`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + // The call already ended + initialRoomInfo = aRoomInfo(hasRoomCall = true), + ) + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + + val resolver = createDefaultNotifiableEventResolver( + clientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }), + ) + val expectedResult = NotifiableMessageEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + senderId = A_USER_ID_2, + roomName = A_ROOM_NAME, + editedEventId = null, + body = "📹 Incoming call", + timestamp = 567L, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + senderDisambiguatedDisplayName = A_USER_NAME_2, + noisy = true, + imageUriString = null, + imageMimeType = null, + threadId = null, + type = "m.call.notify", + ) + + val notificationData = aNotificationData( + content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.NOTIFY) + ) + val result = resolver.resolveEvent(A_SESSION_ID, notificationData) + assertThat(result.getOrNull()).isEqualTo(expectedResult) + } + + @Test + fun `resolve CallNotify - RING but timed out displays the same as NOTIFY`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + // The call already ended + initialRoomInfo = aRoomInfo(hasRoomCall = false), + ) + ) + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + + val resolver = createDefaultNotifiableEventResolver( + clientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }), + ) + val expectedResult = NotifiableMessageEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + senderId = A_USER_ID_2, + roomName = A_ROOM_NAME, + editedEventId = null, + body = "📹 Incoming call", + timestamp = 567L, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + senderDisambiguatedDisplayName = A_USER_NAME_2, + noisy = true, + imageUriString = null, + imageMimeType = null, + threadId = null, + type = "m.call.notify", + ) + + val notificationData = aNotificationData( + content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.RING) + ) + val result = resolver.resolveEvent(A_SESSION_ID, notificationData) + assertThat(result.getOrNull()).isEqualTo(expectedResult) + } + + private fun createDefaultNotifiableEventResolver( + stringProvider: FakeStringProvider = FakeStringProvider(defaultResult = "\uD83D\uDCF9 Incoming call"), + appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(), + clientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), + ) = DefaultCallNotificationEventResolver( + stringProvider = stringProvider, + appForegroundStateService = appForegroundStateService, + clientProvider = clientProvider, + ) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index a194c9d67b1..3c609f184e2 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context import com.google.common.truth.Truth.assertThat +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.notification.CallNotifyType import io.element.android.libraries.matrix.api.notification.NotificationContent @@ -42,17 +43,14 @@ import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notification.aNotificationData import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser -import io.element.android.libraries.preferences.api.store.AppPreferencesStore -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationMediaRepo import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent -import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent +import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.services.toolbox.impl.strings.AndroidStringProvider -import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import kotlinx.coroutines.test.runTest @@ -68,8 +66,8 @@ class DefaultNotifiableEventResolverTest { @Test fun `resolve event no session`() = runTest { val sut = createDefaultNotifiableEventResolver(notificationService = null) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isNull() + val result = sut.resolveEvents(A_SESSION_ID, listOf(NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase"))) + assertThat(result.isFailure).isTrue() } @Test @@ -77,36 +75,31 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.failure(AN_EXCEPTION) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isNull() - } - - @Test - fun `resolve event null`() = runTest { - val sut = createDefaultNotifiableEventResolver( - notificationResult = Result.success(null) - ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isNull() + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) + assertThat(result.getEvent(request)?.isFailure).isTrue() } @Test fun `resolve event message text`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = TextMessageType(body = "Hello world", formatted = null) - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = TextMessageType(body = "Hello world", formatted = null) + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Hello world") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test @@ -114,292 +107,337 @@ class DefaultNotifiableEventResolverTest { fun `resolve event message with mention`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = TextMessageType(body = "Hello world", formatted = null) - ), - hasMention = true, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = TextMessageType(body = "Hello world", formatted = null) + ), + hasMention = true, + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true) ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve HTML formatted event message text takes plain text version`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = TextMessageType( - body = "Hello world!", - formatted = FormattedBody( - body = "Hello world", - format = MessageFormat.HTML, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = TextMessageType( + body = "Hello world!", + formatted = FormattedBody( + body = "Hello world", + format = MessageFormat.HTML, + ) ) - ) - ), + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Hello world") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve incorrectly formatted event message text uses fallback`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = TextMessageType( - body = "Hello world", - formatted = FormattedBody( - body = "???Hello world!???", - format = MessageFormat.UNKNOWN, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = TextMessageType( + body = "Hello world", + formatted = FormattedBody( + body = "???Hello world!???", + format = MessageFormat.UNKNOWN, + ) ) - ) - ), + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Hello world") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve event message audio`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = AudioMessageType("Audio", null, null, MediaSource("url"), null) - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = AudioMessageType("Audio", null, null, MediaSource("url"), null) + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Audio") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve event message video`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = VideoMessageType("Video", null, null, MediaSource("url"), null) - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = VideoMessageType("Video", null, null, MediaSource("url"), null) + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Video") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve event message voice`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null) - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null) + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Voice message") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve event message image`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = ImageMessageType("Image", null, null, MediaSource("url"), null), - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = ImageMessageType("Image", null, null, MediaSource("url"), null), + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Image") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve event message sticker`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = StickerMessageType("Sticker", null, null, MediaSource("url"), null), - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = StickerMessageType("Sticker", null, null, MediaSource("url"), null), + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Sticker") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve event message file`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = FileMessageType("File", null, null, MediaSource("url"), null), - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = FileMessageType("File", null, null, MediaSource("url"), null), + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "File") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve event message location`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = LocationMessageType("Location", "geo:1,2", null), - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = LocationMessageType("Location", "geo:1,2", null), + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Location") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve event message notice`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = NoticeMessageType("Notice", null), - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = NoticeMessageType("Notice", null), + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Notice") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve event message emote`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomMessage( - senderId = A_USER_ID_2, - messageType = EmoteMessageType("is happy", null), - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = EmoteMessageType("is happy", null), + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "* Bob is happy") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve poll`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.Poll( - senderId = A_USER_ID_2, - question = "A question" - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.Poll( + senderId = A_USER_ID_2, + question = "A question" + ), + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( aNotifiableMessageEvent(body = "Poll: A question") ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve RoomMemberContent invite room`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.StateEvent.RoomMemberContent( - userId = A_USER_ID_2, - membershipState = RoomMembershipState.INVITE - ), - isDirect = false, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.StateEvent.RoomMemberContent( + userId = A_USER_ID_2, + membershipState = RoomMembershipState.INVITE + ), + isDirect = false, + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isNull() + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) + assertThat(result.getEvent(request)?.getOrNull()).isNull() } @Test fun `resolve invite room`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.Invite( - senderId = A_USER_ID_2, - ), - isDirect = false, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.Invite( + senderId = A_USER_ID_2, + ), + isDirect = false, + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( InviteNotifiableEvent( sessionId = A_SESSION_ID, @@ -418,22 +456,25 @@ class DefaultNotifiableEventResolverTest { isUpdated = false, ) ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve invite direct`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.Invite( - senderId = A_USER_ID_2, - ), - isDirect = true, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.Invite( + senderId = A_USER_ID_2, + ), + isDirect = true, + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( InviteNotifiableEvent( sessionId = A_SESSION_ID, @@ -452,23 +493,26 @@ class DefaultNotifiableEventResolverTest { isUpdated = false, ) ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve invite direct, no display name`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.Invite( - senderId = A_USER_ID_2, - ), - isDirect = true, - senderDisplayName = null, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.Invite( + senderId = A_USER_ID_2, + ), + isDirect = true, + senderDisplayName = null, + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( InviteNotifiableEvent( sessionId = A_SESSION_ID, @@ -487,23 +531,26 @@ class DefaultNotifiableEventResolverTest { isUpdated = false, ) ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve invite direct, ambiguous display name`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.Invite( - senderId = A_USER_ID_2, - ), - isDirect = false, - senderIsNameAmbiguous = true, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.Invite( + senderId = A_USER_ID_2, + ), + isDirect = false, + senderIsNameAmbiguous = true, + ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( InviteNotifiableEvent( sessionId = A_SESSION_ID, @@ -522,173 +569,134 @@ class DefaultNotifiableEventResolverTest { isUpdated = false, ) ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve RoomMemberContent other`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.StateEvent.RoomMemberContent( - userId = A_USER_ID_2, - membershipState = RoomMembershipState.JOIN + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.StateEvent.RoomMemberContent( + userId = A_USER_ID_2, + membershipState = RoomMembershipState.JOIN + ) ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isNull() + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) + assertThat(result.getEvent(request)?.getOrNull()).isNull() } @Test fun `resolve RoomEncrypted`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomEncrypted - ) + mapOf(AN_EVENT_ID to aNotificationData(content = NotificationContent.MessageLike.RoomEncrypted)) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( FallbackNotifiableEvent( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, eventId = AN_EVENT_ID, editedEventId = null, - description = "Notification", + description = "You have new messages.", canBeReplaced = true, isRedacted = false, isUpdated = false, timestamp = A_FAKE_TIMESTAMP, ) ) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test - fun `resolve CallInvite`() = runTest { + fun `resolve UnableToResolve`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2), - ) + mapOf(AN_EVENT_ID to aNotificationData(content = NotificationContent.MessageLike.UnableToResolve)) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( - NotifiableMessageEvent( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID, - eventId = AN_EVENT_ID, - editedEventId = null, - canBeReplaced = false, - senderId = A_USER_ID_2, - noisy = false, - timestamp = A_TIMESTAMP, - senderDisambiguatedDisplayName = A_USER_NAME_2, - body = "Unsupported call", - imageUriString = null, - imageMimeType = null, - threadId = null, - roomName = A_ROOM_NAME, - roomAvatarPath = null, - senderAvatarPath = null, - soundName = null, - outGoingMessage = false, - outGoingMessageFailed = false, - isRedacted = false, - isUpdated = false - ) - ) - assertThat(result).isEqualTo(expectedResult) - } - - @Test - fun `resolve CallNotify - ringing`() = runTest { - val timestamp = DefaultSystemClock().epochMillis() - val sut = createDefaultNotifiableEventResolver( - notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.CallNotify( - A_USER_ID_2, - CallNotifyType.RING - ), - timestamp = timestamp, - ) - ) - ) - val expectedResult = ResolvedPushEvent.Event( - NotifiableRingingCallEvent( + FallbackNotifiableEvent( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, eventId = AN_EVENT_ID, - senderId = A_USER_ID_2, - roomName = A_ROOM_NAME, editedEventId = null, - description = "📹 Incoming call", - timestamp = timestamp, + description = "You have new messages.", canBeReplaced = true, isRedacted = false, isUpdated = false, - senderDisambiguatedDisplayName = A_USER_NAME_2, - senderAvatarUrl = null, - callNotifyType = CallNotifyType.RING, + timestamp = A_FAKE_TIMESTAMP, ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test - fun `resolve CallNotify - ring but timed out displays the same as notify`() = runTest { + fun `resolve CallInvite`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.CallNotify( - A_USER_ID_2, - CallNotifyType.RING - ), - timestamp = 0L, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2), + ) ) ) ) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) val expectedResult = ResolvedPushEvent.Event( NotifiableMessageEvent( sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, eventId = AN_EVENT_ID, editedEventId = null, - noisy = true, - timestamp = 0L, - senderDisambiguatedDisplayName = A_USER_NAME_2, + canBeReplaced = false, senderId = A_USER_ID_2, - body = "📹 Incoming call", - roomId = A_ROOM_ID, + noisy = false, + timestamp = A_TIMESTAMP, + senderDisambiguatedDisplayName = A_USER_NAME_2, + body = "Unsupported call", + imageUriString = null, + imageMimeType = null, threadId = null, roomName = A_ROOM_NAME, - canBeReplaced = false, + roomAvatarPath = null, + senderAvatarPath = null, + soundName = null, + outGoingMessage = false, + outGoingMessageFailed = false, isRedacted = false, - imageUriString = null, - imageMimeType = null, - type = EventType.CALL_NOTIFY, + isUpdated = false ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isEqualTo(expectedResult) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test - fun `resolve CallNotify - notify`() = runTest { + fun `resolve CallNotify - goes through CallNotificationEventResolver`() = runTest { + val callNotificationEventResolver = FakeCallNotificationEventResolver() val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.CallNotify( - A_USER_ID_2, - CallNotifyType.NOTIFY - ), + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.CallNotify( + A_USER_ID_2, + CallNotifyType.NOTIFY + ), + ) ) - ) + ), + callNotificationEventResolver = callNotificationEventResolver, ) val expectedResult = ResolvedPushEvent.Event( NotifiableMessageEvent( @@ -710,18 +718,22 @@ class DefaultNotifiableEventResolverTest { type = EventType.CALL_NOTIFY, ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isEqualTo(expectedResult) + callNotificationEventResolver.resolveEventLambda = { _, _, _ -> Result.success(expectedResult.notifiableEvent) } + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve RoomRedaction`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomRedaction( - AN_EVENT_ID_2, - A_REDACTION_REASON, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomRedaction( + AN_EVENT_ID_2, + A_REDACTION_REASON, + ) ) ) ) @@ -732,81 +744,90 @@ class DefaultNotifiableEventResolverTest { redactedEventId = AN_EVENT_ID_2, reason = A_REDACTION_REASON, ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isEqualTo(expectedResult) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } @Test fun `resolve RoomRedaction with null redactedEventId should return null`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = NotificationContent.MessageLike.RoomRedaction( - null, - A_REDACTION_REASON, + mapOf( + AN_EVENT_ID to aNotificationData( + content = NotificationContent.MessageLike.RoomRedaction( + null, + A_REDACTION_REASON, + ) ) ) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isNull() + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) + assertThat(result.getEvent(request)?.getOrNull()).isNull() } @Test fun `resolve null cases`() { - testNull(NotificationContent.MessageLike.CallAnswer) - testNull(NotificationContent.MessageLike.CallHangup) - testNull(NotificationContent.MessageLike.CallCandidates) - testNull(NotificationContent.MessageLike.KeyVerificationReady) - testNull(NotificationContent.MessageLike.KeyVerificationStart) - testNull(NotificationContent.MessageLike.KeyVerificationCancel) - testNull(NotificationContent.MessageLike.KeyVerificationAccept) - testNull(NotificationContent.MessageLike.KeyVerificationKey) - testNull(NotificationContent.MessageLike.KeyVerificationMac) - testNull(NotificationContent.MessageLike.KeyVerificationDone) - testNull(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value)) - testNull(NotificationContent.MessageLike.Sticker) - testNull(NotificationContent.StateEvent.PolicyRuleRoom) - testNull(NotificationContent.StateEvent.PolicyRuleServer) - testNull(NotificationContent.StateEvent.PolicyRuleUser) - testNull(NotificationContent.StateEvent.RoomAliases) - testNull(NotificationContent.StateEvent.RoomAvatar) - testNull(NotificationContent.StateEvent.RoomCanonicalAlias) - testNull(NotificationContent.StateEvent.RoomCreate) - testNull(NotificationContent.StateEvent.RoomEncryption) - testNull(NotificationContent.StateEvent.RoomGuestAccess) - testNull(NotificationContent.StateEvent.RoomHistoryVisibility) - testNull(NotificationContent.StateEvent.RoomJoinRules) - testNull(NotificationContent.StateEvent.RoomName) - testNull(NotificationContent.StateEvent.RoomPinnedEvents) - testNull(NotificationContent.StateEvent.RoomPowerLevels) - testNull(NotificationContent.StateEvent.RoomServerAcl) - testNull(NotificationContent.StateEvent.RoomThirdPartyInvite) - testNull(NotificationContent.StateEvent.RoomTombstone) - testNull(NotificationContent.StateEvent.RoomTopic) - testNull(NotificationContent.StateEvent.SpaceChild) - testNull(NotificationContent.StateEvent.SpaceParent) - } - - private fun testNull(content: NotificationContent) = runTest { + testNoResults(NotificationContent.MessageLike.CallAnswer) + testNoResults(NotificationContent.MessageLike.CallHangup) + testNoResults(NotificationContent.MessageLike.CallCandidates) + testNoResults(NotificationContent.MessageLike.KeyVerificationReady) + testNoResults(NotificationContent.MessageLike.KeyVerificationStart) + testNoResults(NotificationContent.MessageLike.KeyVerificationCancel) + testNoResults(NotificationContent.MessageLike.KeyVerificationAccept) + testNoResults(NotificationContent.MessageLike.KeyVerificationKey) + testNoResults(NotificationContent.MessageLike.KeyVerificationMac) + testNoResults(NotificationContent.MessageLike.KeyVerificationDone) + testNoResults(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value)) + testNoResults(NotificationContent.MessageLike.Sticker) + testNoResults(NotificationContent.StateEvent.PolicyRuleRoom) + testNoResults(NotificationContent.StateEvent.PolicyRuleServer) + testNoResults(NotificationContent.StateEvent.PolicyRuleUser) + testNoResults(NotificationContent.StateEvent.RoomAliases) + testNoResults(NotificationContent.StateEvent.RoomAvatar) + testNoResults(NotificationContent.StateEvent.RoomCanonicalAlias) + testNoResults(NotificationContent.StateEvent.RoomCreate) + testNoResults(NotificationContent.StateEvent.RoomEncryption) + testNoResults(NotificationContent.StateEvent.RoomGuestAccess) + testNoResults(NotificationContent.StateEvent.RoomHistoryVisibility) + testNoResults(NotificationContent.StateEvent.RoomJoinRules) + testNoResults(NotificationContent.StateEvent.RoomName) + testNoResults(NotificationContent.StateEvent.RoomPinnedEvents) + testNoResults(NotificationContent.StateEvent.RoomPowerLevels) + testNoResults(NotificationContent.StateEvent.RoomServerAcl) + testNoResults(NotificationContent.StateEvent.RoomThirdPartyInvite) + testNoResults(NotificationContent.StateEvent.RoomTombstone) + testNoResults(NotificationContent.StateEvent.RoomTopic("")) + testNoResults(NotificationContent.StateEvent.SpaceChild) + testNoResults(NotificationContent.StateEvent.SpaceParent) + } + + private fun testNoResults(content: NotificationContent) = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - aNotificationData( - content = content - ) + mapOf(AN_EVENT_ID to aNotificationData(content = content)) ) ) - val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) - assertThat(result).isNull() + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) + assertThat(result.getEvent(request)?.getOrNull()).isNull() + } + + private fun Result>>.getEvent( + request: NotificationEventRequest + ): Result? { + return getOrNull()?.get(request) } private fun createDefaultNotifiableEventResolver( notificationService: FakeNotificationService? = FakeNotificationService(), - notificationResult: Result = Result.success(null), - appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + notificationResult: Result> = Result.success(emptyMap()), + callNotificationEventResolver: FakeCallNotificationEventResolver = FakeCallNotificationEventResolver(), ): DefaultNotifiableEventResolver { val context = RuntimeEnvironment.getApplication() as Context - notificationService?.givenGetNotificationResult(notificationResult) + notificationService?.givenGetNotificationsResult(notificationResult) val matrixClientProvider = FakeMatrixClientProvider(getClient = { if (notificationService == null) { Result.failure(IllegalStateException("Client not found")) @@ -824,10 +845,7 @@ class DefaultNotifiableEventResolverTest { notificationMediaRepoFactory = notificationMediaRepoFactory, context = context, permalinkParser = FakePermalinkParser(), - callNotificationEventResolver = DefaultCallNotificationEventResolver( - stringProvider = AndroidStringProvider(context.resources) - ), - appPreferencesStore = appPreferencesStore, + callNotificationEventResolver = callNotificationEventResolver, ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt index 06a955a084d..1dc21a7ebee 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt @@ -42,7 +42,9 @@ class DefaultOnMissedCallNotificationHandlerTest { // Create a fake matrix client provider that returns a fake matrix client with a fake notification service that returns a valid notification data val matrixClientProvider = FakeMatrixClientProvider(getClient = { val notificationService = FakeNotificationService().apply { - givenGetNotificationResult(Result.success(aNotificationData(senderDisplayName = A_USER_NAME, senderIsNameAmbiguous = false))) + givenGetNotificationsResult( + Result.success(mapOf(AN_EVENT_ID to aNotificationData(senderDisplayName = A_USER_NAME, senderIsNameAmbiguous = false))) + ) } Result.success(FakeMatrixClient(notificationService = notificationService)) }) @@ -60,7 +62,9 @@ class DefaultOnMissedCallNotificationHandlerTest { imageLoaderHolder = FakeImageLoaderHolder(), activeNotificationsProvider = FakeActiveNotificationsProvider(), ), - callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ -> aNotifiableMessageEvent() }), + callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ -> + Result.success(aNotifiableMessageEvent()) + }), ) defaultOnMissedCallNotificationHandler.addMissedCallNotification( diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt deleted file mode 100644 index ca675c8f8b5..00000000000 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt +++ /dev/null @@ -1,231 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.push.impl.notifications - -import android.content.Context -import android.os.Build -import androidx.core.app.NotificationCompat -import com.google.common.truth.Truth.assertThat -import io.element.android.appconfig.NotificationConfig -import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_TIMESTAMP -import io.element.android.libraries.matrix.ui.components.aMatrixUser -import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL -import io.element.android.libraries.matrix.ui.media.MediaRequestData -import io.element.android.libraries.push.impl.notifications.factories.MARK_AS_READ_ACTION_TITLE -import io.element.android.libraries.push.impl.notifications.factories.QUICK_REPLY_ACTION_TITLE -import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator -import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent -import io.element.android.libraries.push.test.notifications.FakeImageLoader -import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider -import io.element.android.services.toolbox.impl.strings.AndroidStringProvider -import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment - -private const val A_ROOM_AVATAR = "mxc://roomAvatar" -private const val A_USER_AVATAR_1 = "mxc://userAvatar1" -private const val A_USER_AVATAR_2 = "mxc://userAvatar2" - -@RunWith(RobolectricTestRunner::class) -class DefaultRoomGroupMessageCreatorTest { - @Test - fun `test createRoomMessage with one Event`() = runTest { - val sut = createRoomGroupMessageCreator() - val fakeImageLoader = FakeImageLoader() - val result = sut.createRoomMessage( - currentUser = aMatrixUser(), - events = listOf( - aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( - imageUriString = "aUri", - ) - ), - roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), - existingNotification = null, - ) - assertThat(result.number).isEqualTo(1) - @Suppress("DEPRECATION") - assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_LOW) - assertThat(result.`when`).isEqualTo(A_TIMESTAMP) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) - } - - @Test - fun `test createRoomMessage with one noisy Event`() = runTest { - val sut = createRoomGroupMessageCreator() - val fakeImageLoader = FakeImageLoader() - val result = sut.createRoomMessage( - currentUser = aMatrixUser(), - events = listOf( - aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( - noisy = true, - ) - ), - roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), - existingNotification = null, - ) - @Suppress("DEPRECATION") - assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) - } - - @Test - fun `test createRoomMessage with room avatar and sender avatar android O`() { - `test createRoomMessage with room avatar and sender avatar`( - api = Build.VERSION_CODES.O, - // Only the Room avatar is loaded - expectedCoilRequests = listOf( - MediaRequestData( - source = MediaSource(url = A_ROOM_AVATAR), - kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) - ) - ) - ) - } - - @Test - fun `test createRoomMessage with room avatar and sender avatar android P`() = runTest { - `test createRoomMessage with room avatar and sender avatar`( - api = Build.VERSION_CODES.P, - // Room and user avatar are loaded - expectedCoilRequests = listOf( - MediaRequestData( - source = MediaSource(url = A_USER_AVATAR_1), - kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) - ), - MediaRequestData( - source = MediaSource(url = A_USER_AVATAR_2), - kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) - ), - MediaRequestData( - source = MediaSource(url = A_ROOM_AVATAR), - kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL) - ), - ) - ) - } - - private fun `test createRoomMessage with room avatar and sender avatar`( - api: Int, - expectedCoilRequests: List, - ) = runTest { - val fakeImageLoader = FakeImageLoader() - val sut = createRoomGroupMessageCreator( - sdkIntProvider = FakeBuildVersionSdkIntProvider(api) - ) - val result = sut.createRoomMessage( - currentUser = aMatrixUser( - // Some user avatar - avatarUrl = A_USER_AVATAR_1, - ), - events = listOf( - aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( - roomAvatarPath = A_ROOM_AVATAR, - senderAvatarPath = A_USER_AVATAR_2, - ) - ), - roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), - existingNotification = null, - ) - assertThat(result.number).isEqualTo(1) - assertThat(fakeImageLoader.getCoilRequests()).containsExactlyElementsIn(expectedCoilRequests) - } - - @Test - fun `test createRoomMessage with two Events`() = runTest { - val sut = createRoomGroupMessageCreator() - val fakeImageLoader = FakeImageLoader() - val result = sut.createRoomMessage( - currentUser = aMatrixUser(), - events = listOf( - aNotifiableMessageEvent(timestamp = A_TIMESTAMP), - aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10), - ), - roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), - existingNotification = null, - ) - assertThat(result.number).isEqualTo(2) - assertThat(result.`when`).isEqualTo(A_TIMESTAMP + 10) - val actionTitles = result.actions?.map { it.title } - assertThat(actionTitles).isEqualTo( - listOfNotNull( - MARK_AS_READ_ACTION_TITLE.takeIf { NotificationConfig.SHOW_MARK_AS_READ_ACTION }, - QUICK_REPLY_ACTION_TITLE.takeIf { NotificationConfig.SHOW_QUICK_REPLY_ACTION }, - ) - ) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) - } - - @Test - fun `test createRoomMessage with smart reply error`() = runTest { - val sut = createRoomGroupMessageCreator() - val fakeImageLoader = FakeImageLoader() - val result = sut.createRoomMessage( - currentUser = aMatrixUser(), - events = listOf( - aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( - outGoingMessage = true, - outGoingMessageFailed = true, - ), - ), - roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), - existingNotification = null, - ) - val actionTitles = result.actions?.map { it.title } - assertThat(actionTitles).isEqualTo( - listOfNotNull( - MARK_AS_READ_ACTION_TITLE.takeIf { NotificationConfig.SHOW_MARK_AS_READ_ACTION } - ) - ) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) - } - - @Test - fun `test createRoomMessage for DM`() = runTest { - val sut = createRoomGroupMessageCreator() - val fakeImageLoader = FakeImageLoader() - val result = sut.createRoomMessage( - currentUser = aMatrixUser(), - events = listOf( - aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( - roomIsDm = true, - ), - ), - roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), - existingNotification = null, - ) - assertThat(result.number).isEqualTo(1) - assertThat(result.`when`).isEqualTo(A_TIMESTAMP) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) - } -} - -fun createRoomGroupMessageCreator( - sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O), -): RoomGroupMessageCreator { - val context = RuntimeEnvironment.getApplication() as Context - val bitmapLoader = DefaultNotificationBitmapLoader( - context = RuntimeEnvironment.getApplication(), - sdkIntProvider = sdkIntProvider, - ) - return DefaultRoomGroupMessageCreator( - notificationCreator = createNotificationCreator(bitmapLoader = bitmapLoader), - bitmapLoader = bitmapLoader, - stringProvider = AndroidStringProvider(context.resources) - ) -} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt index e800ffdfe2e..d38bc098a01 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt @@ -7,16 +7,18 @@ package io.element.android.libraries.push.impl.notifications -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.push.impl.notifications.model.ResolvedPushEvent import io.element.android.tests.testutils.lambda.lambdaError class FakeNotifiableEventResolver( - private val notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() } + private val resolveEventsResult: (SessionId, List) -> Result>> = + { _, _ -> lambdaError() } ) : NotifiableEventResolver { - override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? { - return notifiableEventResult(sessionId, roomId, eventId) + override suspend fun resolveEvents( + sessionId: SessionId, + notificationEventRequests: List + ): Result>> { + return resolveEventsResult(sessionId, notificationEventRequests) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt index 17093fef43c..3fd94863a16 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt @@ -8,15 +8,15 @@ package io.element.android.libraries.push.impl.notifications import android.content.Intent -import com.google.common.truth.Truth.assertThat 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 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.core.asEventId import io.element.android.libraries.matrix.api.room.IntentionalMention -import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.api.room.RoomInfo +import io.element.android.libraries.matrix.api.room.message.ReplyParameters +import io.element.android.libraries.matrix.api.room.message.replyInThread import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE @@ -26,7 +26,9 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.preferences.api.store.SessionPreferencesStore @@ -38,6 +40,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner +import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.test.strings.FakeStringProvider @@ -48,6 +51,7 @@ import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -219,13 +223,18 @@ class NotificationBroadcastReceiverHandlerTest { getLambda = getLambda ) val clearMessagesForRoomLambda = lambdaRecorder { _, _ -> } - val matrixRoom = FakeMatrixRoom() + val markAsReadResult = lambdaRecorder> { Result.success(Unit) } + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + markAsReadResult = markAsReadResult, + ), + ) val fakeNotificationCleaner = FakeNotificationCleaner( clearMessagesForRoomLambda = clearMessagesForRoomLambda, ) val sut = createNotificationBroadcastReceiverHandler( sessionPreferencesStore = sessionPreferencesStore, - matrixRoom = matrixRoom, + joinedRoom = joinedRoom, notificationCleaner = fakeNotificationCleaner ) sut.onReceive( @@ -238,7 +247,7 @@ class NotificationBroadcastReceiverHandlerTest { clearMessagesForRoomLambda.assertions() .isCalledOnce() .with(value(A_SESSION_ID), value(A_ROOM_ID)) - assertThat(matrixRoom.markAsReadCalls).isEqualTo(listOf(expectedReceiptType)) + markAsReadResult.assertions().isCalledOnce().with(value(expectedReceiptType)) } @Test @@ -253,7 +262,7 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test join room`() = runTest { - val joinRoom = lambdaRecorder> { _ -> Result.success(null) } + val joinRoom = lambdaRecorder> { _ -> Result.success(null) } val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> } val fakeNotificationCleaner = FakeNotificationCleaner( clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda, @@ -290,15 +299,15 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test reject room`() = runTest { val leaveRoom = lambdaRecorder> { Result.success(Unit) } - val matrixRoom = FakeMatrixRoom( - leaveRoomLambda = leaveRoom + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom(leaveRoomLambda = leaveRoom), ) val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> } val fakeNotificationCleaner = FakeNotificationCleaner( clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda, ) val sut = createNotificationBroadcastReceiverHandler( - matrixRoom = matrixRoom, + joinedRoom = joinedRoom, notificationCleaner = fakeNotificationCleaner ) sut.onReceive( @@ -311,6 +320,9 @@ class NotificationBroadcastReceiverHandlerTest { clearMembershipNotificationForRoomLambda.assertions() .isCalledOnce() .with(value(A_SESSION_ID), value(A_ROOM_ID)) + + advanceUntilIdle() + leaveRoom.assertions() .isCalledOnce() .with() @@ -329,19 +341,27 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test send reply`() = runTest { val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } - val replyMessage = lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } + val replyMessage = + lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } val liveTimeline = FakeTimeline().apply { sendMessageLambda = sendMessage replyMessageLambda = replyMessage } - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline, - getUpdatedMemberResult = { Result.success(aRoomMember()) }, - ) - val onNotifiableEventReceivedResult = lambdaRecorder { _ -> } - val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) + baseRoom = FakeBaseRoom(getUpdatedMemberResult = { Result.success(aRoomMember()) }), + ).apply { + givenRoomInfo( + aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) + ) + } + val onNotifiableEventsReceivedResult = lambdaRecorder, Unit> { _ -> } + val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceivedResult = onNotifiableEventsReceivedResult) val sut = createNotificationBroadcastReceiverHandler( - matrixRoom = matrixRoom, + joinedRoom = joinedRoom, onNotifiableEventReceived = onNotifiableEventReceived, replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE) ) @@ -351,11 +371,11 @@ class NotificationBroadcastReceiverHandlerTest { roomId = A_ROOM_ID, ), ) - runCurrent() + advanceUntilIdle() sendMessage.assertions() .isCalledOnce() .with(value(A_MESSAGE), value(null), value(emptyList())) - onNotifiableEventReceivedResult.assertions() + onNotifiableEventsReceivedResult.assertions() .isCalledOnce() replyMessage.assertions() .isNeverCalled() @@ -367,11 +387,11 @@ class NotificationBroadcastReceiverHandlerTest { val liveTimeline = FakeTimeline().apply { sendMessageLambda = sendMessage } - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline ) val sut = createNotificationBroadcastReceiverHandler( - matrixRoom = matrixRoom, + joinedRoom = joinedRoom, replyMessageExtractor = FakeReplyMessageExtractor(" "), ) sut.onReceive( @@ -388,19 +408,27 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test send reply to thread`() = runTest { val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } - val replyMessage = lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } + val replyMessage = + lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } val liveTimeline = FakeTimeline().apply { sendMessageLambda = sendMessage replyMessageLambda = replyMessage } - val matrixRoom = FakeMatrixRoom( + val joinedRoom = FakeJoinedRoom( liveTimeline = liveTimeline, - getUpdatedMemberResult = { Result.success(aRoomMember()) }, - ) - val onNotifiableEventReceivedResult = lambdaRecorder { _ -> } - val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) + baseRoom = FakeBaseRoom(getUpdatedMemberResult = { Result.success(aRoomMember()) }), + ).apply { + givenRoomInfo( + aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) + ) + } + val onNotifiableEventsReceivedResult = lambdaRecorder, Unit> { _ -> } + val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceivedResult = onNotifiableEventsReceivedResult) val sut = createNotificationBroadcastReceiverHandler( - matrixRoom = matrixRoom, + joinedRoom = joinedRoom, onNotifiableEventReceived = onNotifiableEventReceived, replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE) ) @@ -408,17 +436,24 @@ class NotificationBroadcastReceiverHandlerTest { createIntent( action = actionIds.smartReply, roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, threadId = A_THREAD_ID, ), ) runCurrent() sendMessage.assertions() .isNeverCalled() - onNotifiableEventReceivedResult.assertions() + onNotifiableEventsReceivedResult.assertions() .isCalledOnce() replyMessage.assertions() .isCalledOnce() - .with(value(A_THREAD_ID.asEventId()), value(A_MESSAGE), value(null), value(emptyList()), value(true)) + .with( + value(replyInThread(eventId = AN_EVENT_ID, explicitReply = false)), + value(A_MESSAGE), + value(null), + value(emptyList()), + value(true) + ) } private fun createIntent( @@ -435,10 +470,10 @@ class NotificationBroadcastReceiverHandlerTest { } private fun TestScope.createNotificationBroadcastReceiverHandler( - matrixRoom: FakeMatrixRoom? = FakeMatrixRoom(), - joinRoom: (RoomId) -> Result = { lambdaError() }, + joinedRoom: FakeJoinedRoom? = FakeJoinedRoom(), + joinRoom: (RoomId) -> Result = { lambdaError() }, matrixClient: MatrixClient? = FakeMatrixClient().apply { - givenGetRoomResult(A_ROOM_ID, matrixRoom) + givenGetRoomResult(A_ROOM_ID, joinedRoom) joinRoomLambda = joinRoom }, sessionPreferencesStore: SessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(), @@ -447,6 +482,7 @@ class NotificationBroadcastReceiverHandlerTest { onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(), stringProvider: StringProvider = FakeStringProvider(), replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(), + activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(), ): NotificationBroadcastReceiverHandler { return NotificationBroadcastReceiverHandler( appCoroutineScope = this, @@ -464,6 +500,7 @@ class NotificationBroadcastReceiverHandlerTest { onNotifiableEventReceived = onNotifiableEventReceived, stringProvider = stringProvider, replyMessageExtractor = replyMessageExtractor, + activeRoomsHolder = activeRoomsHolder, ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt index 6bb718092ac..a6b8bc9985b 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl.notifications.channels import android.os.Build import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationManagerCompat -import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.mockk.every @@ -24,7 +23,7 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) class NotificationChannelsTest { @Test - @Config(sdk = [Build.VERSION_CODES.O]) + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) fun `init - creates notification channels and migrates old ones`() { val notificationManager = mockk(relaxed = true) { every { notificationChannels } returns emptyList() @@ -65,7 +64,6 @@ class NotificationChannelsTest { private fun createNotificationChannels( notificationManager: NotificationManagerCompat = mockk(relaxed = true), ) = DefaultNotificationChannels( - context = InstrumentationRegistry.getInstrumentation().targetContext, notificationManager = notificationManager, stringProvider = FakeStringProvider(), ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt index 67f00274832..155fcd108f9 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt @@ -331,5 +331,5 @@ fun createNotificationCreator( fun createNotificationChannels(): NotificationChannels { val context = RuntimeEnvironment.getApplication() - return DefaultNotificationChannels(context, NotificationManagerCompat.from(context), FakeStringProvider("")) + return DefaultNotificationChannels(NotificationManagerCompat.from(context), FakeStringProvider("")) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt index 6d0c2caf2d8..3cbed6e8bce 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.push.impl.notifications.fake import android.app.Notification import android.graphics.Bitmap -import coil.ImageLoader +import coil3.ImageLoader import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt index f0410d6a098..fed6e3c7a39 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.push.impl.notifications.fake -import coil.ImageLoader +import coil3.ImageLoader import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.NotificationDataFactory import io.element.android.libraries.push.impl.notifications.OneShotNotification diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt index f2ca458c9e2..c531735a1e9 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.push.impl.notifications.fake import android.app.Notification -import coil.ImageLoader +import coil3.ImageLoader import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt index 2c8583bb614..1b08f7b14c4 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt @@ -34,7 +34,7 @@ class DefaultOnRedactedEventReceivedTest { val sut = createDefaultOnRedactedEventReceived( getMessageNotificationsForRoomResult = { _, _ -> emptyList() } ) - sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null)) + sut.onRedactedEventsReceived(listOf(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))) } @Test @@ -48,7 +48,7 @@ class DefaultOnRedactedEventReceivedTest { ) } ) - sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null)) + sut.onRedactedEventsReceived(listOf(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))) } private fun TestScope.createDefaultOnRedactedEventReceived( 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 b0ef3af9ff4..64a3b87aa73 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 @@ -28,7 +28,12 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.push.impl.history.FakePushHistoryService +import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver +import io.element.android.libraries.push.impl.notifications.NotificationEventRequest +import io.element.android.libraries.push.impl.notifications.NotificationResolverQueue +import io.element.android.libraries.push.impl.notifications.ResolvingException import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent @@ -42,24 +47,55 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret +import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.matching import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Test import java.time.Instant +import kotlin.time.Duration.Companion.milliseconds + +private const val A_PUSHER_INFO = "info" class DefaultPushHandlerTest { + @Test + fun `check handleInvalid behavior`() = runTest { + val incrementPushCounterResult = lambdaRecorder {} + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) + val defaultPushHandler = createDefaultPushHandler( + incrementPushCounterResult = incrementPushCounterResult, + pushHistoryService = pushHistoryService, + ) + defaultPushHandler.handleInvalid(A_PUSHER_INFO, "data") + incrementPushCounterResult.assertions() + .isCalledOnce() + onPushReceivedResult.assertions() + .isCalledOnce() + .with(value(A_PUSHER_INFO), value(null), value(null), value(null), value(false), value(false), value("Invalid or ignored push data:\ndata")) + } + @Test fun `when classical PushData is received, the notification drawer is informed`() = runTest { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = - lambdaRecorder { _, _, _ -> - ResolvedPushEvent.Event(aNotifiableMessageEvent) + lambdaRecorder, Result>>> { _, _ -> + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) + Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent)))) } - val onNotifiableEventReceived = lambdaRecorder {} + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val incrementPushCounterResult = lambdaRecorder {} + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) val aPushData = PushData( eventId = AN_EVENT_ID, roomId = A_ROOM_ID, @@ -67,22 +103,28 @@ class DefaultPushHandlerTest { clientSecret = A_SECRET, ) val defaultPushHandler = createDefaultPushHandler( - onNotifiableEventReceived = onNotifiableEventReceived, - notifiableEventResult = notifiableEventResult, + onNotifiableEventsReceived = onNotifiableEventsReceived, + notifiableEventsResult = notifiableEventResult, pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { A_USER_ID } ), - incrementPushCounterResult = incrementPushCounterResult + incrementPushCounterResult = incrementPushCounterResult, + pushHistoryService = pushHistoryService, ) - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) + + advanceTimeBy(300.milliseconds) + incrementPushCounterResult.assertions() .isCalledOnce() notifiableEventResult.assertions() .isCalledOnce() - .with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) - onNotifiableEventReceived.assertions() + .with(value(A_USER_ID), any()) + onNotifiableEventsReceived.assertions() + .isCalledOnce() + .with(value(listOf(aNotifiableMessageEvent))) + onPushReceivedResult.assertions() .isCalledOnce() - .with(value(aNotifiableMessageEvent)) } @Test @@ -90,10 +132,11 @@ class DefaultPushHandlerTest { runTest { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = - lambdaRecorder { _, _, _ -> - ResolvedPushEvent.Event(aNotifiableMessageEvent) + lambdaRecorder, Result>>> { _, _ -> + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) + Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent)))) } - val onNotifiableEventReceived = lambdaRecorder {} + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val incrementPushCounterResult = lambdaRecorder {} val aPushData = PushData( eventId = AN_EVENT_ID, @@ -101,24 +144,34 @@ class DefaultPushHandlerTest { unread = 0, clientSecret = A_SECRET, ) + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) val defaultPushHandler = createDefaultPushHandler( - onNotifiableEventReceived = onNotifiableEventReceived, - notifiableEventResult = notifiableEventResult, + onNotifiableEventsReceived = onNotifiableEventsReceived, + notifiableEventsResult = notifiableEventResult, pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { A_USER_ID } ), userPushStore = FakeUserPushStore().apply { setNotificationEnabledForDevice(false) }, - incrementPushCounterResult = incrementPushCounterResult + incrementPushCounterResult = incrementPushCounterResult, + pushHistoryService = pushHistoryService, ) - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) + + advanceTimeBy(300.milliseconds) + incrementPushCounterResult.assertions() .isCalledOnce() notifiableEventResult.assertions() .isCalledOnce() - onNotifiableEventReceived.assertions() + onNotifiableEventsReceived.assertions() .isNeverCalled() + onPushReceivedResult.assertions() + .isCalledOnce() } @Test @@ -126,10 +179,11 @@ class DefaultPushHandlerTest { runTest { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = - lambdaRecorder { _, _, _ -> - ResolvedPushEvent.Event(aNotifiableMessageEvent) + lambdaRecorder, Result>>> { _, _ -> + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) + Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent)))) } - val onNotifiableEventReceived = lambdaRecorder {} + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val incrementPushCounterResult = lambdaRecorder {} val aPushData = PushData( eventId = AN_EVENT_ID, @@ -137,26 +191,36 @@ class DefaultPushHandlerTest { unread = 0, clientSecret = A_SECRET, ) + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) val defaultPushHandler = createDefaultPushHandler( - onNotifiableEventReceived = onNotifiableEventReceived, - notifiableEventResult = notifiableEventResult, + onNotifiableEventsReceived = onNotifiableEventsReceived, + notifiableEventsResult = notifiableEventResult, pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { null } ), matrixAuthenticationService = FakeMatrixAuthenticationService().apply { getLatestSessionIdLambda = { A_USER_ID } }, - incrementPushCounterResult = incrementPushCounterResult + incrementPushCounterResult = incrementPushCounterResult, + pushHistoryService = pushHistoryService, ) - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) + + advanceTimeBy(300.milliseconds) + incrementPushCounterResult.assertions() .isCalledOnce() notifiableEventResult.assertions() .isCalledOnce() - .with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) - onNotifiableEventReceived.assertions() + .with(value(A_USER_ID), any()) + onNotifiableEventsReceived.assertions() + .isCalledOnce() + .with(value(listOf(aNotifiableMessageEvent))) + onPushReceivedResult.assertions() .isCalledOnce() - .with(value(aNotifiableMessageEvent)) } @Test @@ -164,10 +228,11 @@ class DefaultPushHandlerTest { runTest { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = - lambdaRecorder { _, _, _ -> - ResolvedPushEvent.Event(aNotifiableMessageEvent) + lambdaRecorder, Result>>> { _, _ -> + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) + Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent)))) } - val onNotifiableEventReceived = lambdaRecorder {} + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val incrementPushCounterResult = lambdaRecorder {} val aPushData = PushData( eventId = AN_EVENT_ID, @@ -175,32 +240,65 @@ class DefaultPushHandlerTest { unread = 0, clientSecret = A_SECRET, ) + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) val defaultPushHandler = createDefaultPushHandler( - onNotifiableEventReceived = onNotifiableEventReceived, - notifiableEventResult = notifiableEventResult, + onNotifiableEventsReceived = onNotifiableEventsReceived, + notifiableEventsResult = notifiableEventResult, pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { null } ), matrixAuthenticationService = FakeMatrixAuthenticationService().apply { getLatestSessionIdLambda = { null } }, - incrementPushCounterResult = incrementPushCounterResult + incrementPushCounterResult = incrementPushCounterResult, + pushHistoryService = pushHistoryService, ) - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) incrementPushCounterResult.assertions() .isCalledOnce() notifiableEventResult.assertions() .isNeverCalled() - onNotifiableEventReceived.assertions() + onNotifiableEventsReceived.assertions() .isNeverCalled() + onPushReceivedResult.assertions() + .isCalledOnce() } @Test - fun `when classical PushData is received, but not able to resolve the event, nothing happen`() = + fun `when classical PushData is received, but a failure occurs (session not found), nothing happen`() { + `test notification resolver failure`( + notificationResolveResult = { _ -> + Result.failure(ResolvingException("Unable to restore session")) + }, + shouldSetOptimizationBatteryBanner = false, + ) + } + + @Test + fun `when classical PushData is received, but not able to resolve the event, the banner to disable battery optimization will be displayed`() { + `test notification resolver failure`( + notificationResolveResult = { requests: List -> + Result.success( + requests.associateWith { Result.failure(ResolvingException("Unable to resolve event")) } + ) + }, + shouldSetOptimizationBatteryBanner = true, + ) + } + + private fun `test notification resolver failure`( + notificationResolveResult: (List) -> Result>>, + shouldSetOptimizationBatteryBanner: Boolean, + ) { runTest { val notifiableEventResult = - lambdaRecorder { _, _, _ -> null } - val onNotifiableEventReceived = lambdaRecorder {} + lambdaRecorder, Result>>> { _, requests -> + notificationResolveResult(requests) + } + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val incrementPushCounterResult = lambdaRecorder {} val aPushData = PushData( eventId = AN_EVENT_ID, @@ -208,9 +306,14 @@ class DefaultPushHandlerTest { unread = 0, clientSecret = A_SECRET, ) + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) + val showBatteryOptimizationBannerResult = lambdaRecorder {} val defaultPushHandler = createDefaultPushHandler( - onNotifiableEventReceived = onNotifiableEventReceived, - notifiableEventResult = notifiableEventResult, + onNotifiableEventsReceived = onNotifiableEventsReceived, + notifiableEventsResult = notifiableEventResult, buildMeta = aBuildMeta( // Also test `lowPrivacyLoggingEnabled = false` here lowPrivacyLoggingEnabled = false @@ -218,17 +321,35 @@ class DefaultPushHandlerTest { pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { A_USER_ID } ), - incrementPushCounterResult = incrementPushCounterResult + incrementPushCounterResult = incrementPushCounterResult, + mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore( + showBatteryOptimizationBannerResult = showBatteryOptimizationBannerResult, + ), + pushHistoryService = pushHistoryService, ) - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) + + advanceTimeBy(300.milliseconds) + incrementPushCounterResult.assertions() .isCalledOnce() notifiableEventResult.assertions() .isCalledOnce() - .with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) - onNotifiableEventReceived.assertions() + .with(value(A_USER_ID), any()) + onNotifiableEventsReceived.assertions() .isNeverCalled() + onPushReceivedResult.assertions() + .isCalledOnce() + .with(any(), value(AN_EVENT_ID), value(A_ROOM_ID), value(A_USER_ID), value(false), value(true), any()) + showBatteryOptimizationBannerResult.assertions().let { + if (shouldSetOptimizationBatteryBanner) { + it.isCalledOnce() + } else { + it.isNeverCalled() + } + } } + } @Test fun `when ringing call PushData is received, the incoming call will be handled`() = runTest { @@ -238,23 +359,51 @@ class DefaultPushHandlerTest { unread = 0, clientSecret = A_SECRET, ) - val handleIncomingCallLambda = lambdaRecorder { _, _, _, _, _, _, _ -> } + val handleIncomingCallLambda = lambdaRecorder< + CallType.RoomCall, + EventId, + UserId, + String?, + String?, + String?, + String, + String?, + Unit, + > { _, _, _, _, _, _, _, _ -> } val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda) - val onNotifiableEventReceived = lambdaRecorder {} + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) val defaultPushHandler = createDefaultPushHandler( elementCallEntryPoint = elementCallEntryPoint, - notifiableEventResult = { _, _, _ -> - ResolvedPushEvent.Event(aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli())) + notifiableEventsResult = { _, _ -> + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) + Result.success( + mapOf( + request to Result.success( + ResolvedPushEvent.Event( + aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()) + ) + ) + ) + ) }, incrementPushCounterResult = {}, pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { A_USER_ID } ), - onNotifiableEventReceived = onNotifiableEventReceived, + onNotifiableEventsReceived = onNotifiableEventsReceived, + pushHistoryService = pushHistoryService, ) - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) + + advanceTimeBy(300.milliseconds) + handleIncomingCallLambda.assertions().isCalledOnce() - onNotifiableEventReceived.assertions().isCalledOnce() + onNotifiableEventsReceived.assertions().isNeverCalled() + onPushReceivedResult.assertions().isCalledOnce() } @Test @@ -265,24 +414,43 @@ class DefaultPushHandlerTest { unread = 0, clientSecret = A_SECRET, ) - val onNotifiableEventReceived = lambdaRecorder {} - val handleIncomingCallLambda = lambdaRecorder { _, _, _, _, _, _, _ -> } + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} + val handleIncomingCallLambda = lambdaRecorder< + CallType.RoomCall, + EventId, + UserId, + String?, + String?, + String?, + String, + String?, + Unit, + > { _, _, _, _, _, _, _, _ -> } val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda) + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) val defaultPushHandler = createDefaultPushHandler( elementCallEntryPoint = elementCallEntryPoint, - onNotifiableEventReceived = onNotifiableEventReceived, - notifiableEventResult = { _, _, _ -> - ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY)) + onNotifiableEventsReceived = onNotifiableEventsReceived, + notifiableEventsResult = { _, _ -> + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) + Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY))))) }, incrementPushCounterResult = {}, pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { A_USER_ID } ), + pushHistoryService = pushHistoryService, ) - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) + + advanceTimeBy(300.milliseconds) handleIncomingCallLambda.assertions().isNeverCalled() - onNotifiableEventReceived.assertions().isCalledOnce() + onNotifiableEventsReceived.assertions().isCalledOnce() + onPushReceivedResult.assertions().isCalledOnce() } @Test @@ -293,14 +461,29 @@ class DefaultPushHandlerTest { unread = 0, clientSecret = A_SECRET, ) - val onNotifiableEventReceived = lambdaRecorder {} - val handleIncomingCallLambda = lambdaRecorder { _, _, _, _, _, _, _ -> } + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} + val handleIncomingCallLambda = lambdaRecorder< + CallType.RoomCall, + EventId, + UserId, + String?, + String?, + String?, + String, + String?, + Unit, + > { _, _, _, _, _, _, _, _ -> } val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda) + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) val defaultPushHandler = createDefaultPushHandler( elementCallEntryPoint = elementCallEntryPoint, - onNotifiableEventReceived = onNotifiableEventReceived, - notifiableEventResult = { _, _, _ -> - ResolvedPushEvent.Event(aNotifiableCallEvent()) + onNotifiableEventsReceived = onNotifiableEventsReceived, + notifiableEventsResult = { _, _ -> + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) + Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableCallEvent())))) }, incrementPushCounterResult = {}, userPushStore = FakeUserPushStore().apply { @@ -309,10 +492,15 @@ class DefaultPushHandlerTest { pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { A_USER_ID } ), + pushHistoryService = pushHistoryService, ) - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) + + advanceTimeBy(300.milliseconds) + handleIncomingCallLambda.assertions().isCalledOnce() - onNotifiableEventReceived.assertions().isCalledOnce() + onNotifiableEventsReceived.assertions().isNeverCalled() + onPushReceivedResult.assertions().isCalledOnce() } @Test @@ -329,21 +517,34 @@ class DefaultPushHandlerTest { redactedEventId = AN_EVENT_ID_2, reason = null ) - val onRedactedEventReceived = lambdaRecorder { } + val onRedactedEventReceived = lambdaRecorder, Unit> { } val incrementPushCounterResult = lambdaRecorder {} + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) val defaultPushHandler = createDefaultPushHandler( - onRedactedEventReceived = onRedactedEventReceived, + onRedactedEventsReceived = onRedactedEventReceived, incrementPushCounterResult = incrementPushCounterResult, - notifiableEventResult = { _, _, _ -> aRedaction }, + notifiableEventsResult = { _, _ -> + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) + Result.success(mapOf(request to Result.success(aRedaction))) + }, pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { A_USER_ID } ), + pushHistoryService = pushHistoryService, ) - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) + + advanceTimeBy(300.milliseconds) + incrementPushCounterResult.assertions() .isCalledOnce() onRedactedEventReceived.assertions().isCalledOnce() - .with(value(aRedaction)) + .with(value(listOf(aRedaction))) + onPushReceivedResult.assertions() + .isCalledOnce() } @Test @@ -356,21 +557,83 @@ class DefaultPushHandlerTest { clientSecret = A_SECRET, ) val diagnosticPushHandler = DiagnosticPushHandler() + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) val defaultPushHandler = createDefaultPushHandler( diagnosticPushHandler = diagnosticPushHandler, - incrementPushCounterResult = { } + incrementPushCounterResult = { }, + pushHistoryService = pushHistoryService, ) diagnosticPushHandler.state.test { - defaultPushHandler.handle(aPushData) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) awaitItem() } + onPushReceivedResult.assertions() + .isCalledOnce() } - private fun createDefaultPushHandler( - onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() }, - onRedactedEventReceived: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() }, - notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() }, + @Test + fun `when receiving several push notifications at the same time, those are batched before being processed`() = runTest { + val aNotifiableMessageEvent = aNotifiableMessageEvent() + val notifiableEventResult = + lambdaRecorder, Result>>> { _, _ -> + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) + Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent)))) + } + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} + val incrementPushCounterResult = lambdaRecorder {} + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ) + val aPushData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val anotherPushData = PushData( + eventId = AN_EVENT_ID_2, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val defaultPushHandler = createDefaultPushHandler( + onNotifiableEventsReceived = onNotifiableEventsReceived, + notifiableEventsResult = notifiableEventResult, + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { A_USER_ID } + ), + incrementPushCounterResult = incrementPushCounterResult, + pushHistoryService = pushHistoryService, + ) + defaultPushHandler.handle(aPushData, A_PUSHER_INFO) + defaultPushHandler.handle(anotherPushData, A_PUSHER_INFO) + + advanceTimeBy(300.milliseconds) + + incrementPushCounterResult.assertions() + .isCalledExactly(2) + notifiableEventResult.assertions() + .isCalledOnce() + .with(value(A_USER_ID), matching> { requests -> + requests.size == 2 && requests.first().eventId == AN_EVENT_ID && requests.last().eventId == AN_EVENT_ID_2 + }) + onNotifiableEventsReceived.assertions() + .isCalledOnce() + onPushReceivedResult.assertions() + .isCalledExactly(2) + } + + private fun TestScope.createDefaultPushHandler( + onNotifiableEventsReceived: (List) -> Unit = { lambdaError() }, + onRedactedEventsReceived: (List) -> Unit = { lambdaError() }, + notifiableEventsResult: (SessionId, List) -> Result>> = + { _, _ -> lambdaError() }, incrementPushCounterResult: () -> Unit = { lambdaError() }, + mutableBatteryOptimizationStore: MutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(), userPushStore: UserPushStore = FakeUserPushStore(), pushClientSecret: PushClientSecret = FakePushClientSecret(), buildMeta: BuildMeta = aBuildMeta(), @@ -378,16 +641,17 @@ class DefaultPushHandlerTest { diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(), elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(), notificationChannels: FakeNotificationChannels = FakeNotificationChannels(), + pushHistoryService: PushHistoryService = FakePushHistoryService(), ): DefaultPushHandler { return DefaultPushHandler( - onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived), - onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived), - notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult), + onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived), + onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventsReceived), incrementPushDataStore = object : IncrementPushDataStore { override suspend fun incrementPushCounter() { incrementPushCounterResult() } }, + mutableBatteryOptimizationStore = mutableBatteryOptimizationStore, userPushStoreFactory = FakeUserPushStoreFactory { userPushStore }, pushClientSecret = pushClientSecret, buildMeta = buildMeta, @@ -395,6 +659,9 @@ class DefaultPushHandlerTest { diagnosticPushHandler = diagnosticPushHandler, elementCallEntryPoint = elementCallEntryPoint, notificationChannels = notificationChannels, + pushHistoryService = pushHistoryService, + resolverQueue = NotificationResolverQueue(notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventsResult), backgroundScope), + appCoroutineScope = backgroundScope, ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt new file mode 100644 index 00000000000..d4d3992f1e1 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt @@ -0,0 +1,28 @@ +/* + * 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.push.impl.push + +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeMutableBatteryOptimizationStore( + private val showBatteryOptimizationBannerResult: () -> Unit = { lambdaError() }, + private val onOptimizationBannerDismissedResult: () -> Unit = { lambdaError() }, + private val resetResult: () -> Unit = { lambdaError() }, +) : MutableBatteryOptimizationStore { + override suspend fun showBatteryOptimizationBanner() { + showBatteryOptimizationBannerResult() + } + + override suspend fun onOptimizationBannerDismissed() { + onOptimizationBannerDismissedResult() + } + + override suspend fun reset() { + resetResult() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt index 0de4094809a..055b1f322e5 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt @@ -11,9 +11,9 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven import io.element.android.tests.testutils.lambda.lambdaError class FakeOnNotifiableEventReceived( - private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit = { lambdaError() }, + private val onNotifiableEventsReceivedResult: (List) -> Unit = { lambdaError() }, ) : OnNotifiableEventReceived { - override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { - onNotifiableEventReceivedResult(notifiableEvent) + override fun onNotifiableEventsReceived(notifiableEvents: List) { + onNotifiableEventsReceivedResult(notifiableEvents) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnRedactedEventReceived.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnRedactedEventReceived.kt index 96e76f6da0a..b5a3731830e 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnRedactedEventReceived.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnRedactedEventReceived.kt @@ -11,9 +11,9 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv import io.element.android.tests.testutils.lambda.lambdaError class FakeOnRedactedEventReceived( - private val onRedactedEventReceivedResult: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() }, + private val onRedactedEventsReceivedResult: (List) -> Unit = { lambdaError() }, ) : OnRedactedEventReceived { - override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) { - onRedactedEventReceivedResult(redaction) + override fun onRedactedEventsReceived(redactions: List) { + onRedactedEventsReceivedResult(redactions) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEventTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEventTest.kt index 08b14e5680a..e1c4b33acf5 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEventTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEventTest.kt @@ -7,46 +7,44 @@ package io.element.android.libraries.push.impl.push +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.sync.SyncState -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.sync.FakeSyncService -import io.element.android.libraries.matrix.test.timeline.FakeTimeline -import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Test +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.seconds class SyncOnNotifiableEventTest { - private val timelineItems = MutableStateFlow>(emptyList()) private val startSyncLambda = lambdaRecorder> { Result.success(Unit) } private val stopSyncLambda = lambdaRecorder> { Result.success(Unit) } private val subscribeToSyncLambda = lambdaRecorder { } - private val liveTimeline = FakeTimeline( - timelineItems = timelineItems, - ) - private val room = FakeMatrixRoom( - roomId = A_ROOM_ID, - liveTimeline = liveTimeline, - subscribeToSyncLambda = subscribeToSyncLambda + private val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomId = A_ROOM_ID, + subscribeToSyncLambda = subscribeToSyncLambda, + ), ) private val syncService = FakeSyncService(SyncState.Idle).also { it.startSyncLambda = startSyncLambda @@ -66,95 +64,109 @@ class SyncOnNotifiableEventTest { fun `when feature flag is disabled, nothing happens`() = runTest { val sut = createSyncOnNotifiableEvent(client = client, isSyncOnPushEnabled = false) - sut(notifiableEvent) + sut(listOf(notifiableEvent)) assert(startSyncLambda).isNeverCalled() assert(stopSyncLambda).isNeverCalled() assert(subscribeToSyncLambda).isNeverCalled() } + @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `when feature flag is enabled, a ringing call starts and stops the sync`() = runTest { - val sut = createSyncOnNotifiableEvent(client = client, isAppInForeground = false, isSyncOnPushEnabled = true) + fun `when feature flag is enabled, a ringing call waits until the room is in 'in-call' state`() = runTest { + val appForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = false, + ) + val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true) - sut(incomingCallNotifiableEvent) + val unlocked = AtomicBoolean(false) + launch { + advanceTimeBy(1.seconds) + unlocked.set(true) + room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) + } + sut(listOf(incomingCallNotifiableEvent)) - assert(startSyncLambda).isCalledOnce() - assert(stopSyncLambda).isCalledOnce() - assert(subscribeToSyncLambda).isCalledOnce() + // The process was completed before the timeout + assertThat(unlocked.get()).isTrue() } + @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `when feature flag is disabled, a ringing call starts and stops the sync`() = runTest { - val sut = createSyncOnNotifiableEvent(client = client, isAppInForeground = false, isSyncOnPushEnabled = false) + fun `when feature flag is enabled, a ringing call waits until the room is in 'in-call' state or timeouts`() = runTest { + val appForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = false, + ) + val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true) - sut(incomingCallNotifiableEvent) + val unlocked = AtomicBoolean(false) + launch { + advanceTimeBy(120.seconds) + unlocked.set(true) + room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) + } + sut(listOf(incomingCallNotifiableEvent)) - assert(startSyncLambda).isCalledOnce() - assert(stopSyncLambda).isCalledOnce() - assert(subscribeToSyncLambda).isCalledOnce() + // Didn't unlock before the timeout + assertThat(unlocked.get()).isFalse() } @Test - fun `when feature flag is enabled and app is in foreground, sync is not started`() = runTest { - val sut = createSyncOnNotifiableEvent(client = client, isAppInForeground = true, isSyncOnPushEnabled = true) + fun `when feature flag is enabled and app is in background, sync is started and stopped`() = runTest { + val appForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = false, + ) + val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true) - sut(notifiableEvent) - sut(incomingCallNotifiableEvent) + appForegroundStateService.isSyncingNotificationEvent.test { + syncService.emitSyncState(SyncState.Running) + sut(listOf(notifiableEvent)) - assert(startSyncLambda).isNeverCalled() - assert(stopSyncLambda).isNeverCalled() - assert(subscribeToSyncLambda).isCalledExactly(2) + // It's initially false + assertThat(awaitItem()).isFalse() + // Then it becomes true when we receive the push + assertThat(awaitItem()).isTrue() + // It becomes false once when the push is processed + assertThat(awaitItem()).isFalse() + + ensureAllEventsConsumed() + } } @Test - fun `when feature flag is enabled and app is in background, sync is started and stopped`() = runTest { - val sut = createSyncOnNotifiableEvent(client = client, isAppInForeground = false, isSyncOnPushEnabled = true) - - timelineItems.emit( - listOf(MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())) + fun `when feature flag is enabled and app is in background, running multiple times only call once`() = runTest { + val appForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = false, ) - syncService.emitSyncState(SyncState.Running) - sut(notifiableEvent) + val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true) - assert(startSyncLambda).isCalledOnce() - assert(stopSyncLambda).isCalledOnce() - assert(subscribeToSyncLambda).isCalledOnce() - } + appForegroundStateService.isSyncingNotificationEvent.test { + launch { sut(listOf(notifiableEvent)) } + launch { sut(listOf(notifiableEvent)) } - @Test - fun `when feature flag is enabled and app is in background, running multiple time only call once`() = runTest { - val sut = createSyncOnNotifiableEvent(client = client, isAppInForeground = false, isSyncOnPushEnabled = true) - - coroutineScope { - launch { sut(notifiableEvent) } - launch { sut(notifiableEvent) } - launch { - delay(1) - timelineItems.emit( - listOf(MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())) - ) - } - } + // It's initially false + assertThat(awaitItem()).isFalse() + // Then it becomes true once, for the first received push + assertThat(awaitItem()).isTrue() + // It becomes false once all pushes are processed + assertThat(awaitItem()).isFalse() - assert(startSyncLambda).isCalledOnce() - assert(stopSyncLambda).isCalledOnce() - assert(subscribeToSyncLambda).isCalledExactly(2) + ensureAllEventsConsumed() + } } private fun TestScope.createSyncOnNotifiableEvent( client: MatrixClient = FakeMatrixClient(), isSyncOnPushEnabled: Boolean = true, - isAppInForeground: Boolean = true, + appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService( + initialForegroundValue = true, + ), ): SyncOnNotifiableEvent { val featureFlagService = FakeFeatureFlagService( initialState = mapOf( FeatureFlags.SyncOnPush.key to isSyncOnPushEnabled ) ) - val appForegroundStateService = FakeAppForegroundStateService( - initialValue = isAppInForeground - ) val matrixClientProvider = FakeMatrixClientProvider { Result.success(client) } return SyncOnNotifiableEvent( matrixClientProvider = matrixClientProvider, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt new file mode 100644 index 00000000000..41b0546f33b --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt @@ -0,0 +1,41 @@ +/* + * 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.push.impl.store + +import io.element.android.libraries.push.api.history.PushHistoryItem +import io.element.android.tests.testutils.lambda.lambdaError +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class InMemoryPushDataStore( + initialPushCounter: Int = 0, + initialShouldDisplayBatteryOptimizationBanner: Boolean = false, + initialPushHistoryItems: List = emptyList(), + private val resetResult: () -> Unit = { lambdaError() } +) : PushDataStore { + private val mutablePushCounterFlow = MutableStateFlow(initialPushCounter) + override val pushCounterFlow: Flow = mutablePushCounterFlow.asStateFlow() + + private val mutableShouldDisplayBatteryOptimizationBannerFlow = MutableStateFlow(initialShouldDisplayBatteryOptimizationBanner) + override val shouldDisplayBatteryOptimizationBannerFlow: Flow = mutableShouldDisplayBatteryOptimizationBannerFlow.asStateFlow() + + private val mutablePushHistoryItemsFlow = MutableStateFlow(initialPushHistoryItems) + + override fun getPushHistoryItemsFlow(): Flow> { + return mutablePushHistoryItemsFlow.asStateFlow() + } + + suspend fun emitPushHistoryItems(items: List) { + mutablePushHistoryItemsFlow.emit(items) + } + + override suspend fun reset() { + resetResult() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTestTest.kt index cc4df51b556..8947b646a69 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTestTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTestTest.kt @@ -53,7 +53,6 @@ class PushProvidersTestTest { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) val lastItem = awaitItem() assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) - assertThat(lastItem.description).contains("2") assertThat(lastItem.description).contains("foo") assertThat(lastItem.description).contains("bar") } diff --git a/libraries/push/test/build.gradle.kts b/libraries/push/test/build.gradle.kts index 7f178bc1f45..fa0db0738b2 100644 --- a/libraries/push/test/build.gradle.kts +++ b/libraries/push/test/build.gradle.kts @@ -15,10 +15,10 @@ android { dependencies { api(projects.libraries.push.api) + api(projects.libraries.pushproviders.api) implementation(projects.libraries.push.impl) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) - implementation(projects.libraries.pushproviders.api) implementation(projects.tests.testutils) implementation(libs.androidx.core) implementation(libs.coil.compose) diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt index b3fac42d326..553ac094655 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.push.test import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.api.history.PushHistoryItem import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.tests.testutils.lambda.lambdaError @@ -26,6 +27,8 @@ class FakePushService( private val currentPushProvider: () -> PushProvider? = { availablePushProviders.firstOrNull() }, private val selectPushProviderLambda: suspend (SessionId, PushProvider) -> Unit = { _, _ -> lambdaError() }, private val setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() }, + private val resetPushHistoryResult: () -> Unit = { lambdaError() }, + private val resetBatteryOptimizationStateResult: () -> Unit = { lambdaError() }, ) : PushService { override suspend fun getCurrentPushProvider(): PushProvider? { return registeredPushProvider ?: currentPushProvider() @@ -68,4 +71,30 @@ class FakePushService( override suspend fun testPush(): Boolean = simulateLongTask { testPushBlock() } + + private val pushHistoryItemsFlow = MutableStateFlow>(emptyList()) + + override fun getPushHistoryItemsFlow(): Flow> { + return pushHistoryItemsFlow + } + + fun emitPushHistoryItems(items: List) { + pushHistoryItemsFlow.value = items + } + + private val pushCounterFlow = MutableStateFlow(0) + + override val pushCounter: Flow = pushCounterFlow + + fun emitPushCounter(counter: Int) { + pushCounterFlow.value = counter + } + + override suspend fun resetPushHistory() = simulateLongTask { + resetPushHistoryResult() + } + + override suspend fun resetBatteryOptimizationState() { + resetBatteryOptimizationStateResult() + } } diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt index d77bda026d9..947d57ee7e0 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt @@ -11,11 +11,14 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.push.impl.notifications.CallNotificationEventResolver import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.tests.testutils.lambda.lambdaError class FakeCallNotificationEventResolver( - var resolveEventLambda: (sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean) -> NotifiableEvent? = { _, _, _ -> null }, + var resolveEventLambda: (sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean) -> Result = { _, _, _ -> + lambdaError() + }, ) : CallNotificationEventResolver { - override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? { + override suspend fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): Result { return resolveEventLambda(sessionId, notificationData, forceNotify) } } diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt index 814b9a88bde..067376cfbc9 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt @@ -9,12 +9,11 @@ package io.element.android.libraries.push.test.notifications import android.graphics.Color import android.graphics.drawable.ColorDrawable -import coil.ImageLoader -import coil.annotation.ExperimentalCoilApi -import coil.test.FakeImageLoaderEngine +import coil3.ImageLoader +import coil3.test.FakeImageLoaderEngine +import coil3.test.intercept import org.robolectric.RuntimeEnvironment -@OptIn(ExperimentalCoilApi::class) class FakeImageLoader { private val coilRequests = mutableListOf() diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt index e05619e4301..4c92dc8c186 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.push.test.notifications -import coil.ImageLoader +import coil3.ImageLoader import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt index c02797c89fb..d8551dd1e15 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.push.test.notifications.push import android.graphics.Bitmap import androidx.core.graphics.drawable.IconCompat -import coil.ImageLoader +import coil3.ImageLoader import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader class FakeNotificationBitmapLoader( diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/test/FakePushHandler.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/test/FakePushHandler.kt index 9e0c71442f5..1279d6e5a08 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/test/FakePushHandler.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/test/FakePushHandler.kt @@ -12,9 +12,14 @@ import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.tests.testutils.lambda.lambdaError class FakePushHandler( - private val handleResult: (PushData) -> Unit = { lambdaError() } + private val handleResult: (PushData, String) -> Unit = { _, _ -> lambdaError() }, + private val handleInvalidResult: (String, String) -> Unit = { _, _ -> lambdaError() }, ) : PushHandler { - override suspend fun handle(pushData: PushData) { - handleResult(pushData) + override suspend fun handle(pushData: PushData, providerInfo: String) { + handleResult(pushData, providerInfo) + } + + override suspend fun handleInvalid(providerInfo: String, data: String) { + handleInvalidResult(providerInfo, data) } } diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/Distributor.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/Distributor.kt index c99e5072b74..d6237404447 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/Distributor.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/Distributor.kt @@ -18,4 +18,6 @@ package io.element.android.libraries.pushproviders.api data class Distributor( val value: String, val name: String, -) +) { + val fullName = "$name ($value)" +} diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushHandler.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushHandler.kt index ded2b163b59..a9dfd76c2c8 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushHandler.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushHandler.kt @@ -8,5 +8,13 @@ package io.element.android.libraries.pushproviders.api interface PushHandler { - suspend fun handle(pushData: PushData) + suspend fun handle( + pushData: PushData, + providerInfo: String, + ) + + suspend fun handleInvalid( + providerInfo: String, + data: String, + ) } diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 619db35282f..735de54d111 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -7,6 +7,7 @@ @file:Suppress("UnstableApiUsage") +import config.BuildTimeConfig import extension.setupAnvil plugins { @@ -22,22 +23,14 @@ android { resValue( type = "string", name = "google_app_id", - value = if (isEnterpriseBuild) { - "1:912726360885:android:d273c2077ec3291500427c" - } else { - "1:912726360885:android:d097de99a4c23d2700427c" - } + value = BuildTimeConfig.GOOGLE_APP_ID_RELEASE, ) } getByName("debug") { resValue( type = "string", name = "google_app_id", - value = if (isEnterpriseBuild) { - "1:912726360885:android:f8de9126a94143d300427c" - } else { - "1:912726360885:android:def0a4e454042e9b00427c" - } + value = BuildTimeConfig.GOOGLE_APP_ID_DEBUG, ) } register("nightly") { @@ -46,11 +39,7 @@ android { resValue( type = "string", name = "google_app_id", - value = if (isEnterpriseBuild) { - "1:912726360885:android:3f7e1fe644d99d5a00427c" - } else { - "1:912726360885:android:e17435e0beb0303000427c" - } + value = BuildTimeConfig.GOOGLE_APP_ID_NIGHTLY, ) } } @@ -61,6 +50,7 @@ setupAnvil() dependencies { implementation(libs.dagger) implementation(libs.androidx.corektx) + implementation(projects.features.enterprise.api) implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.libraries.di) @@ -84,6 +74,8 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(libs.test.robolectric) + testImplementation(libs.kotlinx.collections.immutable) + testImplementation(projects.features.enterprise.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushstore.test) diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseGatewayProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseGatewayProvider.kt new file mode 100644 index 00000000000..2b378766bc8 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseGatewayProvider.kt @@ -0,0 +1,26 @@ +/* + * 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.pushproviders.firebase + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +interface FirebaseGatewayProvider { + fun getFirebaseGateway(): String +} + +@ContributesBinding(AppScope::class) +class DefaultFirebaseGatewayProvider @Inject constructor( + private val enterpriseService: EnterpriseService, +) : FirebaseGatewayProvider { + override fun getFirebaseGateway(): String { + return enterpriseService.firebasePushGateway() ?: FirebaseConfig.PUSHER_HTTP_URL + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt index bf616b7de39..e5589af4931 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt @@ -36,6 +36,7 @@ class DefaultFirebaseNewTokenHandler @Inject constructor( private val userPushStoreFactory: UserPushStoreFactory, private val matrixClientProvider: MatrixClientProvider, private val firebaseStore: FirebaseStore, + private val firebaseGatewayProvider: FirebaseGatewayProvider, ) : FirebaseNewTokenHandler { override suspend fun handle(firebaseToken: String) { firebaseStore.storeFcmToken(firebaseToken) @@ -55,7 +56,7 @@ class DefaultFirebaseNewTokenHandler @Inject constructor( .registerPusher( matrixClient = client, pushKey = firebaseToken, - gateway = FirebaseConfig.PUSHER_HTTP_URL, + gateway = firebaseGatewayProvider.getFirebaseGateway(), ) .onFailure { Timber.tag(loggerTag.value).e(it, "Failed to register pusher for session $sessionId") diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt index 0b12e00bbd4..a6be83e8034 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt @@ -27,6 +27,7 @@ class FirebasePushProvider @Inject constructor( private val pusherSubscriber: PusherSubscriber, private val isPlayServiceAvailable: IsPlayServiceAvailable, private val firebaseTokenRotator: FirebaseTokenRotator, + private val firebaseGatewayProvider: FirebaseGatewayProvider, ) : PushProvider { override val index = FirebaseConfig.INDEX override val name = FirebaseConfig.NAME @@ -48,7 +49,7 @@ class FirebasePushProvider @Inject constructor( return pusherSubscriber.registerPusher( matrixClient = matrixClient, pushKey = pushKey, - gateway = FirebaseConfig.PUSHER_HTTP_URL, + gateway = firebaseGatewayProvider.getFirebaseGateway(), ) } @@ -60,7 +61,7 @@ class FirebasePushProvider @Inject constructor( Timber.tag(loggerTag.value).w("Unable to unregister pusher, Firebase token is not known.") Result.success(Unit) } else { - pusherSubscriber.unregisterPusher(matrixClient, pushKey, FirebaseConfig.PUSHER_HTTP_URL) + pusherSubscriber.unregisterPusher(matrixClient, pushKey, firebaseGatewayProvider.getFirebaseGateway()) } } @@ -72,7 +73,7 @@ class FirebasePushProvider @Inject constructor( override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? { return firebaseStore.getFcmToken()?.let { fcmToken -> CurrentUserPushConfig( - url = FirebaseConfig.PUSHER_HTTP_URL, + url = firebaseGatewayProvider.getFirebaseGateway(), pushKey = fcmToken ) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt index d648e3475f7..cf6eb4b2cc4 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.pushproviders.firebase import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import javax.inject.Inject @@ -24,7 +25,7 @@ class DefaultFirebaseTokenRotator @Inject constructor( private val firebaseTokenGetter: FirebaseTokenGetter, ) : FirebaseTokenRotator { override suspend fun rotate(): Result { - return runCatching { + return runCatchingExceptions { firebaseTokenDeleter.delete() firebaseTokenGetter.get() } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt index 5ed3fb95800..8326496dd2d 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.pushproviders.firebase import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import javax.inject.Inject @@ -24,7 +25,7 @@ class DefaultFirebaseTroubleshooter @Inject constructor( private val firebaseTokenGetter: FirebaseTokenGetter, ) : FirebaseTroubleshooter { override suspend fun troubleshoot(): Result { - return runCatching { + return runCatchingExceptions { val token = firebaseTokenGetter.get() newTokenHandler.handle(token) } 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 d03fefcac66..2c138c3cb62 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 @@ -11,6 +11,7 @@ import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage 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.pushproviders.api.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -23,6 +24,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler + @AppCoroutineScope @Inject lateinit var coroutineScope: CoroutineScope override fun onCreate() { @@ -31,20 +33,29 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } override fun onNewToken(token: String) { - Timber.tag(loggerTag.value).d("New Firebase token") + Timber.tag(loggerTag.value).w("New Firebase token") coroutineScope.launch { firebaseNewTokenHandler.handle(token) } } override fun onMessageReceived(message: RemoteMessage) { - Timber.tag(loggerTag.value).d("New Firebase message") + Timber.tag(loggerTag.value).w("New Firebase message. Priority: ${message.priority}/${message.originalPriority}") coroutineScope.launch { val pushData = pushParser.parse(message.data) if (pushData == null) { Timber.tag(loggerTag.value).w("Invalid data received from Firebase") + pushHandler.handleInvalid( + providerInfo = FirebaseConfig.NAME, + data = message.data.keys.joinToString("\n") { + "$it: ${message.data[it]}" + }, + ) } else { - pushHandler.handle(pushData) + pushHandler.handle( + pushData = pushData, + providerInfo = FirebaseConfig.NAME, + ) } } } diff --git a/libraries/pushproviders/firebase/src/main/res/values-cy/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..337a4773652 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-cy/translations.xml @@ -0,0 +1,11 @@ + + + "Gwnewch yn siwr fod Firebase ar gael." + "Nid yw Firebase ar gael." + "Mae Firebase ar gael." + "Gwiriwch Firebase" + "Gwnewch yn siŵr fod tocyn Firebase ar gael." + "Nid yw tocyn Firebase yn hysbys." + "Tocyn Firebase: %1$s." + "Gwiriwch tocyn Firebase" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-da/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..ea9afebf7f5 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-da/translations.xml @@ -0,0 +1,11 @@ + + + "Sørg for, at Firebase er tilgængelig." + "Firebase er ikke tilgængelig." + "Firebase er tilgængelig." + "Tjek Firebase" + "Sørg for, at Firebase-tokenet er tilgængeligt." + "Firebase-tokenet er ikke kendt." + "Firebase-token: %1$s." + "Tjek Firebase-token" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-es/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..d6a3de2d3c4 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-es/translations.xml @@ -0,0 +1,11 @@ + + + "Asegurarse de que Firebase esté disponible." + "Firebase no está disponible." + "Firebase está disponible." + "Verificar Firebase" + "Asegurarse de que el token de Firebase esté disponible." + "Se desconoce el token de Firebase." + "Token de Firebase: %1$s." + "Verificar token de Firebase" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-eu/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..e67e55ec2d8 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-eu/translations.xml @@ -0,0 +1,11 @@ + + + "Ziurtatu Firebase erabilgarri dagoela." + "Firebase ez dago erabilgarri." + "Firebase eskuragarri dago." + "Egiaztatu Firebase" + "Ziurtatu Firebaseren tokena erabilgarri dagoela." + "Ez da Firebaseren tokena ezagutzen." + "Firebaseren tokena: %1$s." + "Egiaztatu Firebaseren tokena" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-in/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..0c1a98ae74d --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-in/translations.xml @@ -0,0 +1,11 @@ + + + "Pastikan bahwa Firebase tersedia." + "Firebase tidak tersedia." + "Firebase tersedia." + "Periksa Firebase" + "Pastikan token Firebase tersedia." + "Token Firebase tidak diketahui." + "Token Firebase: %1$s." + "Periksa token Firebase" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-ka/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-ka/translations.xml new file mode 100644 index 00000000000..eb3645b5104 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-ka/translations.xml @@ -0,0 +1,11 @@ + + + "დარწმუნდით რომ Firebase ხელმისაწვდომია." + "Firebase არაა ხელმისაწვდომი." + "Firebase ხელმისაწვდომია." + "Firebase-ის შემოწმება" + "დარწმუნდით რომ Firebase ტოკენი ხელმისაწვდომია." + "Firebase ტოკენი უცნობია." + "Firebase ტოკენი: %1$s." + "შეამოწმეთ Firebase ტოკენი" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-nb/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..9bbff0e5380 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-nb/translations.xml @@ -0,0 +1,11 @@ + + + "Sørg for at Firebase er tilgjengelig." + "Firebase er ikke tilgjengelig." + "Firebase er tilgjengelig." + "Sjekk Firebase" + "Sørg for at Firebase-token er tilgjengelig." + "Firebase-token er ikke kjent." + "Firebase-token: %1$s." + "Sjekk Firebase-token" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-pt-rBR/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..3d7dcf4dd41 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,11 @@ + + + "Certifique-se de que o Firebase esteja disponível." + "O Firebase não está disponível." + "O Firebase está disponível." + "Verifique o Firebase" + "Certifique-se de que o token do Firebase esteja disponível." + "O token do Firebase não é conhecido." + "Token do Firebase: %1$s." + "Verifique o token do Firebase" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-tr/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..af9b7c2318e --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-tr/translations.xml @@ -0,0 +1,11 @@ + + + "Firebase\'in kullanılabilir olduğundan emin olun." + "Firebase kullanılamıyor." + "Firebase kullanılabilir." + "Firebase\'i kontrol et" + "Firebase belirtecinin mevcut olduğundan emin olun." + "Firebase belirteci bilinmiyor." + "Firebase belirteci: %1$s." + "Firebase jetonunu kontrol edin" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-ur/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..22605af3ca4 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-ur/translations.xml @@ -0,0 +1,11 @@ + + + "یقینی بنائیں کہ Firebase دستیاب ہے۔" + "Firebase دستیاب نہیں ہے۔" + "Firebase دستیاب ہے۔" + "Firebase کی پڑتال کریں" + "یقینی بنائیں کہ Firebase رمزِ ممیز دستیاب ہے۔" + "Firebase رمزِ ممیز معلوم نہیں ہے۔" + "Firebase رمزِ ممیز:%1$s۔" + "Firebase رمزِ ممیز کی پڑتال کریں" + diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt index 08e9c5be363..c8c5dfa9bfa 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt @@ -79,8 +79,8 @@ class DefaultFirebaseNewTokenHandlerTest { registerPusherResult.assertions() .isCalledExactly(2) .withSequence( - listOf(value(aMatrixClient1), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)), - listOf(value(aMatrixClient3), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)), + listOf(value(aMatrixClient1), value("aToken"), value(A_FIREBASE_GATEWAY)), + listOf(value(aMatrixClient3), value("aToken"), value(A_FIREBASE_GATEWAY)), ) } @@ -130,7 +130,7 @@ class DefaultFirebaseNewTokenHandlerTest { registerPusherResult.assertions() registerPusherResult.assertions() .isCalledOnce() - .with(value(aMatrixClient1), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)) + .with(value(aMatrixClient1), value("aToken"), value(A_FIREBASE_GATEWAY)) } private fun createDefaultFirebaseNewTokenHandler( @@ -139,13 +139,15 @@ class DefaultFirebaseNewTokenHandlerTest { userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(), matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(), firebaseStore: FirebaseStore = InMemoryFirebaseStore(), + firebaseGatewayProvider: FirebaseGatewayProvider = FakeFirebaseGatewayProvider(), ): FirebaseNewTokenHandler { return DefaultFirebaseNewTokenHandler( pusherSubscriber = pusherSubscriber, sessionStore = sessionStore, userPushStoreFactory = userPushStoreFactory, matrixClientProvider = matrixClientProvider, - firebaseStore = firebaseStore + firebaseStore = firebaseStore, + firebaseGatewayProvider = firebaseGatewayProvider, ) } } diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseGatewayProvider.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseGatewayProvider.kt new file mode 100644 index 00000000000..3540562a3d4 --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseGatewayProvider.kt @@ -0,0 +1,16 @@ +/* + * 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.pushproviders.firebase + +const val A_FIREBASE_GATEWAY = "aGateway" + +class FakeFirebaseGatewayProvider( + private val firebaseGatewayResult: () -> String = { A_FIREBASE_GATEWAY } +) : FirebaseGatewayProvider { + override fun getFirebaseGateway() = firebaseGatewayResult() +} diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt index fbb945106a1..53ed52be078 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt @@ -70,7 +70,7 @@ class FirebasePushProviderTest { assertThat(result).isEqualTo(Result.success(Unit)) registerPusherResultLambda.assertions() .isCalledOnce() - .with(value(matrixClient), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)) + .with(value(matrixClient), value("aToken"), value(A_FIREBASE_GATEWAY)) } @Test @@ -117,7 +117,7 @@ class FirebasePushProviderTest { assertThat(result).isEqualTo(Result.success(Unit)) unregisterPusherResultLambda.assertions() .isCalledOnce() - .with(value(matrixClient), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)) + .with(value(matrixClient), value("aToken"), value(A_FIREBASE_GATEWAY)) } @Test @@ -164,7 +164,7 @@ class FirebasePushProviderTest { ), ) val result = firebasePushProvider.getCurrentUserPushConfig() - assertThat(result).isEqualTo(CurrentUserPushConfig(FirebaseConfig.PUSHER_HTTP_URL, "aToken")) + assertThat(result).isEqualTo(CurrentUserPushConfig(A_FIREBASE_GATEWAY, "aToken")) } @Test @@ -194,12 +194,14 @@ class FirebasePushProviderTest { pusherSubscriber: PusherSubscriber = FakePusherSubscriber(), isPlayServiceAvailable: IsPlayServiceAvailable = FakeIsPlayServiceAvailable(false), firebaseTokenRotator: FirebaseTokenRotator = FakeFirebaseTokenRotator(), + firebaseGatewayProvider: FirebaseGatewayProvider = FakeFirebaseGatewayProvider() ): FirebasePushProvider { return FirebasePushProvider( firebaseStore = firebaseStore, pusherSubscriber = pusherSubscriber, isPlayServiceAvailable = isPlayServiceAvailable, firebaseTokenRotator = firebaseTokenRotator, + firebaseGatewayProvider = firebaseGatewayProvider, ) } } 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 59f9465298b..6b8c19934fb 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 @@ -22,6 +22,7 @@ import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -31,16 +32,29 @@ import org.robolectric.RobolectricTestRunner class VectorFirebaseMessagingServiceTest { @Test fun `test receiving invalid data`() = runTest { - val lambda = lambdaRecorder(ensureNeverCalled = true) { } + val lambda = lambdaRecorder { _, _ -> } val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( - pushHandler = FakePushHandler(handleResult = lambda) + pushHandler = FakePushHandler(handleInvalidResult = lambda) + ) + vectorFirebaseMessagingService.onMessageReceived( + message = RemoteMessage( + Bundle().apply { + putString("a", "A") + putString("b", "B") + } + ) ) - vectorFirebaseMessagingService.onMessageReceived(RemoteMessage(Bundle())) + runCurrent() + lambda.assertions().isCalledOnce() + .with( + value(FirebaseConfig.NAME), + value("a: A\nb: B"), + ) } @Test fun `test receiving valid data`() = runTest { - val lambda = lambdaRecorder { } + val lambda = lambdaRecorder { _, _ -> } val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( pushHandler = FakePushHandler(handleResult = lambda) ) @@ -56,7 +70,10 @@ class VectorFirebaseMessagingServiceTest { advanceUntilIdle() lambda.assertions() .isCalledOnce() - .with(value(PushData(AN_EVENT_ID, A_ROOM_ID, null, A_SECRET))) + .with( + value(PushData(AN_EVENT_ID, A_ROOM_ID, null, A_SECRET)), + value(FirebaseConfig.NAME) + ) } @Test diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index fd502263e9d..c98d98f64eb 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -19,6 +19,7 @@ setupAnvil() dependencies { implementation(libs.dagger) + implementation(projects.features.enterprise.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) @@ -41,13 +42,14 @@ dependencies { implementation(libs.serialization.json) // UnifiedPush library - api(libs.unifiedpush) - + implementation(libs.unifiedpush) testImplementation(libs.coroutines.test) testImplementation(libs.test.junit) testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.kotlinx.collections.immutable) + testImplementation(projects.features.enterprise.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultPushGatewayHttpUrlProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultPushGatewayHttpUrlProvider.kt new file mode 100644 index 00000000000..af434c30204 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultPushGatewayHttpUrlProvider.kt @@ -0,0 +1,26 @@ +/* + * 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.pushproviders.unifiedpush + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +interface DefaultPushGatewayHttpUrlProvider { + fun provide(): String +} + +@ContributesBinding(AppScope::class) +class DefaultDefaultPushGatewayHttpUrlProvider @Inject constructor( + private val enterpriseService: EnterpriseService, +) : DefaultPushGatewayHttpUrlProvider { + override fun provide(): String { + return enterpriseService.unifiedPushDefaultPushGateway() ?: UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL + } +} diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt index dc32dbff763..862212c3331 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -33,8 +33,9 @@ class DefaultRegisterUnifiedPushUseCase @Inject constructor( UnifiedPush.saveDistributor(context, distributor.value) // This will trigger the callback // VectorUnifiedPushMessagingReceiver.onNewEndpoint - UnifiedPush.registerApp(context = context, instance = clientSecret) + UnifiedPush.register(context = context, instance = clientSecret) // Wait for VectorUnifiedPushMessagingReceiver.onNewEndpoint to proceed + @Suppress("RunCatchingNotAllowed") return runCatching { withTimeout(30.seconds) { val result = endpointRegistrationHandler.state diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt index a6fbd7051fb..8dc54dff8c1 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.pushproviders.unifiedpush import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import kotlinx.coroutines.withContext import retrofit2.HttpException @@ -29,6 +30,8 @@ interface UnifiedPushGatewayResolver { suspend fun getGateway(endpoint: String): UnifiedPushGatewayResolverResult } +private val loggerTag = LoggerTag("DefaultUnifiedPushGatewayResolver") + @ContributesBinding(AppScope::class) class DefaultUnifiedPushGatewayResolver @Inject constructor( private val unifiedPushApiFactory: UnifiedPushApiFactory, @@ -36,36 +39,36 @@ class DefaultUnifiedPushGatewayResolver @Inject constructor( ) : UnifiedPushGatewayResolver { override suspend fun getGateway(endpoint: String): UnifiedPushGatewayResolverResult { val url = tryOrNull( - onError = { Timber.tag("DefaultUnifiedPushGatewayResolver").d(it, "Cannot parse endpoint as an URL") } + onException = { Timber.tag(loggerTag.value).d(it, "Cannot parse endpoint as an URL") } ) { URL(endpoint) } return if (url == null) { - Timber.tag("DefaultUnifiedPushGatewayResolver").d("ErrorInvalidUrl") + Timber.tag(loggerTag.value).d("ErrorInvalidUrl") UnifiedPushGatewayResolverResult.ErrorInvalidUrl } else { val port = if (url.port != -1) ":${url.port}" else "" val customBase = "${url.protocol}://${url.host}$port" val customUrl = "$customBase/_matrix/push/v1/notify" - Timber.tag("DefaultUnifiedPushGatewayResolver").i("Testing $customUrl") + Timber.tag(loggerTag.value).i("Testing $customUrl") return withContext(coroutineDispatchers.io) { val api = unifiedPushApiFactory.create(customBase) try { val discoveryResponse = api.discover() if (discoveryResponse.unifiedpush.gateway == "matrix") { - Timber.tag("DefaultUnifiedPushGatewayResolver").d("The endpoint seems to be a valid UnifiedPush gateway") + Timber.tag(loggerTag.value).d("The endpoint seems to be a valid UnifiedPush gateway") UnifiedPushGatewayResolverResult.Success(customUrl) } else { // The endpoint returned a 200 OK but didn't promote an actual matrix gateway, which means it doesn't have any - Timber.tag("DefaultUnifiedPushGatewayResolver").w("The endpoint does not seem to be a valid UnifiedPush gateway, using fallback") + Timber.tag(loggerTag.value).w("The endpoint does not seem to be a valid UnifiedPush gateway, using fallback") UnifiedPushGatewayResolverResult.NoMatrixGateway } } catch (throwable: Throwable) { if ((throwable as? HttpException)?.code() == HttpURLConnection.HTTP_NOT_FOUND) { - Timber.tag("DefaultUnifiedPushGatewayResolver").i("Checking for UnifiedPush endpoint yielded 404, using fallback") + Timber.tag(loggerTag.value).i("Checking for UnifiedPush endpoint yielded 404, using fallback") UnifiedPushGatewayResolverResult.NoMatrixGateway } else { - Timber.tag("DefaultUnifiedPushGatewayResolver").e(throwable, "Error checking for UnifiedPush endpoint") + Timber.tag(loggerTag.value).e(throwable, "Error checking for UnifiedPush endpoint") UnifiedPushGatewayResolverResult.Error(customUrl) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayUrlResolver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayUrlResolver.kt index 918db35e4ef..e7e31cfd673 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayUrlResolver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayUrlResolver.kt @@ -21,6 +21,7 @@ interface UnifiedPushGatewayUrlResolver { @ContributesBinding(AppScope::class) class DefaultUnifiedPushGatewayUrlResolver @Inject constructor( private val unifiedPushStore: UnifiedPushStore, + private val defaultPushGatewayHttpUrlProvider: DefaultPushGatewayHttpUrlProvider, ) : UnifiedPushGatewayUrlResolver { override fun resolve( gatewayResult: UnifiedPushGatewayResolverResult, @@ -33,7 +34,7 @@ class DefaultUnifiedPushGatewayUrlResolver @Inject constructor( } UnifiedPushGatewayResolverResult.ErrorInvalidUrl, UnifiedPushGatewayResolverResult.NoMatrixGateway -> { - UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL + defaultPushGatewayHttpUrlProvider.provide() } is UnifiedPushGatewayResolverResult.Success -> { gatewayResult.gateway diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt index e41aa6a51f1..24b9676c11c 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt @@ -53,6 +53,6 @@ class DefaultUnregisterUnifiedPushUseCase @Inject constructor( override fun cleanup(clientSecret: String) { unifiedPushStore.storeUpEndpoint(clientSecret, null) unifiedPushStore.storePushGateway(clientSecret, null) - UnifiedPush.unregisterApp(context, clientSecret) + UnifiedPush.unregister(context, clientSecret) } } 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 2e48574de11..b19aa985fed 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 @@ -11,12 +11,16 @@ import android.content.Context import android.content.Intent 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.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import org.unifiedpush.android.connector.FailedReason import org.unifiedpush.android.connector.MessagingReceiver +import org.unifiedpush.android.connector.data.PushEndpoint +import org.unifiedpush.android.connector.data.PushMessage import timber.log.Timber import javax.inject.Inject @@ -31,6 +35,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var unifiedPushGatewayUrlResolver: UnifiedPushGatewayUrlResolver @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler @Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler + @AppCoroutineScope @Inject lateinit var coroutineScope: CoroutineScope override fun onReceive(context: Context, intent: Intent) { @@ -45,14 +50,21 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { * @param message the message * @param instance connection, for multi-account */ - override fun onMessage(context: Context, message: ByteArray, instance: String) { - Timber.tag(loggerTag.value).d("New message") + override fun onMessage(context: Context, message: PushMessage, instance: String) { + Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}") coroutineScope.launch { - val pushData = pushParser.parse(message, instance) + val pushData = pushParser.parse(message.content, instance) if (pushData == null) { Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush") + pushHandler.handleInvalid( + providerInfo = "${UnifiedPushConfig.NAME} - $instance", + data = String(message.content), + ) } else { - pushHandler.handle(pushData) + pushHandler.handle( + pushData = pushData, + providerInfo = "${UnifiedPushConfig.NAME} - $instance", + ) } } } @@ -61,20 +73,20 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { * Called when a new endpoint is to be used for sending push messages. * You should send the endpoint to your application server and sync for missing notifications. */ - override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { - Timber.tag(loggerTag.value).i("onNewEndpoint: $endpoint") + override fun onNewEndpoint(context: Context, endpoint: PushEndpoint, instance: String) { + Timber.tag(loggerTag.value).w("onNewEndpoint: $endpoint") coroutineScope.launch { - val gateway = unifiedPushGatewayResolver.getGateway(endpoint) + val gateway = unifiedPushGatewayResolver.getGateway(endpoint.url) .let { gatewayResult -> unifiedPushGatewayUrlResolver.resolve(gatewayResult, instance) } unifiedPushStore.storePushGateway(instance, gateway) - val result = newGatewayHandler.handle(endpoint, gateway, instance) + val result = newGatewayHandler.handle(endpoint.url, gateway, instance) .onFailure { Timber.tag(loggerTag.value).e(it, "Failed to handle new gateway") } .onSuccess { - unifiedPushStore.storeUpEndpoint(instance, endpoint) + unifiedPushStore.storeUpEndpoint(instance, endpoint.url) } endpointRegistrationHandler.registrationDone( RegistrationResult( @@ -89,8 +101,8 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { /** * Called when the registration is not possible, eg. no network. */ - override fun onRegistrationFailed(context: Context, instance: String) { - Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance") + override fun onRegistrationFailed(context: Context, reason: FailedReason, instance: String) { + Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance, reason: $reason") /* Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME @@ -103,7 +115,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { * Called when this application is unregistered from receiving push messages. */ override fun onUnregistered(context: Context, instance: String) { - Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") + Timber.tag(loggerTag.value).w("UnifiedPush: Unregistered") /* val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME pushDataStore.setFdroidSyncBackgroundMode(mode) diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-cy/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..1a46d85451b --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-cy/translations.xml @@ -0,0 +1,14 @@ + + + "Sicrhewch fod dosbarthwyr UnifiedPush ar gael." + "Heb ganfod dosbarthwyr gwthio." + + "Wedi canfod %1$d dosbarthwyr: %2$s" + "Wedi canfod %1$d dosbarthwr: %2$s" + "Wedi canfod %1$d dosbarthwr: %2$s" + "Wedi canfod %1$d dosbarthwr: %2$s" + "Wedi canfod %1$d dosbarthwr: %2$s" + "Wedi canfod %1$d dosbarthwr: %2$s" + + "Gwiriwch UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-da/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..b6d549b58fd --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-da/translations.xml @@ -0,0 +1,10 @@ + + + "Sørg for, at UnifiedPush-distributører er tilgængelige." + "Ingen push-distributører fundet." + + "%1$d distributør fundet:%2$s." + "%1$d distributører fundet:%2$s." + + "Afprøv UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-es/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..dca4f019be5 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-es/translations.xml @@ -0,0 +1,10 @@ + + + "Asegurarse de que los distribuidores de UnifiedPush están disponibles." + "No se ha encontrado ningún distribuidor push." + + "%1$d distribuidor encontrado: %2$s." + "%1$d distribuidores encontrados: %2$s." + + "Verificar UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-eu/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..7952c7d1033 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-eu/translations.xml @@ -0,0 +1,9 @@ + + + "Ez da push banatzailerik aurkitu." + + "Banatzaile %1$d aurkitu da: %2$s." + "%1$d banatzailea aurkitu dira: %2$s." + + "Egiaztatu UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-in/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..8190062b85b --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-in/translations.xml @@ -0,0 +1,9 @@ + + + "Pastikan distributor UnifiedPush tersedia." + "Tidak ada distributor notifikasi dorongan yang ditemukan." + + "%1$d distributor ditemukan: %2$s." + + "Periksa UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-ka/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-ka/translations.xml new file mode 100644 index 00000000000..b68a00704e8 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-ka/translations.xml @@ -0,0 +1,10 @@ + + + "დარწმუნდით რომ UnifiedPush დისტრიბუტორები ხელმისაწვდომია." + "Push დისტრიბუტორები არ მოიძებნა." + + "%1$d დისტრიბუტორი მოიძებნა: %2$s" + "%1$d დისტრიბუტორი მოიძებნა: %2$s" + + "შეამოწმეთ UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-nb/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-nb/translations.xml new file mode 100644 index 00000000000..67f6895953b --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-nb/translations.xml @@ -0,0 +1,10 @@ + + + "Påse at UnifiedPush-distributører er tilgjengelige." + "Ingen push-distributører funnet." + + "%1$d distributør funnet: %2$s." + "%1$d distributører funnet: %2$s." + + "Sjekk UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-pt-rBR/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 00000000000..4f7a8726ceb --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,10 @@ + + + "Certifique-se de que os distribuidores do UnifiedPush estejam disponíveis." + "Nenhum distribuidor push encontrado." + + "%1$d distribuidor encontrado: %2$s." + "%1$d Distribuidores encontrados: %2$s." + + "Verifique o UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-tr/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..5fee0a69386 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-tr/translations.xml @@ -0,0 +1,10 @@ + + + "UnifiedPush distribütörlerinin mevcut olduğundan emin olun." + "İtme dağıtıcı bulunamadı." + + "%1$d dağıtıcı bulundu: %2$s." + "%1$d dağıtıcı bulundu: %2$s." + + "UnifiedPush\'u kontrol edin" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-ur/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..4e74d70cb79 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-ur/translations.xml @@ -0,0 +1,10 @@ + + + "یقینی بنائیں کہ UnifiedPush تقسیم کاران دستیاب ہیں۔" + "کوئی دھکا تقسیم کاران نہیں ملے۔" + + "%1$d تقسیم کار ملا: %2$s۔" + "%1$d تقسیم کاران ملے: %2$s۔" + + "UnifiedPush کی پڑتال کریں" + diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayUrlResolverTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayUrlResolverTest.kt index 7edc223f73a..27008d96ecf 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayUrlResolverTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayUrlResolverTest.kt @@ -18,7 +18,7 @@ class DefaultUnifiedPushGatewayUrlResolverTest { gatewayResult = UnifiedPushGatewayResolverResult.ErrorInvalidUrl, instance = "", ) - assertThat(result).isEqualTo(UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL) + assertThat(result).isEqualTo(A_UNIFIED_PUSH_GATEWAY) } @Test @@ -28,7 +28,7 @@ class DefaultUnifiedPushGatewayUrlResolverTest { gatewayResult = UnifiedPushGatewayResolverResult.NoMatrixGateway, instance = "", ) - assertThat(result).isEqualTo(UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL) + assertThat(result).isEqualTo(A_UNIFIED_PUSH_GATEWAY) } @Test @@ -77,7 +77,9 @@ class DefaultUnifiedPushGatewayUrlResolverTest { private fun createDefaultUnifiedPushGatewayUrlResolver( unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(), + defaultPushGatewayHttpUrlProvider: DefaultPushGatewayHttpUrlProvider = FakeDefaultPushGatewayHttpUrlProvider(), ) = DefaultUnifiedPushGatewayUrlResolver( unifiedPushStore = unifiedPushStore, + defaultPushGatewayHttpUrlProvider = defaultPushGatewayHttpUrlProvider, ) } diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeDefaultPushGatewayHttpUrlProvider.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeDefaultPushGatewayHttpUrlProvider.kt new file mode 100644 index 00000000000..de0a293fc21 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeDefaultPushGatewayHttpUrlProvider.kt @@ -0,0 +1,18 @@ +/* + * 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.pushproviders.unifiedpush + +const val A_UNIFIED_PUSH_GATEWAY = "aGateway" + +class FakeDefaultPushGatewayHttpUrlProvider( + private val provideResult: () -> String = { A_UNIFIED_PUSH_GATEWAY } +) : DefaultPushGatewayHttpUrlProvider { + override fun provide(): String { + return provideResult() + } +} 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 69502f020e4..5bb0a6731fc 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 @@ -32,6 +32,10 @@ import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.unifiedpush.android.connector.FailedReason +import org.unifiedpush.android.connector.data.PublicKeySet +import org.unifiedpush.android.connector.data.PushEndpoint +import org.unifiedpush.android.connector.data.PushMessage @RunWith(RobolectricTestRunner::class) class VectorUnifiedPushMessagingReceiverTest { @@ -56,19 +60,19 @@ class VectorUnifiedPushMessagingReceiverTest { fun `onRegistrationFailed does nothing`() = runTest { val context = InstrumentationRegistry.getInstrumentation().context val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver() - vectorUnifiedPushMessagingReceiver.onRegistrationFailed(context, A_SECRET) + vectorUnifiedPushMessagingReceiver.onRegistrationFailed(context, FailedReason.NETWORK, A_SECRET) } @Test - fun `onMessage valid invoke the push handler`() = runTest { + fun `onMessage valid invokes the push handler`() = runTest { val context = InstrumentationRegistry.getInstrumentation().context - val pushHandlerResult = lambdaRecorder {} + val pushHandlerResult = lambdaRecorder { _, _ -> } val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( pushHandler = FakePushHandler( handleResult = pushHandlerResult ), ) - vectorUnifiedPushMessagingReceiver.onMessage(context, UnifiedPushParserTest.UNIFIED_PUSH_DATA.toByteArray(), A_SECRET) + vectorUnifiedPushMessagingReceiver.onMessage(context, aPushMessage(), A_SECRET) advanceUntilIdle() pushHandlerResult.assertions() .isCalledOnce() @@ -80,23 +84,25 @@ class VectorUnifiedPushMessagingReceiverTest { unread = 1, clientSecret = A_SECRET ) + ), + value( + UnifiedPushConfig.NAME + " - " + A_SECRET ) ) } @Test - fun `onMessage invalid does not invoke the push handler`() = runTest { + fun `onMessage invalid invokes the push handler invalid method`() = runTest { val context = InstrumentationRegistry.getInstrumentation().context - val pushHandlerResult = lambdaRecorder {} + val handleInvalidResult = lambdaRecorder { _, _ -> } val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( pushHandler = FakePushHandler( - handleResult = pushHandlerResult + handleInvalidResult = handleInvalidResult, ), ) - vectorUnifiedPushMessagingReceiver.onMessage(context, "".toByteArray(), A_SECRET) + vectorUnifiedPushMessagingReceiver.onMessage(context, aPushMessage(""), A_SECRET) advanceUntilIdle() - pushHandlerResult.assertions() - .isNeverCalled() + handleInvalidResult.assertions().isCalledOnce() } @Test @@ -125,7 +131,7 @@ class VectorUnifiedPushMessagingReceiverTest { unifiedPushNewGatewayHandler = unifiedPushNewGatewayHandler, ) endpointRegistrationHandler.state.test { - vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, "anEndpoint", A_SECRET) + vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, aPushEndpoint("anEndpoint"), A_SECRET) advanceUntilIdle() assertThat(awaitItem()).isEqualTo( RegistrationResult( @@ -168,7 +174,7 @@ class VectorUnifiedPushMessagingReceiverTest { unifiedPushNewGatewayHandler = unifiedPushNewGatewayHandler, ) endpointRegistrationHandler.state.test { - vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, "anEndpoint", A_SECRET) + vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, aPushEndpoint(), A_SECRET) advanceUntilIdle() assertThat(awaitItem()).isEqualTo( RegistrationResult( @@ -205,3 +211,19 @@ class VectorUnifiedPushMessagingReceiverTest { } } } + +private fun aPushMessage( + data: String = UnifiedPushParserTest.UNIFIED_PUSH_DATA, + decrypted: Boolean = true, +) = PushMessage( + content = data.toByteArray(), + decrypted = decrypted, +) + +private fun aPushEndpoint( + url: String = "anEndpoint", + pubKeySet: PublicKeySet? = null, +) = PushEndpoint( + url = url, + pubKeySet = pubKeySet, +) diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts index 2afd3cb87aa..ec81643cfd1 100644 --- a/libraries/pushstore/impl/build.gradle.kts +++ b/libraries/pushstore/impl/build.gradle.kts @@ -46,5 +46,4 @@ dependencies { androidTestImplementation(libs.test.junit) androidTestImplementation(libs.test.truth) androidTestImplementation(libs.test.runner) - androidTestImplementation(projects.libraries.sessionStorage.test) } diff --git a/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt b/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt index 549ba59db9e..dbc212efe1a 100644 --- a/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt +++ b/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.pushstore.impl import androidx.test.platform.app.InstrumentationRegistry import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore -import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Test @@ -28,7 +27,7 @@ class DefaultUserPushStoreFactoryTest { fun testParallelCreation() { val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext val sessionId = SessionId("@alice:server.org") - val userPushStoreFactory = DefaultUserPushStoreFactory(context, NoOpSessionObserver()) + val userPushStoreFactory = DefaultUserPushStoreFactory(context) var userPushStore1: UserPushStore? = null val thread1 = thread { userPushStore1 = userPushStoreFactory.getOrCreate(sessionId) diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt index d59de4da9be..0087a8a9bba 100644 --- a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt +++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt @@ -44,8 +44,8 @@ import kotlin.coroutines.suspendCoroutine @Composable fun QrCodeCameraView( onScanQrCode: (ByteArray) -> Unit, + renderPreview: Boolean, modifier: Modifier = Modifier, - renderPreview: Boolean = true, ) { if (LocalInspectionMode.current) { Box( @@ -62,9 +62,11 @@ fun QrCodeCameraView( var cameraProvider by remember { mutableStateOf(null) } val previewUseCase = remember { Preview.Builder().build() } var lastFrame by remember { mutableStateOf(null) } - val imageAnalysis = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() + val imageAnalysis = remember { + ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + } LaunchedEffect(Unit) { cameraProvider = localContext.getCameraProvider() diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 1f13346b3ae..7d90629a50a 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -32,12 +32,12 @@ 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.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.RadioButton import io.element.android.libraries.designsystem.theme.components.Scaffold @@ -94,14 +94,9 @@ fun RoomSelectView( modifier = modifier, topBar = { TopAppBar( - title = { - Text( - text = when (state.mode) { - RoomSelectMode.Forward -> stringResource(CommonStrings.common_forward_message) - RoomSelectMode.Share -> stringResource(CommonStrings.common_send_to) - }, - style = ElementTheme.typography.aliasScreenTitle - ) + titleStr = when (state.mode) { + RoomSelectMode.Forward -> stringResource(CommonStrings.common_forward_message) + RoomSelectMode.Share -> stringResource(CommonStrings.common_send_to) }, navigationIcon = { BackButton(onClick = { onBackButton(state) }) @@ -214,11 +209,14 @@ private fun RoomSummaryView( .heightIn(56.dp), verticalAlignment = Alignment.CenterVertically ) { - CompositeAvatar( + Avatar( avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem), - heroes = roomInfo.heroes.map { user -> - user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem) - }.toPersistentList() + avatarType = AvatarType.Room( + heroes = roomInfo.heroes.map { user -> + user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem) + }.toPersistentList(), + isTombstoned = roomInfo.isTombstoned, + ), ) Column( modifier = Modifier diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index 22eb2338b63..76462ef0351 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -1,6 +1,4 @@ import extension.setupAnvil -import org.gradle.internal.extensions.stdlib.capitalized -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile /* * Copyright 2023, 2024 New Vector Ltd. @@ -28,7 +26,6 @@ dependencies { implementation(libs.sqldelight.driver.android) implementation(libs.sqlcipher) implementation(libs.sqlite) - implementation(libs.androidx.security.crypto) implementation(projects.libraries.di) implementation(libs.sqldelight.coroutines) @@ -53,15 +50,3 @@ sqldelight { } } } - -// Workaround for KSP not picking up the generated files from SqlDelight -androidComponents { - onVariants(selector().all()) { variant -> - afterEvaluate { - val variantName = variant.name.capitalized() - tasks.getByName("ksp${variantName}Kotlin") { - setSource(tasks.getByName("generate${variantName}SessionDatabaseInterface").outputs) - } - } - } -} diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index 95d3f501e46..d65fa5da90c 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -48,25 +48,29 @@ class DatabaseSessionStore @Inject constructor( } } - override suspend fun storeData(sessionData: SessionData) = sessionDataMutex.withLock { - database.sessionDataQueries.insertSessionData(sessionData.toDbModel()) + override suspend fun storeData(sessionData: SessionData) { + sessionDataMutex.withLock { + database.sessionDataQueries.insertSessionData(sessionData.toDbModel()) + } } - override suspend fun updateData(sessionData: SessionData) = sessionDataMutex.withLock { - val result = database.sessionDataQueries.selectByUserId(sessionData.userId) - .executeAsOneOrNull() - ?.toApiModel() + override suspend fun updateData(sessionData: SessionData) { + sessionDataMutex.withLock { + val result = database.sessionDataQueries.selectByUserId(sessionData.userId) + .executeAsOneOrNull() + ?.toApiModel() - if (result == null) { - Timber.e("User ${sessionData.userId} not found in session database") - return - } + if (result == null) { + Timber.e("User ${sessionData.userId} not found in session database") + return + } // Copy new data from SDK, but keep login timestamp - database.sessionDataQueries.updateSession( - sessionData.copy( - loginTimestamp = result.loginTimestamp, - ).toDbModel() - ) + database.sessionDataQueries.updateSession( + sessionData.copy( + loginTimestamp = result.loginTimestamp, + ).toDbModel() + ) + } } override suspend fun getLatestSession(): SessionData? { diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt index e35768ed6cc..71fc9b6a980 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver @@ -28,6 +29,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultSessionObserver @Inject constructor( private val sessionStore: SessionStore, + @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, ) : SessionObserver { diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/Compose.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/Compose.kt index eacc2c13713..7f8700adb41 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/Compose.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/Compose.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.testtags -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -16,7 +15,6 @@ import androidx.compose.ui.semantics.testTagsAsResourceId /** * Add a testTag to a Modifier, to be used by external tool, like TrafficLight for instance. */ -@OptIn(ExperimentalComposeUiApi::class) fun Modifier.testTag(id: TestTag) = semantics { testTag = id.value testTagsAsResourceId = true 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 ddc720ed401..6363c6781ba 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 @@ -97,10 +97,16 @@ object TestTags { */ val floatingActionButton = TestTag("floating-action-button") + /** + * Timeline. + */ + val timeline = TestTag("timeline") + /** * Timeline item. */ - val timelineItemSenderInfo = TestTag("timeline_item-sender_info") + val timelineItemSenderAvatar = TestTag("timeline_item-sender_avatar") + val timelineItemSenderName = TestTag("timeline_item-sender_name") /** * Search field. 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 bd5451b397b..1299f994632 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 @@ -88,7 +88,7 @@ private fun EditingModeView( Icon( imageVector = CompoundIcons.Edit(), contentDescription = null, - tint = ElementTheme.materialColors.secondary, + tint = ElementTheme.colors.iconSecondary, modifier = Modifier .padding(vertical = 8.dp) .size(16.dp), @@ -97,7 +97,7 @@ private fun EditingModeView( text = text, style = ElementTheme.typography.fontBodySmRegular, textAlign = TextAlign.Start, - color = ElementTheme.materialColors.secondary, + color = ElementTheme.colors.textSecondary, modifier = Modifier .padding(vertical = 8.dp) .weight(1f) @@ -105,7 +105,7 @@ private fun EditingModeView( Icon( imageVector = CompoundIcons.Close(), contentDescription = stringResource(CommonStrings.action_close), - tint = ElementTheme.materialColors.secondary, + tint = ElementTheme.colors.iconSecondary, modifier = Modifier .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 12.dp) .size(16.dp) @@ -140,7 +140,7 @@ private fun ReplyToModeView( Icon( imageVector = CompoundIcons.Close(), contentDescription = stringResource(CommonStrings.action_close), - tint = MaterialTheme.colorScheme.secondary, + tint = ElementTheme.colors.iconSecondary, modifier = Modifier .padding(end = 4.dp, top = 4.dp, start = 8.dp, bottom = 16.dp) .size(16.dp) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ElementRichTextEditorStyle.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ElementRichTextEditorStyle.kt index f7ff275e3a5..357507ba239 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ElementRichTextEditorStyle.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ElementRichTextEditorStyle.kt @@ -27,10 +27,11 @@ object ElementRichTextEditorStyle { return baseStyle.copy( text = baseStyle.text.copy( color = if (hasFocus) { - ElementTheme.materialColors.primary + ElementTheme.colors.textPrimary } else { - ElementTheme.materialColors.secondary + ElementTheme.colors.textSecondary }, + placeholderColor = ElementTheme.colors.textSecondary, lineHeight = TextUnit.Unspecified, includeFontPadding = true, ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt index 108e969b04f..e98c6453e79 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/SoftKeyboardEffect.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalView import androidx.compose.ui.viewinterop.AndroidView import io.element.android.libraries.androidutils.ui.awaitWindowFocus +import io.element.android.libraries.androidutils.ui.isKeyboardVisible import io.element.android.libraries.androidutils.ui.showKeyboard /** @@ -40,11 +41,13 @@ internal fun SoftKeyboardEffect( // Await window focus in case returning from a dialog view.awaitWindowFocus() - // Show the keyboard, temporarily using the root view for focus - view.showKeyboard(andRequestFocus = true) + if (!view.isKeyboardVisible()) { + // Show the keyboard, temporarily using the root view for focus + view.showKeyboard(andRequestFocus = true) - // Refocus to the correct view - latestOnRequestFocus() + // Refocus to the correct view + latestOnRequestFocus() + } } } } 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 a5d672ab5d9..59f622de70b 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 @@ -7,13 +7,14 @@ package io.element.android.libraries.textcomposer +import android.content.res.Configuration import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row @@ -27,25 +28,39 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope 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.platform.LocalView import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.hideFromAccessibility +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.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.androidutils.ui.showKeyboard import io.element.android.libraries.designsystem.components.media.createFakeWaveform +import io.element.android.libraries.designsystem.preview.DAY_MODE_NAME import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.NIGHT_MODE_NAME 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.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon +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.timeline.item.event.EventOrTransactionId @@ -54,8 +69,6 @@ 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.testtags.TestTags import io.element.android.libraries.testtags.testTag -import io.element.android.libraries.textcomposer.components.ComposerOptionsButton -import io.element.android.libraries.textcomposer.components.DismissTextFormattingButton import io.element.android.libraries.textcomposer.components.SendButton import io.element.android.libraries.textcomposer.components.TextFormatting import io.element.android.libraries.textcomposer.components.VoiceMessageDeleteButton @@ -75,11 +88,11 @@ import io.element.android.libraries.textcomposer.model.aTextEditorStateRich import io.element.android.libraries.textcomposer.model.showCaptionCompatibilityWarning import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.compose.RichTextEditor -import io.element.android.wysiwyg.compose.RichTextEditorState import io.element.android.wysiwyg.display.TextDisplay import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch import uniffi.wysiwyg_composer.MenuAction import kotlin.time.Duration.Companion.seconds @@ -103,9 +116,9 @@ fun TextComposer( onReceiveSuggestion: (Suggestion?) -> Unit, onSelectRichContent: ((Uri) -> Unit)?, resolveMentionDisplay: (text: String, url: String) -> TextDisplay, + resolveAtRoomMentionDisplay: () -> TextDisplay, modifier: Modifier = Modifier, showTextFormatting: Boolean = false, - subcomposing: Boolean = false, ) { val markdown = when (state) { is TextEditorState.Markdown -> state.state.text.value() @@ -141,10 +154,10 @@ fun TextComposer( Spacer(modifier = Modifier.width(16.dp)) } else -> { - ComposerOptionsButton( - modifier = Modifier - .size(48.dp), - onClick = onAddAttachment + IconColorButton( + onClick = onAddAttachment, + imageVector = CompoundIcons.Plus(), + contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), ) } } @@ -165,20 +178,43 @@ fun TextComposer( } else { when (state) { is TextEditorState.Rich -> { - remember(state.richTextEditorState, subcomposing, composerMode, onResetComposerMode, onError) { + val coroutineScope = rememberCoroutineScope() + val view = LocalView.current + remember(state.richTextEditorState, composerMode, onResetComposerMode, onError) { @Composable { - TextInput( - state = state.richTextEditorState, - subcomposing = subcomposing, - placeholder = placeholder, + TextInputBox( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { + coroutineScope.launch { + state.requestFocus() + view.showKeyboard() + } + } + .semantics { + hideFromAccessibility() + }, composerMode = composerMode, onResetComposerMode = onResetComposerMode, - resolveMentionDisplay = resolveMentionDisplay, - resolveRoomMentionDisplay = { resolveMentionDisplay("@room", "#") }, - onError = onError, - onTyping = onTyping, - onSelectRichContent = onSelectRichContent, - ) + isTextEmpty = state.richTextEditorState.messageHtml.isEmpty(), + ) { + RichTextEditor( + state = state.richTextEditorState, + placeholder = placeholder, + registerStateUpdates = true, + modifier = Modifier + .padding(top = 6.dp, bottom = 6.dp) + .fillMaxWidth(), + style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.richTextEditorState.hasFocus), + resolveMentionDisplay = resolveMentionDisplay, + resolveRoomMentionDisplay = resolveAtRoomMentionDisplay, + onError = onError, + onRichContentSelected = onSelectRichContent, + onTyping = onTyping, + ) + } } } } @@ -188,13 +224,12 @@ fun TextComposer( TextInputBox( composerMode = composerMode, onResetComposerMode = onResetComposerMode, - placeholder = placeholder, - showPlaceholder = state.state.text.value().isEmpty(), - subcomposing = subcomposing, + isTextEmpty = state.state.text.value().isEmpty(), ) { MarkdownTextInput( state = state.state, - subcomposing = subcomposing, + placeholder = placeholder, + placeholderColor = ElementTheme.colors.textSecondary, onTyping = onTyping, onReceiveSuggestion = onReceiveSuggestion, richTextEditorStyle = style, @@ -250,6 +285,13 @@ fun TextComposer( else -> sendButton } + val endButtonA11y = endButtonA11y( + composerMode = composerMode, + voiceMessageState = voiceMessageState, + enableVoiceMessages = enableVoiceMessages, + canSendMessage = canSendMessage, + ) + val voiceRecording = @Composable { when (voiceMessageState) { is VoiceMessageState.Preview -> @@ -283,34 +325,40 @@ fun TextComposer( if (showTextFormatting && textFormattingOptions != null) { TextFormattingLayout( modifier = layoutModifier, + isRoomEncrypted = state.isRoomEncrypted, textInput = textInput, dismissTextFormattingButton = { - DismissTextFormattingButton(onClick = onDismissTextFormatting) + IconColorButton( + onClick = onDismissTextFormatting, + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(R.string.rich_text_editor_close_formatting_options), + ) }, textFormatting = textFormattingOptions, + endButtonA11y = endButtonA11y, sendButton = sendButton, ) } else { StandardLayout( voiceMessageState = voiceMessageState, enableVoiceMessages = enableVoiceMessages, + isRoomEncrypted = state.isRoomEncrypted, modifier = layoutModifier, composerOptionsButton = composerOptionsButton, textInput = textInput, endButton = sendOrRecordButton, + endButtonA11y = endButtonA11y, voiceRecording = voiceRecording, voiceDeleteButton = voiceDeleteButton, ) } - if (!subcomposing) { - SoftKeyboardEffect(composerMode, onRequestFocus) { - it is MessageComposerMode.Special - } - - SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it } + SoftKeyboardEffect(composerMode, onRequestFocus) { + it is MessageComposerMode.Special } + SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it } + val latestOnReceiveSuggestion by rememberUpdatedState(onReceiveSuggestion) if (state is TextEditorState.Rich) { val menuAction = state.richTextEditorState.menuAction @@ -325,79 +373,148 @@ fun TextComposer( } } +@ReadOnlyComposable +@Composable +private fun endButtonA11y( + composerMode: MessageComposerMode, + voiceMessageState: VoiceMessageState, + enableVoiceMessages: Boolean, + canSendMessage: Boolean, +): (SemanticsPropertyReceiver) -> Unit { + val a11ySendButtonDescription = stringResource( + id = when { + enableVoiceMessages && !canSendMessage -> + when (voiceMessageState) { + VoiceMessageState.Idle, + is VoiceMessageState.Recording -> if (voiceMessageState is VoiceMessageState.Recording) { + CommonStrings.a11y_voice_message_stop_recording + } else { + CommonStrings.a11y_voice_message_record + } + is VoiceMessageState.Preview -> when (voiceMessageState.isSending) { + true -> CommonStrings.common_sending + false -> CommonStrings.action_send_voice_message + } + } + composerMode.isEditing -> CommonStrings.action_send_edited_message + else -> CommonStrings.action_send_message + } + ) + val endButtonA11y: (SemanticsPropertyReceiver.() -> Unit) = { + contentDescription = a11ySendButtonDescription + onClick(null, null) + } + return endButtonA11y +} + @Composable private fun StandardLayout( voiceMessageState: VoiceMessageState, enableVoiceMessages: Boolean, + isRoomEncrypted: Boolean?, textInput: @Composable () -> Unit, composerOptionsButton: @Composable () -> Unit, voiceRecording: @Composable () -> Unit, voiceDeleteButton: @Composable () -> Unit, endButton: @Composable () -> Unit, + endButtonA11y: (SemanticsPropertyReceiver.() -> Unit), modifier: Modifier = Modifier, ) { - Row( - modifier = modifier, - verticalAlignment = Alignment.Bottom, - ) { - if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) { - if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) { + Column(modifier = modifier) { + if (isRoomEncrypted == false) { +// Spacer(Modifier.height(16.dp)) +// NotEncryptedBadge() +// Spacer(Modifier.height(4.dp)) + } + Row(verticalAlignment = Alignment.Bottom) { + if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) { + if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) { + Box( + modifier = Modifier + .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) + .size(48.dp), + contentAlignment = Alignment.Center, + ) { + voiceDeleteButton() + } + } else { + Spacer(modifier = Modifier.width(16.dp)) + } Box( modifier = Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) - .size(48.dp), - contentAlignment = Alignment.Center, + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) ) { - voiceDeleteButton() + voiceRecording() } } else { - Spacer(modifier = Modifier.width(16.dp)) - } - Box( - modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) - ) { - voiceRecording() + Box( + Modifier + .padding(bottom = 5.dp, top = 5.dp, start = 3.dp) + ) { + composerOptionsButton() + } + Box( + modifier = Modifier + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) + ) { + textInput() + } } - } else { Box( Modifier - .padding(bottom = 5.dp, top = 5.dp, start = 3.dp) - ) { - composerOptionsButton() - } - Box( - modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) + .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) + .size(48.dp) + .clearAndSetSemantics(endButtonA11y), + contentAlignment = Alignment.Center, ) { - textInput() + endButton() } } - Box( - Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) - .size(48.dp), - contentAlignment = Alignment.Center, - ) { - endButton() - } + } +} + +@Composable +private fun NotEncryptedBadge() { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(16.dp), + imageVector = CompoundIcons.LockOff(), + contentDescription = null, + tint = ElementTheme.colors.iconInfoPrimary, + ) + Spacer(Modifier.width(4.dp)) + Text( + text = stringResource(CommonStrings.common_not_encrypted), + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) } } @Composable private fun TextFormattingLayout( + isRoomEncrypted: Boolean?, textInput: @Composable () -> Unit, dismissTextFormattingButton: @Composable () -> Unit, textFormatting: @Composable () -> Unit, sendButton: @Composable () -> Unit, + endButtonA11y: (SemanticsPropertyReceiver.() -> Unit), modifier: Modifier = Modifier ) { Column( modifier = modifier.padding(vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { + if (isRoomEncrypted == false) { +// NotEncryptedBadge() +// Spacer(Modifier.height(8.dp)) + } Box( modifier = Modifier .weight(1f) @@ -419,10 +536,12 @@ private fun TextFormattingLayout( textFormatting() } Box( - modifier = Modifier.padding( - start = 14.dp, - end = 6.dp - ) + modifier = Modifier + .padding( + start = 14.dp, + end = 6.dp, + ) + .clearAndSetSemantics(endButtonA11y) ) { sendButton() } @@ -434,10 +553,9 @@ private fun TextFormattingLayout( private fun TextInputBox( composerMode: MessageComposerMode, onResetComposerMode: () -> Unit, - placeholder: String, - showPlaceholder: Boolean, - subcomposing: Boolean, - textInput: @Composable BoxScope.() -> Unit, + isTextEmpty: Boolean, + modifier: Modifier = Modifier, + textInput: @Composable () -> Unit, ) { val bgColor = ElementTheme.colors.bgSubtleSecondary val borderColor = ElementTheme.colors.borderDisabled @@ -449,7 +567,8 @@ private fun TextInputBox( .border(0.5.dp, borderColor, roundedCorners) .background(color = bgColor) .requiredHeightIn(min = 42.dp) - .fillMaxSize(), + .fillMaxSize() + .then(modifier), ) { if (composerMode is MessageComposerMode.Special) { ComposerModeView( @@ -457,29 +576,14 @@ private fun TextInputBox( onResetComposerMode = onResetComposerMode, ) } - val defaultTypography = ElementTheme.typography.fontBodyLgRegular Box( modifier = Modifier .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp) - // Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail - .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), + .then(Modifier.testTag(TestTags.textEditor)), contentAlignment = Alignment.CenterStart, ) { - // Placeholder - if (showPlaceholder) { - Text( - text = placeholder, - style = defaultTypography.copy( - color = ElementTheme.colors.textSecondary, - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - textInput() - - if (showPlaceholder && composerMode.showCaptionCompatibilityWarning()) { + if (isTextEmpty && composerMode.showCaptionCompatibilityWarning()) { var showBottomSheet by remember { mutableStateOf(false) } Icon( modifier = Modifier @@ -500,62 +604,26 @@ private fun TextInputBox( } } -@Composable -private fun TextInput( - state: RichTextEditorState, - subcomposing: Boolean, - placeholder: String, - composerMode: MessageComposerMode, - onResetComposerMode: () -> Unit, - resolveRoomMentionDisplay: () -> TextDisplay, - resolveMentionDisplay: (text: String, url: String) -> TextDisplay, - onError: (Throwable) -> Unit, - onTyping: (Boolean) -> Unit, - onSelectRichContent: ((Uri) -> Unit)?, -) { - TextInputBox( - composerMode = composerMode, - onResetComposerMode = onResetComposerMode, - placeholder = placeholder, - showPlaceholder = state.messageHtml.isEmpty(), - subcomposing = subcomposing, - ) { - RichTextEditor( - state = state, - // Disable most of the editor functionality if it's just being measured for a subcomposition. - // This prevents it gaining focus and mutating the state. - registerStateUpdates = !subcomposing, - modifier = Modifier - .padding(top = 6.dp, bottom = 6.dp) - .fillMaxWidth(), - style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus), - resolveMentionDisplay = resolveMentionDisplay, - resolveRoomMentionDisplay = resolveRoomMentionDisplay, - onError = onError, - onRichContentSelected = onSelectRichContent, - onTyping = onTyping, - ) - } -} - -private fun aTextEditorStateMarkdownList() = persistentListOf( - aTextEditorStateMarkdown(initialText = "", initialFocus = true), - aTextEditorStateMarkdown(initialText = "A message", initialFocus = true), +private fun aTextEditorStateMarkdownList(isRoomEncrypted: Boolean? = null) = persistentListOf( + aTextEditorStateMarkdown(initialText = "", initialFocus = true, isRoomEncrypted = isRoomEncrypted), + aTextEditorStateMarkdown(initialText = "A message", initialFocus = true, isRoomEncrypted = isRoomEncrypted), aTextEditorStateMarkdown( initialText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow", initialFocus = true, + isRoomEncrypted = isRoomEncrypted, ), - aTextEditorStateMarkdown(initialText = "A message without focus", initialFocus = false), + aTextEditorStateMarkdown(initialText = "A message without focus", initialFocus = false, isRoomEncrypted = isRoomEncrypted), ) -private fun aTextEditorStateRichList() = persistentListOf( - aTextEditorStateRich(initialFocus = true), - aTextEditorStateRich(initialText = "A message", initialFocus = true), +private fun aTextEditorStateRichList(isRoomEncrypted: Boolean? = null) = persistentListOf( + aTextEditorStateRich(initialFocus = true, isRoomEncrypted = isRoomEncrypted), + aTextEditorStateRich(initialText = "A message", initialFocus = true, isRoomEncrypted = isRoomEncrypted), aTextEditorStateRich( initialText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow", - initialFocus = true + initialFocus = true, + isRoomEncrypted = isRoomEncrypted, ), - aTextEditorStateRich(initialText = "A message without focus", initialFocus = false), + aTextEditorStateRich(initialText = "A message without focus", initialFocus = false, isRoomEncrypted = isRoomEncrypted), ) @PreviewsDayNight @@ -573,6 +641,21 @@ internal fun TextComposerSimplePreview() = ElementPreview { } } +@PreviewsDayNight +@Composable +internal fun TextComposerSimpleNotEncryptedPreview() = ElementPreview { + PreviewColumn( + items = aTextEditorStateMarkdownList(isRoomEncrypted = false), + ) { _, textEditorState -> + ATextComposer( + state = textEditorState, + voiceMessageState = VoiceMessageState.Idle, + composerMode = MessageComposerMode.Normal, + enableVoiceMessages = true, + ) + } +} + @PreviewsDayNight @Composable internal fun TextComposerFormattingPreview() = ElementPreview { @@ -589,6 +672,22 @@ internal fun TextComposerFormattingPreview() = ElementPreview { } } +@PreviewsDayNight +@Composable +internal fun TextComposerFormattingNotEncryptedPreview() = ElementPreview { + PreviewColumn( + items = aTextEditorStateRichList(isRoomEncrypted = false) + ) { _, textEditorState -> + ATextComposer( + state = textEditorState, + voiceMessageState = VoiceMessageState.Idle, + showTextFormatting = true, + composerMode = MessageComposerMode.Normal, + enableVoiceMessages = true, + ) + } +} + @PreviewsDayNight @Composable internal fun TextComposerEditPreview() = ElementPreview { @@ -604,6 +703,21 @@ internal fun TextComposerEditPreview() = ElementPreview { } } +@PreviewsDayNight +@Composable +internal fun TextComposerEditNotEncryptedPreview() = ElementPreview { + PreviewColumn( + items = aTextEditorStateRichList(isRoomEncrypted = false) + ) { _, textEditorState -> + ATextComposer( + state = textEditorState, + voiceMessageState = VoiceMessageState.Idle, + composerMode = aMessageComposerModeEdit(), + enableVoiceMessages = true, + ) + } +} + @PreviewsDayNight @Composable internal fun TextComposerEditCaptionPreview() = ElementPreview { @@ -673,6 +787,31 @@ internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider } } +@Preview( + name = DAY_MODE_NAME, + heightDp = 800, +) +@Preview( + name = NIGHT_MODE_NAME, + uiMode = Configuration.UI_MODE_NIGHT_YES, + heightDp = 800, +) +@Composable +internal fun TextComposerReplyNotEncryptedPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview { + PreviewColumn( + items = aTextEditorStateRichList(isRoomEncrypted = false) + ) { _, textEditorState -> + ATextComposer( + state = textEditorState, + voiceMessageState = VoiceMessageState.Idle, + composerMode = aMessageComposerModeReply( + replyToDetails = inReplyToDetails, + ), + enableVoiceMessages = true, + ) + } +} + @PreviewsDayNight @Composable internal fun TextComposerCaptionPreview() = ElementPreview { @@ -733,6 +872,47 @@ internal fun TextComposerVoicePreview() = ElementPreview { } } +@PreviewsDayNight +@Composable +internal fun TextComposerVoiceNotEncryptedPreview() = ElementPreview { + PreviewColumn( + items = persistentListOf( + VoiceMessageState.Recording(61.seconds, createFakeWaveform()), + VoiceMessageState.Preview( + isSending = false, + isPlaying = false, + showCursor = false, + waveform = createFakeWaveform(), + time = 0.seconds, + playbackProgress = 0.0f + ), + VoiceMessageState.Preview( + isSending = false, + isPlaying = true, + showCursor = true, + waveform = createFakeWaveform(), + time = 3.seconds, + playbackProgress = 0.2f + ), + VoiceMessageState.Preview( + isSending = true, + isPlaying = false, + showCursor = false, + waveform = createFakeWaveform(), + time = 61.seconds, + playbackProgress = 0.0f + ), + ) + ) { _, voiceMessageState -> + ATextComposer( + state = aTextEditorStateRich(initialFocus = true, isRoomEncrypted = false), + voiceMessageState = voiceMessageState, + composerMode = MessageComposerMode.Normal, + enableVoiceMessages = true, + ) + } +} + @Composable private fun PreviewColumn( items: ImmutableList, @@ -740,6 +920,7 @@ private fun PreviewColumn( ) { Column { items.forEachIndexed { index, item -> + HorizontalDivider() Box( modifier = Modifier.height(IntrinsicSize.Min) ) { @@ -776,6 +957,7 @@ private fun ATextComposer( onTyping = {}, onReceiveSuggestion = {}, resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, + resolveAtRoomMentionDisplay = { TextDisplay.Plain }, onSelectRichContent = null, ) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt deleted file mode 100644 index c4b13507745..00000000000 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.textcomposer.components - -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton -import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.textcomposer.R - -@Composable -internal fun ComposerOptionsButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - IconButton( - modifier = modifier - .size(48.dp), - onClick = onClick - ) { - Icon( - modifier = Modifier.size(30.dp), - resourceId = CommonDrawables.ic_plus_composer, - contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), - tint = ElementTheme.colors.iconPrimary, - ) - } -} - -@PreviewsDayNight -@Composable -internal fun ComposerOptionsButtonPreview() = ElementPreview { - ComposerOptionsButton(onClick = {}) -} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt deleted file mode 100644 index 229cfc8275f..00000000000 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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. - */ - -package io.element.android.libraries.textcomposer.components - -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton -import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.ui.strings.CommonStrings - -@Composable -internal fun DismissTextFormattingButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - IconButton( - modifier = modifier - .size(48.dp), - onClick = onClick - ) { - Icon( - modifier = Modifier.size(30.dp), - resourceId = CommonDrawables.ic_cancel, - contentDescription = stringResource(CommonStrings.action_close), - tint = ElementTheme.colors.iconPrimary, - ) - } -} - -@PreviewsDayNight -@Composable -internal fun DismissTextFormattingButtonPreview() = ElementPreview { - DismissTextFormattingButton(onClick = {}) -} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt index c90203b6ae9..48ad19f812e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -21,6 +22,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -32,9 +35,10 @@ import io.element.android.libraries.designsystem.theme.iconSuccessPrimaryBackgro @Composable internal fun FormattingOption( state: FormattingOptionState, + toggleable: Boolean, onClick: () -> Unit, imageVector: ImageVector, - contentDescription: String?, + contentDescription: String, modifier: Modifier = Modifier, ) { val backgroundColor = when (state) { @@ -52,6 +56,7 @@ internal fun FormattingOption( modifier = modifier .clickable( onClick = onClick, + enabled = state != FormattingOptionState.Disabled, interactionSource = remember { MutableInteractionSource() }, indication = ripple( bounded = false, @@ -59,6 +64,20 @@ internal fun FormattingOption( ), ) .size(48.dp) + .then( + if (toggleable) { + Modifier.toggleable( + value = state == FormattingOptionState.Selected, + enabled = state != FormattingOptionState.Disabled, + onValueChange = { onClick() }, + ) + } else { + Modifier + } + ) + .clearAndSetSemantics { + this.contentDescription = contentDescription + } ) { Box( modifier = Modifier @@ -84,21 +103,24 @@ internal fun FormattingOptionPreview() = ElementPreview { Row { FormattingOption( state = FormattingOptionState.Default, + toggleable = false, onClick = { }, imageVector = CompoundIcons.Bold(), - contentDescription = null, + contentDescription = "", ) FormattingOption( state = FormattingOptionState.Selected, + toggleable = true, onClick = { }, imageVector = CompoundIcons.Italic(), - contentDescription = null, + contentDescription = "", ) FormattingOption( state = FormattingOptionState.Disabled, + toggleable = false, onClick = { }, imageVector = CompoundIcons.Underline(), - contentDescription = null, + contentDescription = "", ) } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt index aeeda862693..55e4350fb29 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.LinearGradientShader import androidx.compose.ui.graphics.ShaderBrush -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 @@ -32,7 +31,6 @@ import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.textcomposer.model.MessageComposerMode -import io.element.android.libraries.ui.strings.CommonStrings /** * Send button for the message composer. @@ -60,10 +58,6 @@ internal fun SendButton( composerMode.isEditing -> 0.dp else -> 2.dp } - val contentDescription = when { - composerMode.isEditing -> stringResource(CommonStrings.action_edit) - else -> stringResource(CommonStrings.action_send) - } Box( modifier = Modifier .clip(CircleShape) @@ -81,7 +75,8 @@ internal fun SendButton( .padding(start = iconStartPadding) .align(Alignment.Center), imageVector = iconVector, - contentDescription = contentDescription, + // Note: accessibility is managed in TextComposer. + contentDescription = null, tint = if (canSendMessage) { if (ElementTheme.colors.isLight) { ElementTheme.colors.iconOnSolidPrimary diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt index 911707022ca..08c1bec23a3 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt @@ -104,24 +104,28 @@ internal fun TextFormatting( ) { FormattingOption( state = state.actions[ComposerAction.BOLD].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.Bold) }, imageVector = CompoundIcons.Bold(), contentDescription = stringResource(R.string.rich_text_editor_format_bold) ) FormattingOption( state = state.actions[ComposerAction.ITALIC].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.Italic) }, imageVector = CompoundIcons.Italic(), contentDescription = stringResource(R.string.rich_text_editor_format_italic) ) FormattingOption( state = state.actions[ComposerAction.UNDERLINE].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.Underline) }, imageVector = CompoundIcons.Underline(), contentDescription = stringResource(R.string.rich_text_editor_format_underline) ) FormattingOption( state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.StrikeThrough) }, imageVector = CompoundIcons.Strikethrough(), contentDescription = stringResource(R.string.rich_text_editor_format_strikethrough) @@ -141,6 +145,7 @@ internal fun TextFormatting( FormattingOption( state = state.actions[ComposerAction.LINK].toButtonState(), + toggleable = true, onClick = { linkDialogAction = state.linkAction }, imageVector = CompoundIcons.Link(), contentDescription = stringResource(R.string.rich_text_editor_link) @@ -148,42 +153,49 @@ internal fun TextFormatting( FormattingOption( state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), + toggleable = true, onClick = { onToggleListClick(ordered = false) }, imageVector = CompoundIcons.ListBulleted(), contentDescription = stringResource(R.string.rich_text_editor_bullet_list) ) FormattingOption( state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), + toggleable = true, onClick = { onToggleListClick(ordered = true) }, imageVector = CompoundIcons.ListNumbered(), contentDescription = stringResource(R.string.rich_text_editor_numbered_list) ) FormattingOption( state = state.actions[ComposerAction.INDENT].toButtonState(), + toggleable = false, onClick = { onIndentClick() }, imageVector = CompoundIcons.IndentIncrease(), contentDescription = stringResource(R.string.rich_text_editor_indent) ) FormattingOption( state = state.actions[ComposerAction.UNINDENT].toButtonState(), + toggleable = false, onClick = { onUnindentClick() }, imageVector = CompoundIcons.IndentDecrease(), contentDescription = stringResource(R.string.rich_text_editor_unindent) ) FormattingOption( state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.InlineCode) }, imageVector = CompoundIcons.InlineCode(), contentDescription = stringResource(R.string.rich_text_editor_inline_code) ) FormattingOption( state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), + toggleable = true, onClick = { onCodeBlockClick() }, imageVector = CompoundIcons.Code(), contentDescription = stringResource(R.string.rich_text_editor_code_block) ) FormattingOption( state = state.actions[ComposerAction.QUOTE].toButtonState(), + toggleable = true, onClick = { onQuoteClick() }, imageVector = CompoundIcons.Quote(), contentDescription = stringResource(R.string.rich_text_editor_quote) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index 4e69a1b6a6c..d965a3d0d6d 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -84,7 +84,7 @@ internal fun VoiceMessagePreview( Text( text = time.formatShort(), - color = ElementTheme.materialColors.secondary, + color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodySmMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt index 03965566e50..3a92dfbeb08 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback -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 @@ -26,7 +25,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent -import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun VoiceMessageRecorderButton( @@ -70,7 +68,8 @@ private fun StartButton( Icon( modifier = Modifier.size(24.dp), imageVector = CompoundIcons.MicOn(), - contentDescription = stringResource(CommonStrings.a11y_voice_message_record), + // Note: accessibility is managed in TextComposer. + contentDescription = null, tint = ElementTheme.colors.iconSecondary, ) } @@ -95,7 +94,8 @@ private fun StopButton( Icon( modifier = Modifier.size(24.dp), resourceId = CommonDrawables.ic_stop, - contentDescription = stringResource(CommonStrings.a11y_voice_message_stop_recording), + // Note: accessibility is managed in TextComposer. + contentDescription = null, tint = ElementTheme.colors.iconOnSolidPrimary, ) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownEditText.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownEditText.kt index 7d7a2c00e96..413760e209c 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownEditText.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownEditText.kt @@ -38,6 +38,8 @@ internal class MarkdownEditText( } } + // When using the EditText within a Compose layout, we need to override focusSearch to prevent the default behavior + // Otherwise it can try searching for focusable nodes in the Compose hierarchy while they're being laid out, which will crash override fun focusSearch(direction: Int): View? { return null } 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 ee0ec181cad..ffce571dc43 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 @@ -8,6 +8,7 @@ package io.element.android.libraries.textcomposer.components.markdown import android.content.ClipData +import android.content.res.ColorStateList import android.graphics.Color import android.net.Uri import android.text.Editable @@ -18,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.text.getSpans @@ -26,13 +28,13 @@ import androidx.core.view.OnReceiveContentListener import androidx.core.view.ViewCompat import androidx.core.view.setPadding import androidx.core.widget.addTextChangedListener +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle -import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme +import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanUpdater import io.element.android.libraries.textcomposer.mentions.MentionSpan -import io.element.android.libraries.textcomposer.mentions.updateMentionStyles import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType @@ -44,14 +46,13 @@ import io.element.android.wysiwyg.compose.internal.applyStyleInCompose @Composable fun MarkdownTextInput( state: MarkdownTextEditorState, - subcomposing: Boolean, + placeholder: String, + placeholderColor: androidx.compose.ui.graphics.Color, onTyping: (Boolean) -> Unit, onReceiveSuggestion: (Suggestion?) -> Unit, richTextEditorStyle: RichTextEditorStyle, onSelectRichContent: ((Uri) -> Unit)?, ) { - val canUpdateState = !subcomposing - // Copied from io.element.android.wysiwyg.internal.utils.UriContentListener class ReceiveUriContentListener( private val onContent: (uri: Uri) -> Unit, @@ -75,7 +76,7 @@ fun MarkdownTextInput( } } - val mentionSpanTheme = LocalMentionSpanTheme.current + val mentionSpanUpdater = LocalMentionSpanUpdater.current AndroidView( modifier = Modifier @@ -88,60 +89,55 @@ fun MarkdownTextInput( setBackgroundColor(Color.TRANSPARENT) val text = state.text.value() setText(text) + setHint(placeholder) + setHintTextColor(ColorStateList.valueOf(placeholderColor.toArgb())) inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT - if (canUpdateState) { - val textRange = 0..text.length - setSelection(state.selection.first.coerceIn(textRange), state.selection.last.coerceIn(textRange)) - setOnFocusChangeListener { _, hasFocus -> - state.hasFocus = hasFocus - } - addTextChangedListener { editable -> - onTyping(!editable.isNullOrEmpty()) - state.text.update(editable, false) - state.lineCount = lineCount + val textRange = 0..text.length + setSelection(state.selection.first.coerceIn(textRange), state.selection.last.coerceIn(textRange)) + setOnFocusChangeListener { _, hasFocus -> + state.hasFocus = hasFocus + } + addTextChangedListener { editable -> + onTyping(!editable.isNullOrEmpty()) + state.text.update(editable, false) + state.lineCount = lineCount - state.currentSuggestion = editable?.checkSuggestionNeeded() - onReceiveSuggestion(state.currentSuggestion) - } - onSelectionChangeListener = { selStart, selEnd -> - state.selection = selStart..selEnd - state.currentSuggestion = editableText.checkSuggestionNeeded() - onReceiveSuggestion(state.currentSuggestion) - } - if (onSelectRichContent != null) { - ViewCompat.setOnReceiveContentListener( - this, - arrayOf("image/*"), - ReceiveUriContentListener { onSelectRichContent(it) } - ) - } - state.requestFocusAction = { this.requestFocus() } + state.currentSuggestion = editable?.checkSuggestionNeeded() + onReceiveSuggestion(state.currentSuggestion) + } + onSelectionChangeListener = { selStart, selEnd -> + state.selection = selStart..selEnd + state.currentSuggestion = editableText.checkSuggestionNeeded() + onReceiveSuggestion(state.currentSuggestion) + } + if (onSelectRichContent != null) { + ViewCompat.setOnReceiveContentListener( + this, + arrayOf("image/*"), + ReceiveUriContentListener { onSelectRichContent(it) } + ) } + state.requestFocusAction = { this.requestFocus() } } }, update = { editText -> editText.applyStyleInCompose(richTextEditorStyle) - + val text = state.text.value() + mentionSpanUpdater.updateMentionSpans(text) if (state.text.needsDisplaying()) { - val text = state.text.value() - mentionSpanTheme.updateMentionStyles(text) editText.updateEditableText(text) - if (canUpdateState) { - state.text.update(editText.editableText, false) - } + state.text.update(editText.editableText, false) } - if (canUpdateState) { - val newSelectionStart = state.selection.first - val newSelectionEnd = state.selection.last - val currentTextRange = 0..editText.editableText.length - val didSelectionChange = { editText.selectionStart != newSelectionStart || editText.selectionEnd != newSelectionEnd } - val isNewSelectionValid = { newSelectionStart in currentTextRange && newSelectionEnd in currentTextRange } - if (didSelectionChange() && isNewSelectionValid()) { - editText.setSelection(state.selection.first, state.selection.last) - } + val newSelectionStart = state.selection.first + val newSelectionEnd = state.selection.last + val currentTextRange = 0..editText.editableText.length + val didSelectionChange = { editText.selectionStart != newSelectionStart || editText.selectionEnd != newSelectionEnd } + val isNewSelectionValid = { newSelectionStart in currentTextRange && newSelectionEnd in currentTextRange } + if (didSelectionChange() && isNewSelectionValid()) { + editText.setSelection(state.selection.first, state.selection.last) } } ) @@ -186,7 +182,8 @@ internal fun MarkdownTextInputPreview() { val style = ElementRichTextEditorStyle.composerStyle(hasFocus = true) MarkdownTextInput( state = aMarkdownTextEditorState(initialText = "Hello, World!"), - subcomposing = false, + placeholder = "Placeholder", + placeholderColor = ElementTheme.colors.textSecondary, onTyping = {}, onReceiveSuggestion = {}, richTextEditorStyle = style, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/StableCharSequence.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/StableCharSequence.kt index 6d861894a65..bc1de30826d 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/StableCharSequence.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/StableCharSequence.kt @@ -16,11 +16,11 @@ import io.element.android.libraries.core.extensions.orEmpty @Stable class StableCharSequence(initialText: CharSequence = "") { - private var value by mutableStateOf(SpannableString(initialText)) + private var value by mutableStateOf(SpannableString.valueOf(initialText)) private var needsDisplaying by mutableStateOf(false) fun update(newText: CharSequence?, needsDisplaying: Boolean) { - value = SpannableString(newText.orEmpty()) + value = SpannableString.valueOf(newText.orEmpty()) this.needsDisplaying = needsDisplaying } 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 2f55b96c9fe..76587d6e121 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 @@ -11,119 +11,153 @@ import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF import android.graphics.Typeface +import android.text.TextPaint +import android.text.TextUtils import android.text.style.ReplacementSpan import androidx.core.text.getSpans -import io.element.android.libraries.core.extensions.orEmpty +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.wysiwyg.view.spans.CustomMentionSpan -import kotlin.math.min import kotlin.math.roundToInt +/** + * A span that represents a mention (user, room, etc.) in text. + * @param type The type of mention this span represents. + */ class MentionSpan( - text: String, - val rawValue: String, - val type: Type, + val type: MentionType, ) : ReplacementSpan() { - companion object { - private const val MAX_LENGTH = 20 - } + private val backgroundPaint = Paint() + private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG) - var backgroundColor: Int = 0 - var textColor: Int = 0 - var startPadding: Int = 0 - var endPadding: Int = 0 - var typeface: Typeface = Typeface.DEFAULT + private var backgroundColor: Int = 0 + private var textColor: Int = 0 + private var startPadding: Int = 0 + private var endPadding: Int = 0 + private var typeface: Typeface = Typeface.DEFAULT - private var textWidth = 0 - private val backgroundPaint = Paint().apply { - isAntiAlias = true - color = backgroundColor - } + private var measuredTextWidth = 0 - var text: String = text - set(value) { - field = value - mentionText = getActualText(text) - } + // The formatted display text, will be set by the formatter + var displayText: CharSequence = "" + private set - private var mentionText: CharSequence = getActualText(text) + /** + * Updates the visual properties of this span. + */ + fun updateTheme(mentionSpanTheme: MentionSpanTheme) { + val isCurrentUser = when (type) { + is MentionType.User -> type.userId == mentionSpanTheme.currentUserId + else -> false + } - fun update(mentionSpanTheme: MentionSpanTheme) { - val isCurrentUser = rawValue == mentionSpanTheme.currentUserId?.value backgroundColor = when (type) { - Type.USER -> if (isCurrentUser) mentionSpanTheme.currentUserBackgroundColor else mentionSpanTheme.otherBackgroundColor - Type.ROOM -> mentionSpanTheme.otherBackgroundColor - Type.EVERYONE -> mentionSpanTheme.currentUserBackgroundColor + is MentionType.User -> if (isCurrentUser) mentionSpanTheme.currentUserBackgroundColor else mentionSpanTheme.otherBackgroundColor + is MentionType.Everyone -> mentionSpanTheme.currentUserBackgroundColor + is MentionType.Room -> mentionSpanTheme.otherBackgroundColor + is MentionType.Message -> mentionSpanTheme.otherBackgroundColor } + textColor = when (type) { - Type.USER -> if (isCurrentUser) mentionSpanTheme.currentUserTextColor else mentionSpanTheme.otherTextColor - Type.ROOM -> mentionSpanTheme.otherTextColor - Type.EVERYONE -> mentionSpanTheme.currentUserTextColor + is MentionType.User -> if (isCurrentUser) mentionSpanTheme.currentUserTextColor else mentionSpanTheme.otherTextColor + is MentionType.Everyone -> mentionSpanTheme.currentUserTextColor + is MentionType.Room -> mentionSpanTheme.otherTextColor + is MentionType.Message -> mentionSpanTheme.otherTextColor } - backgroundPaint.color = backgroundColor + val (startPaddingPx, endPaddingPx) = mentionSpanTheme.paddingValuesPx.value startPadding = startPaddingPx endPadding = endPaddingPx typeface = mentionSpanTheme.typeface.value } - override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { - paint.typeface = typeface - textWidth = paint.measureText(mentionText, 0, mentionText.length).roundToInt() - return textWidth + startPadding + endPadding + /** + * Updates the display text using a formatter. + */ + fun updateDisplayText(formatter: MentionSpanFormatter) { + displayText = formatter.formatDisplayText(type) } - override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { + override fun getSize( + paint: Paint, + text: CharSequence?, + start: Int, + end: Int, + fm: Paint.FontMetricsInt? + ): Int { + textPaint.set(paint) + textPaint.typeface = typeface + // Measure the full text width without truncation + measuredTextWidth = textPaint.measureText(displayText, 0, displayText.length).roundToInt() + return measuredTextWidth + startPadding + endPadding + } + + override fun draw( + canvas: Canvas, + text: CharSequence?, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + 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 rect = RectF(x, top.toFloat(), x + textWidth + startPadding + endPadding, y.toFloat() + extraVerticalSpace) + 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, y.toFloat() + extraVerticalSpace) val radius = rect.height() / 2 canvas.drawRoundRect(rect, radius, radius, backgroundPaint) - paint.color = textColor - paint.typeface = typeface - canvas.drawText(mentionText, 0, mentionText.length, x + startPadding, y.toFloat(), paint) - } - private fun getActualText(text: String): CharSequence { - return buildString { - val mentionText = text.orEmpty() - when (type) { - Type.USER -> { - if (text.firstOrNull() != '@') { - append("@") - } - } - Type.ROOM -> { - if (text.firstOrNull() != '#') { - append("#") - } - } - Type.EVERYONE -> Unit - } - append(mentionText.substring(0, min(mentionText.length, MAX_LENGTH))) - if (mentionText.length > MAX_LENGTH) { - append("…") - } + textPaint.set(paint) + textPaint.color = textColor + textPaint.typeface = typeface + + val availableWidthForText = availableWidth - startPadding - endPadding + val textToDraw = if (measuredTextWidth > availableWidthForText) { + TextUtils.ellipsize( + displayText, + textPaint, + availableWidthForText, + TextUtils.TruncateAt.END + ) + } else { + displayText } + canvas.drawText(textToDraw, 0, textToDraw.length, x + startPadding, y.toFloat(), textPaint) } +} - enum class Type { - USER, - ROOM, - EVERYONE, - } +/** + * Sealed interface representing different types of mentions. + */ +sealed interface MentionType { + data class User(val userId: UserId) : MentionType + data class Room(val roomIdOrAlias: RoomIdOrAlias) : MentionType + data class Message(val roomIdOrAlias: RoomIdOrAlias, val eventId: EventId) : MentionType + data object Everyone : MentionType } -fun CharSequence.getMentionSpans(): List { +/** + * Extension function to get all MentionSpans from a CharSequence. + */ +fun CharSequence.getMentionSpans(start: Int = 0, end: Int = length): List { return if (this is android.text.Spanned) { - val customMentionSpans = getSpans() - if (customMentionSpans.isNotEmpty()) { - // If we have custom mention spans created by the RTE, we need to extract the provided spans and filter them - customMentionSpans.map { it.providedSpan }.filterIsInstance() - } else { - // Otherwise try to get the spans directly - getSpans().toList() - } + // If we have custom mention spans created by the RTE, we need to extract the provided spans and filter them + val customMentionSpans = getSpans(start, end) + .map { it.providedSpan } + .filterIsInstance() + // Collect all direct mention spans + val directMentionSpans = getSpans(start, end) + // Return the union of both + customMentionSpans + directMentionSpans } else { emptyList() } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanFormatter.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanFormatter.kt new file mode 100644 index 00000000000..f4b5f96bf49 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanFormatter.kt @@ -0,0 +1,75 @@ +/* + * 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. + */ + +package io.element.android.libraries.textcomposer.mentions + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache +import io.element.android.libraries.matrix.ui.messages.RoomNamesCache +import javax.inject.Inject + +private const val EVERYONE_DISPLAY_TEXT = "@room" +private const val BUBBLE_ICON = "\uD83D\uDCAC" // 💬 + +interface MentionSpanFormatter { + fun formatDisplayText(mentionType: MentionType): CharSequence +} + +/** + * Formatter for MentionSpan display text. + * This class is responsible for formatting the display text of a MentionSpan + * based on its MentionType and context. + */ +@ContributesBinding(RoomScope::class) +class DefaultMentionSpanFormatter @Inject constructor( + private val roomMemberProfilesCache: RoomMemberProfilesCache, + private val roomNamesCache: RoomNamesCache, +) : MentionSpanFormatter { + /** + * Format the display text for a mention span. + * + * @param mentionType The type of mention + * @return The formatted display text + */ + override fun formatDisplayText(mentionType: MentionType): CharSequence { + return when (mentionType) { + is MentionType.User -> formatUserMention(mentionType.userId) + is MentionType.Room -> formatRoomMention(mentionType.roomIdOrAlias) + is MentionType.Message -> formatMessageMention(mentionType.roomIdOrAlias) + is MentionType.Everyone -> EVERYONE_DISPLAY_TEXT + } + } + + private fun formatUserMention(userId: UserId): String { + // Try to get the display name from cache, fallback to userId + val displayName = roomMemberProfilesCache.getDisplayName(userId) + return if (displayName != null) { + "@$displayName" + } else { + userId.value + } + } + + private fun formatRoomMention(roomIdOrAlias: RoomIdOrAlias): String { + val displayName = roomNamesCache.getDisplayName(roomIdOrAlias) + return if (displayName != null) { + "#$displayName" + } else { + roomIdOrAlias.identifier + } + } + + private fun formatMessageMention( + roomIdOrAlias: RoomIdOrAlias, + ): String { + val roomMention = formatRoomMention(roomIdOrAlias) + return "$BUBBLE_ICON > $roomMention" + } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt index 63197ffb9c8..a2338972543 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt @@ -7,46 +7,118 @@ package io.element.android.libraries.textcomposer.mentions -import androidx.compose.runtime.Stable +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser import javax.inject.Inject -@Stable +private const val EVERYONE_MENTION_TEXT = "@room" + +/** + * Provider for [MentionSpan]s. + */ open class MentionSpanProvider @Inject constructor( private val permalinkParser: PermalinkParser, + private val mentionSpanFormatter: MentionSpanFormatter, + private val mentionSpanTheme: MentionSpanTheme, ) { - fun getMentionSpanFor(text: String, url: String): MentionSpan { + /** + * Creates a mention span from a text and URL. + * + * @param text The text associated with the mention + * @param url The URL associated with the mention + * @return A mention span if the URL can be parsed as a permalink, null otherwise + */ + fun getMentionSpanFor(text: String, url: String): MentionSpan? { val permalinkData = permalinkParser.parse(url) - return when { - permalinkData is PermalinkData.UserLink -> { - MentionSpan( - text = text, - rawValue = permalinkData.userId.toString(), - type = MentionSpan.Type.USER, - ) - } - text == "@room" && permalinkData is PermalinkData.FallbackLink -> { - MentionSpan( - text = text, - rawValue = "@room", - type = MentionSpan.Type.EVERYONE, - ) + return getMentionSpanFor(text, permalinkData) + } + + /** + * Creates a mention span from a text and permalink data. + * + * @param text The text associated with the mention + * @param permalinkData The permalink data associated with the mention + * @return A mention span based on the permalink data, null if the permalink data is not supported + */ + private fun getMentionSpanFor(text: String, permalinkData: PermalinkData): MentionSpan? { + return when (permalinkData) { + is PermalinkData.UserLink -> { + createUserMentionSpan(permalinkData.userId) } - permalinkData is PermalinkData.RoomLink -> { - MentionSpan( - text = text, - rawValue = permalinkData.roomIdOrAlias.identifier, - type = MentionSpan.Type.ROOM, - ) + is PermalinkData.RoomLink -> { + val eventId = permalinkData.eventId + if (eventId != null) { + createMessageMentionSpan(permalinkData.roomIdOrAlias, eventId) + } else { + createRoomMentionSpan(permalinkData.roomIdOrAlias) + } } - else -> { - MentionSpan( - text = text, - rawValue = text, - type = MentionSpan.Type.ROOM, - ) + is PermalinkData.FallbackLink -> { + if (text == EVERYONE_MENTION_TEXT) { + createEveryoneMentionSpan() + } else { + null + } } + else -> null + } + } + + /** + * Create a mention span for a user mention. + * + * @param userId The user ID + * @return A mention span for the user + */ + fun createUserMentionSpan(userId: UserId): MentionSpan { + return MentionSpan(type = MentionType.User(userId = userId)).apply { + updateDisplayText(mentionSpanFormatter) + updateTheme(mentionSpanTheme) + } + } + + /** + * Create a mention span for a room mention. + * + * @param roomIdOrAlias The room ID or alias + * @return A mention span for the room + */ + fun createRoomMentionSpan(roomIdOrAlias: RoomIdOrAlias): MentionSpan { + return MentionSpan(MentionType.Room(roomIdOrAlias)).apply { + updateDisplayText(mentionSpanFormatter) + updateTheme(mentionSpanTheme) + } + } + + /** + * Create a mention span for a message mention. + * + * @param roomIdOrAlias The room ID or alias where the message is located + * @param eventId The event ID of the message + * @return A mention span for the message + */ + fun createMessageMentionSpan( + roomIdOrAlias: RoomIdOrAlias, + eventId: EventId, + ): MentionSpan { + return MentionSpan(type = MentionType.Message(roomIdOrAlias, eventId)).apply { + updateTheme(mentionSpanTheme) + updateDisplayText(mentionSpanFormatter) + } + } + + /** + * Create a mention span for @room (everyone). + * + * @return A mention span for @room + */ + fun createEveryoneMentionSpan(): MentionSpan { + return MentionSpan(type = MentionType.Everyone).apply { + updateTheme(mentionSpanTheme) + updateDisplayText(mentionSpanFormatter) } } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanTheme.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanTheme.kt index b81fb45b326..edd5cfe9dd2 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanTheme.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanTheme.kt @@ -15,11 +15,9 @@ import android.view.ViewGroup import android.widget.TextView import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection @@ -34,6 +32,9 @@ import io.element.android.libraries.designsystem.theme.currentUserMentionPillBac import io.element.android.libraries.designsystem.theme.currentUserMentionPillText import io.element.android.libraries.designsystem.theme.mentionPillBackground import io.element.android.libraries.designsystem.theme.mentionPillText +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias @@ -45,13 +46,14 @@ import javax.inject.Inject /** * Theme used for mention spans. * To make this work, you need to: - * 1. Provide [LocalMentionSpanTheme] in a composable that wraps the ones where you want to use mentions. - * 2. Call [MentionSpanTheme.updateStyles] with the current [UserId] so the colors and sizes are computed. - * 3. Use either [MentionSpanTheme.updateMentionStyles] or [MentionSpan.update] to update the styles of the mention spans. + * 1. Call [MentionSpanTheme.updateStyles] so the colors and sizes are computed. + * 2. Use either [MentionSpanTheme.updateMentionStyles] or [MentionSpan.updateTheme] to update the styles of the mention spans. */ @Stable -class MentionSpanTheme @Inject constructor() { - internal var currentUserId: UserId? = null +@SingleIn(SessionScope::class) +class MentionSpanTheme(val currentUserId: UserId) { + @Inject constructor(matrixClient: MatrixClient) : this(matrixClient.sessionId) + internal var currentUserTextColor: Int = 0 internal var currentUserBackgroundColor: Int = Color.WHITE internal var otherTextColor: Int = 0 @@ -66,8 +68,7 @@ class MentionSpanTheme @Inject constructor() { */ @Suppress("ComposableNaming") @Composable - fun updateStyles(currentUserId: UserId) { - this.currentUserId = currentUserId + fun updateStyles() { currentUserTextColor = ElementTheme.colors.currentUserMentionPillText.toArgb() currentUserBackgroundColor = ElementTheme.colors.currentUserMentionPillBackground.toArgb() otherTextColor = ElementTheme.colors.mentionPillText.toArgb() @@ -93,24 +94,28 @@ fun MentionSpanTheme.updateMentionStyles(charSequence: CharSequence) { val spanned = charSequence as? Spanned ?: return val mentionSpans = spanned.getMentionSpans() for (span in mentionSpans) { - span.update(this) + span.updateTheme(this) } } -/** - * Composition local containing the current [MentionSpanTheme]. - */ -val LocalMentionSpanTheme = staticCompositionLocalOf { - MentionSpanTheme() -} - - @PreviewsDayNight - @Composable - internal fun MentionSpanThemePreview() { +@PreviewsDayNight +@Composable +internal fun MentionSpanThemePreview() { ElementPreview { - val mentionSpanTheme = remember { MentionSpanTheme() } + val mentionSpanTheme = remember { MentionSpanTheme(UserId("@me:matrix.org")) } val provider = remember { MentionSpanProvider( + mentionSpanTheme = mentionSpanTheme, + mentionSpanFormatter = object : MentionSpanFormatter { + override fun formatDisplayText(mentionType: MentionType): CharSequence { + return when (mentionType) { + is MentionType.User -> mentionType.userId.value + is MentionType.Room -> mentionType.roomIdOrAlias.identifier + is MentionType.Message -> "\uD83D\uDCAC️ > ${mentionType.roomIdOrAlias.identifier}" + is MentionType.Everyone -> "@room" + } + } + }, permalinkParser = object : PermalinkParser { override fun parse(uriString: String): PermalinkData { return when (uriString) { @@ -133,36 +138,31 @@ val LocalMentionSpanTheme = staticCompositionLocalOf { fun mentionSpanMe() = provider.getMentionSpanFor("mention", "https://matrix.to/#/@me:matrix.org") fun mentionSpanOther() = provider.getMentionSpanFor("mention", "https://matrix.to/#/@other:matrix.org") fun mentionSpanRoom() = provider.getMentionSpanFor("room:matrix.org", "https://matrix.to/#/#room:matrix.org") - fun mentionSpanEveryone() = provider.getMentionSpanFor("@room", "@room") - mentionSpanTheme.updateStyles(currentUserId = UserId("@me:matrix.org")) + fun mentionSpanEveryone() = provider.createEveryoneMentionSpan() + mentionSpanTheme.updateStyles() - CompositionLocalProvider( - LocalMentionSpanTheme provides mentionSpanTheme - ) { - AndroidView(factory = { context -> - TextView(context).apply { - includeFontPadding = false - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - text = buildSpannedString { - append("This is a ") - append("@mention", mentionSpanMe(), 0) - append(" to the current user and this is a ") - append("@mention", mentionSpanOther(), 0) - append(" to other user. This is for everyone in the ") - append("@room", mentionSpanEveryone(), 0) - append(". This one is for a link to another room: ") - append("#room:matrix.org", mentionSpanRoom(), 0) - append("\n\n") - append("This ") - append("mention", mentionSpanMe(), 0) - append(" didn't have an '@' and it was automatically added, same as this ") - append("room:matrix.org", mentionSpanRoom(), 0) - append(" one, which had no leading '#'.") - } - mentionSpanTheme.updateMentionStyles(text) - setTextColor(textColor) + AndroidView(factory = { context -> + TextView(context).apply { + includeFontPadding = false + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + text = buildSpannedString { + append("This is a ") + append("@mention", mentionSpanMe(), 0) + append(" to the current user and this is a ") + append("@mention", mentionSpanOther(), 0) + append(" to other user. This is for everyone in the ") + append("@room", mentionSpanEveryone(), 0) + append(". This one is for a link to another room: ") + append("#room:matrix.org", mentionSpanRoom(), 0) + append("\n\n") + append("This ") + append("mention", mentionSpanMe(), 0) + append(" didn't have an '@' and it was automatically added, same as this ") + append("room:matrix.org", mentionSpanRoom(), 0) + append(" one, which had no leading '#'.") } - }) - } + setTextColor(textColor) + } + }) } - } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanUpdater.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanUpdater.kt new file mode 100644 index 00000000000..94d74a72e34 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanUpdater.kt @@ -0,0 +1,67 @@ +/* + * 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.textcomposer.mentions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache +import io.element.android.libraries.matrix.ui.messages.RoomNamesCache +import javax.inject.Inject + +interface MentionSpanUpdater { + fun updateMentionSpans(text: CharSequence): CharSequence + + @Composable + fun rememberMentionSpans(text: CharSequence): CharSequence +} + +@ContributesBinding(RoomScope::class) +class DefaultMentionSpanUpdater @Inject constructor( + private val formatter: MentionSpanFormatter, + private val theme: MentionSpanTheme, + private val roomMemberProfilesCache: RoomMemberProfilesCache, + private val roomNamesCache: RoomNamesCache, +) : MentionSpanUpdater { + @Composable + override fun rememberMentionSpans(text: CharSequence): CharSequence { + val isLightTheme = ElementTheme.isLightTheme + val roomInfoCacheUpdate by roomNamesCache.updateFlow.collectAsState(0) + val roomMemberProfilesCacheUpdate by roomMemberProfilesCache.updateFlow.collectAsState(0) + return remember(text, roomInfoCacheUpdate, roomMemberProfilesCacheUpdate, isLightTheme) { + updateMentionSpans(text) + text + } + } + + override fun updateMentionSpans(text: CharSequence): CharSequence { + for (mentionSpan in text.getMentionSpans()) { + mentionSpan.updateTheme(theme) + mentionSpan.updateDisplayText(formatter) + } + return text + } +} + +private object NoOpMentionSpanUpdater : MentionSpanUpdater { + override fun updateMentionSpans(text: CharSequence): CharSequence { + return text + } + + @Composable + override fun rememberMentionSpans(text: CharSequence): CharSequence { + return text + } +} + +val LocalMentionSpanUpdater = staticCompositionLocalOf { NoOpMentionSpanUpdater } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Fixtures.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Fixtures.kt index 26b398db045..d9a71273f1e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Fixtures.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Fixtures.kt @@ -12,12 +12,14 @@ import io.element.android.wysiwyg.compose.RichTextEditorState fun aTextEditorStateMarkdown( initialText: String? = "", initialFocus: Boolean = false, + isRoomEncrypted: Boolean? = null, ): TextEditorState { return TextEditorState.Markdown( aMarkdownTextEditorState( initialText = initialText, initialFocus = initialFocus, - ) + ), + isRoomEncrypted = isRoomEncrypted, ) } @@ -36,6 +38,7 @@ fun aTextEditorStateRich( initialHtml: String = initialText, initialMarkdown: String = initialText, initialFocus: Boolean = false, + isRoomEncrypted: Boolean? = null, ): TextEditorState { return TextEditorState.Rich( aRichTextEditorState( @@ -43,7 +46,8 @@ fun aTextEditorStateRich( initialHtml = initialHtml, initialMarkdown = initialMarkdown, initialFocus = initialFocus, - ) + ), + isRoomEncrypted = isRoomEncrypted, ) } 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 543fe093d47..14f56f3bbb4 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 @@ -9,7 +9,6 @@ package io.element.android.libraries.textcomposer.model import android.os.Parcelable import android.text.Spannable -import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned import androidx.compose.runtime.Composable @@ -21,14 +20,13 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.core.text.getSpans -import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.textcomposer.components.markdown.StableCharSequence -import io.element.android.libraries.textcomposer.mentions.MentionSpan import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.mentions.MentionType import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.mentions.getMentionSpans import kotlinx.parcelize.Parcelize @@ -48,39 +46,33 @@ class MarkdownTextEditorState( fun insertSuggestion( resolvedSuggestion: ResolvedSuggestion, mentionSpanProvider: MentionSpanProvider, - permalinkBuilder: PermalinkBuilder, ) { val suggestion = currentSuggestion ?: return when (resolvedSuggestion) { is ResolvedSuggestion.AtRoom -> { val currentText = SpannableStringBuilder(text.value()) - val replaceText = "@room" - val roomPill = mentionSpanProvider.getMentionSpanFor(replaceText, "") + val mentionSpan = mentionSpanProvider.createEveryoneMentionSpan() currentText.replace(suggestion.start, suggestion.end, "@ ") val end = suggestion.start + 1 - currentText.setSpan(roomPill, suggestion.start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + currentText.setSpan(mentionSpan, suggestion.start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) text.update(currentText, true) selection = IntRange(end + 1, end + 1) } is ResolvedSuggestion.Member -> { val currentText = SpannableStringBuilder(text.value()) - val text = resolvedSuggestion.roomMember.displayName?.prependIndent("@") ?: resolvedSuggestion.roomMember.userId.value - val link = permalinkBuilder.permalinkForUser(resolvedSuggestion.roomMember.userId).getOrNull() ?: return - val mentionPill = mentionSpanProvider.getMentionSpanFor(text, link) + val mentionSpan = mentionSpanProvider.createUserMentionSpan(resolvedSuggestion.roomMember.userId) currentText.replace(suggestion.start, suggestion.end, "@ ") val end = suggestion.start + 1 - currentText.setSpan(mentionPill, suggestion.start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + currentText.setSpan(mentionSpan, suggestion.start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) this.text.update(currentText, true) this.selection = IntRange(end + 1, end + 1) } is ResolvedSuggestion.Alias -> { val currentText = SpannableStringBuilder(text.value()) - val text = resolvedSuggestion.roomAlias.value - val link = permalinkBuilder.permalinkForRoomAlias(resolvedSuggestion.roomAlias).getOrNull() ?: return - val mentionPill = mentionSpanProvider.getMentionSpanFor(text, link) + val mentionSpan = mentionSpanProvider.createRoomMentionSpan(resolvedSuggestion.roomAlias.toRoomIdOrAlias()) currentText.replace(suggestion.start, suggestion.end, "# ") val end = suggestion.start + 1 - currentText.setSpan(mentionPill, suggestion.start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + currentText.setSpan(mentionSpan, suggestion.start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) this.text.update(currentText, true) this.selection = IntRange(end + 1, end + 1) } @@ -98,19 +90,23 @@ class MarkdownTextEditorState( val start = charSequence.getSpanStart(mention) val end = charSequence.getSpanEnd(mention) when (mention.type) { - MentionSpan.Type.USER -> { - permalinkBuilder.permalinkForUser(UserId(mention.rawValue)).getOrNull()?.let { link -> - replace(start, end, "[${mention.rawValue}]($link)") + is MentionType.User -> { + permalinkBuilder.permalinkForUser(mention.type.userId).getOrNull()?.let { link -> + replace(start, end, "[${mention.type.userId}]($link)") } } - MentionSpan.Type.EVERYONE -> { + is MentionType.Everyone -> { replace(start, end, "@room") } - MentionSpan.Type.ROOM -> { - permalinkBuilder.permalinkForRoomAlias(RoomAlias(mention.rawValue)).getOrNull()?.let { link -> - replace(start, end, "[${mention.text}]($link)") + is MentionType.Room -> { + val roomIdOrAlias = mention.type.roomIdOrAlias + if (roomIdOrAlias is RoomIdOrAlias.Alias) { + permalinkBuilder.permalinkForRoomAlias(roomIdOrAlias.roomAlias).getOrNull()?.let { link -> + replace(start, end, "[${roomIdOrAlias.roomAlias}]($link)") + } } } + else -> Unit } } } @@ -121,13 +117,12 @@ class MarkdownTextEditorState( } fun getMentions(): List { - val text = SpannableString(text.value()) - val mentionSpans = text.getSpans(0, text.length) + val mentionSpans = text.value().getMentionSpans() return mentionSpans.mapNotNull { mentionSpan -> when (mentionSpan.type) { - MentionSpan.Type.USER -> IntentionalMention.User(UserId(mentionSpan.rawValue)) - MentionSpan.Type.EVERYONE -> IntentionalMention.Room - MentionSpan.Type.ROOM -> null + is MentionType.User -> IntentionalMention.User(mentionSpan.type.userId) + is MentionType.Everyone -> IntentionalMention.Room + else -> null } } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/TextEditorState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/TextEditorState.kt index 7e293d5941d..b0090185d25 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/TextEditorState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/TextEditorState.kt @@ -13,12 +13,16 @@ import io.element.android.wysiwyg.compose.RichTextEditorState @Immutable sealed interface TextEditorState { + val isRoomEncrypted: Boolean? + data class Markdown( val state: MarkdownTextEditorState, + override val isRoomEncrypted: Boolean?, ) : TextEditorState data class Rich( - val richTextEditorState: RichTextEditorState + val richTextEditorState: RichTextEditorState, + override val isRoomEncrypted: Boolean?, ) : TextEditorState fun messageHtml(): String? = when (this) { diff --git a/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml b/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml index 94e25da3ac9..9693f648356 100644 --- a/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-cs/translations.xml @@ -5,11 +5,17 @@ "Zavřít možnosti formátování" "Přepnout blok kódu" "Volitelný titulek…" + "Šifrovaná zpráva…" "Zpráva…" + "Nešifrovaná zpráva…" "Vytvořit odkaz" "Upravit odkaz" + "%1$s, stav: %2$s" "Použít tučný text" "Použít kurzívu" + "zakázáno" + "VYP" + "ZAP" "Použít přeškrtnutí" "Použít podtržení" "Přepnout režim celé obrazovky" diff --git a/libraries/textcomposer/impl/src/main/res/values-cy/translations.xml b/libraries/textcomposer/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..497766cc1e0 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,32 @@ + + + "Ychwanegu atodiad" + "Toglo\'r rhestr fwledi" + "Cau\'r dewisiadau fformatio" + "Toglo\'r bloc cod" + "Ychwanegu capsiwn" + "Neges wedi\'i hamgryptio…" + "Neges…" + "Neges heb ei hamgryptio…" + "Creu dolen" + "Golygu dolen" + "%1$s, cyflwr: %2$s" + "Gosod fformat trwm" + "Gosod fformat italig" + "analluogwyd" + "i ffwrdd" + "ymlaen" + "Gosod fformat llinell trwodd" + "Gosod fformat tanlinellu" + "Toglo\'r modd sgrin lawn" + "Mewnoliad" + "Gosod fformat cod mewnlin" + "Gosod dolen" + "Toglo\'r rhestr wedi\'i rhifo" + "Agor y dewisiadau cyfansoddi" + "Toglo\'r dyfyniad" + "Dileu dolen" + "Dadmewnoli" + "Dolen" + "Daliwch i recordio" + diff --git a/libraries/textcomposer/impl/src/main/res/values-da/translations.xml b/libraries/textcomposer/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..dca3d5d11e9 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,32 @@ + + + "Tilføj vedhæftet fil" + "Slå punktopstilling til/fra" + "Annullér og luk tekstformatering" + "Slå kodeblok til/fra" + "Tilføj en billedtekst" + "Krypteret besked…" + "Besked…" + "Ukrypteret besked…" + "Opret et link" + "Rediger link" + "%1$s, tilstand: %2$s" + "Anvend fed skrift" + "Anvend kursiv" + "deaktiveret" + "slukket" + "aktiv" + "Anvend gennemstregning" + "Anvend understregning" + "Slå fuldskærmsvisning til/fra" + "Indrykning" + "Anvend inline kodeformat" + "Indstil link" + "Slå nummereret liste til/fra" + "Åbn skriveindstillinger" + "Slå citation til/fra" + "Fjern link" + "Fjern indrykning" + "Link" + "Hold nede for at optage" + diff --git a/libraries/textcomposer/impl/src/main/res/values-de/translations.xml b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml index 53ea09cfa84..b12e7e6080c 100644 --- a/libraries/textcomposer/impl/src/main/res/values-de/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-de/translations.xml @@ -2,15 +2,21 @@ "Anhang hinzufügen" "Aufzählungsliste umschalten" - "Formatierungsoptionen schließen" + "Textformatierung abbrechen und schließen" "Codeblock umschalten" - "Optionale Bildunterschrift…" + "Bildunterschrift hinzufügen" + "Verschlüsselte Nachricht…" "Nachricht…" + "Unverschlüsselte Nachricht" "Einen Link erstellen" "Link bearbeiten" + "%1$s, Zustand: %2$s" "Fettes Format anwenden" "Kursives Format anwenden" - "Durchgestrichenes Format anwenden" + "deaktiviert" + "aus" + "ein" + "Text durchstreichen" "Unterstreichungsformat anwenden" "Vollbildmodus umschalten" "Einrückung" diff --git a/libraries/textcomposer/impl/src/main/res/values-el/translations.xml b/libraries/textcomposer/impl/src/main/res/values-el/translations.xml index 3e5c5065c7a..1c8f6c8b65c 100644 --- a/libraries/textcomposer/impl/src/main/res/values-el/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-el/translations.xml @@ -5,11 +5,17 @@ "Κλείσε τις επιλογές μορφοποίησης" "Διακόπτης μπλοκ κώδικα" "Προαιρετική λεζάντα…" + "Κρυπτογραφημένο μήνυμα…" "Μήνυμα…" + "Μη κρυπτογραφημένο μήνυμα…" "Δημιούργησε έναν σύνδεσμο" "Επεξεργασία συνδέσμου" + "%1$s, κατάσταση: %2$s" "Εφαρμογή έντονης μορφής" "Εφαρμογή πλάγιας μορφής" + "ανενεργό" + "κλειστό" + "ενεργό" "Εφαρμογή μορφής διαγραφής" "Εφαρμογή μορφής υπογράμμισης" "Εναλλαγή λειτουργίας πλήρους οθόνης" diff --git a/libraries/textcomposer/impl/src/main/res/values-es/translations.xml b/libraries/textcomposer/impl/src/main/res/values-es/translations.xml index bc0db632ad7..2685733c3ed 100644 --- a/libraries/textcomposer/impl/src/main/res/values-es/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-es/translations.xml @@ -4,7 +4,10 @@ "Lista de puntos" "Cerrar opciones de formato" "Bloque de código" + "Agregar una leyenda" + "Mensaje cifrado…" "Mensaje…" + "Mensaje no cifrado…" "Crear un enlace" "Editar enlace" "Aplicar formato negrita" diff --git a/libraries/textcomposer/impl/src/main/res/values-et/translations.xml b/libraries/textcomposer/impl/src/main/res/values-et/translations.xml index c1a8035e0eb..05c09aa335c 100644 --- a/libraries/textcomposer/impl/src/main/res/values-et/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-et/translations.xml @@ -2,14 +2,20 @@ "Lisa manus" "Lülita mummudega loend sisse/välja" - "Sulge vorminduse valikud" + "Katkesta ja sulge tekstivorminduse valikud" "Lülita lähtekoodi lõik sisse/välja" "Selgitus või nimi, kui soovid lisada…" + "Krüptitud sõnum…" "Sõnum…" + "Krüptimata sõnum…" "Lisa link" "Muuda linki" + "%1$s, olek: %2$s" "Kasuta paksu kirja" "Kasuta kaldkirja" + "pole kasutusel" + "väljas" + "sees" "Kasuta läbikriipsutatud kirja" "Kasuta allajoonitud kirja" "Lülita täisekraanivaade sisse/välja" diff --git a/libraries/textcomposer/impl/src/main/res/values-eu/translations.xml b/libraries/textcomposer/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..ee029ac038d --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,26 @@ + + + "Gehitu eranskina" + "Buleten zerrenda bai/ez" + "Itxi formatu aukerak" + "Gehitu testua" + "Zifratutako mezua…" + "Mezua…" + "Zifratu gabeko mezua…" + "Sortu esteka" + "Editatu esteka" + "Aplikatu formatu lodia" + "Aplikatu formatu etzana" + "Aplikatu ezabaketa formatua" + "Aplikatu azpimarra formatua" + "Pantaila osoa bai/ez" + "Koska" + "Ezarri esteka" + "Zenbakidu zerrenda bai/ez" + "Ireki idazketa aukerak" + "Aipua bai/ez" + "Kendu esteka" + "Koskarik gabe" + "Esteka" + "Mantendu sakatuta grabatzeko" + diff --git a/libraries/textcomposer/impl/src/main/res/values-fa/translations.xml b/libraries/textcomposer/impl/src/main/res/values-fa/translations.xml index 2a20aecd216..d50983d7330 100644 --- a/libraries/textcomposer/impl/src/main/res/values-fa/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-fa/translations.xml @@ -4,7 +4,10 @@ "تغییر وضعیت سیاههٔ گلوله‌ای" "بستن گزینه‌های قالب‌بندی" "تغییر حالت بلوک کد" + "افزودن عنوان" + "پیام رمزنگاری شده…" "پیام…" + "پیام رمزنگاری نشده…" "ایجاد پیوند" "ویرایش پیوند" "اعمال قالب توپر" diff --git a/libraries/textcomposer/impl/src/main/res/values-fi/translations.xml b/libraries/textcomposer/impl/src/main/res/values-fi/translations.xml index a7d29139377..473bd218f4c 100644 --- a/libraries/textcomposer/impl/src/main/res/values-fi/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-fi/translations.xml @@ -5,7 +5,9 @@ "Sulje muotoiluasetukset" "Koodilohko päälle/pois" "Lisää kuvateksti" + "Salattu viesti…" "Viesti…" + "Salaamaton viesti…" "Luo linkki" "Muokkaa linkkiä" "Käytä lihavoitua muotoa" diff --git a/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml b/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml index 48b36687152..f21e9f90ba9 100644 --- a/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-fr/translations.xml @@ -2,14 +2,20 @@ "Ajouter une pièce jointe" "Afficher une liste à puces" - "Fermer les options de formatage" + "Annuler et fermer les options de formatage" "Afficher le bloc de code" "Légende facultative…" + "Message chiffré…" "Message…" + "Message non chiffré…" "Créer un lien" "Modifier le lien" + "%1$s, état : %2$s" "Appliquer le format gras" "Appliquer le format italique" + "désactivé" + "désactivé" + "activé" "Appliquer le format barré" "Appliquer le format souligné" "Activer/désactiver le mode plein écran" diff --git a/libraries/textcomposer/impl/src/main/res/values-hu/translations.xml b/libraries/textcomposer/impl/src/main/res/values-hu/translations.xml index 0353dca4196..462b18aa9f7 100644 --- a/libraries/textcomposer/impl/src/main/res/values-hu/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-hu/translations.xml @@ -2,14 +2,20 @@ "Melléklet hozzáadása" "Felsorolás be/ki" - "Formázási beállítások bezárása" + "Mégse, és a formázási beállítások bezárása" "Kódblokk be/ki" - "Nem kötelező felirat…" + "Felirat hozzáadása…" + "Titkosított üzenet…" "Üzenet…" + "Titkosítatlan üzenet…" "Hivatkozás létrehozása" "Hivatkozás szerkesztése" + "%1$s, állapot: %2$s" "Félkövér formátum alkalmazása" "Dőlt formátum alkalmazása" + "letiltva" + "ki" + "be" "Áthúzott formátum alkalmazása" "Aláhúzott formátum alkalmazása" "Teljes képernyős mód be/ki" diff --git a/libraries/textcomposer/impl/src/main/res/values-in/translations.xml b/libraries/textcomposer/impl/src/main/res/values-in/translations.xml index df7d6992e27..8646be898b9 100644 --- a/libraries/textcomposer/impl/src/main/res/values-in/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-in/translations.xml @@ -2,14 +2,20 @@ "Tambahkan lampiran" "Alihkan daftar poin" - "Tutup opsi pemformatan" + "Batalkan dan tutup pemformatan teks" "Alihkan blok kode" "Keterangan opsional…" + "Pesan terenkripsi…" "Kirim pesan…" + "Pesan tidak terenkripsi…" "Buat tautan" "Sunting tautan" + "%1$s, keadaan: %2$s" "Terapkan format tebal" "Terapkan format miring" + "dinonaktifkan" + "mati" + "nyala" "Terapkan format coret" "Terapkan format garis bawah" "Alihkan mode layar penuh" diff --git a/libraries/textcomposer/impl/src/main/res/values-it/translations.xml b/libraries/textcomposer/impl/src/main/res/values-it/translations.xml index 04e45678ebe..79d0bebc548 100644 --- a/libraries/textcomposer/impl/src/main/res/values-it/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-it/translations.xml @@ -2,14 +2,20 @@ "Aggiungi allegato" "Attiva/disattiva l\'elenco puntato" - "Chiudi le opzioni di formattazione" + "Annullare e chiudere la formattazione del testo" "Attiva/disattiva il blocco di codice" "Aggiungi una didascalia" + "Messaggio cifrato…" "Messaggio…" + "Messaggio non cifrato…" "Crea un collegamento" "Modifica collegamento" + "%1$s stato: %2$s" "Applica il formato grassetto" "Applicare il formato corsivo" + "disabilitato" + "Disattivato" + "Attivo" "Applica il formato barrato" "Applicare il formato di sottolineatura" "Attiva/disattiva la modalità a schermo intero" diff --git a/libraries/textcomposer/impl/src/main/res/values-lt/translations.xml b/libraries/textcomposer/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 00000000000..b23358c57a2 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,17 @@ + + + "Perjungti punktų sąrašą" + "Kodo blokas" + "Žinutė…" + "Taikyti paryškintą formatą" + "Taikyti pasvirusį formatą" + "Taikyti perbrauktą formatą" + "Taikyti pabrauktą formatą" + "Perjungti viso ekrano režimą" + "Atitraukti" + "Taikyti įterpto kodo formatą" + "Nustatyti nuorodą" + "Perjungti sunumeruotą sąrašą" + "Cituoti" + "Panaikinti atitraukimą" + diff --git a/libraries/textcomposer/impl/src/main/res/values-nb/translations.xml b/libraries/textcomposer/impl/src/main/res/values-nb/translations.xml index 4dcab4e7553..18a724a4b23 100644 --- a/libraries/textcomposer/impl/src/main/res/values-nb/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-nb/translations.xml @@ -1,16 +1,32 @@ + "Legg til vedlegg" + "Aktiver/deaktiver punktliste" + "Lukk formateringsvalg" + "Aktiver kodeblokk" + "Legg til en tekstbeskrivelse" + "Kryptert melding…" "Melding…" + "Ukryptert melding…" "Opprett en lenke" "Rediger lenke" + "%1$s, tilstand: %2$s" "Bruk fet skrift" "Bruk kursivformat" + "deaktivert" + "av" + "på" "Bruke gjennomstrekingsformat" "Bruke understrekingsformat" + "Veksle fullskjermmodus" "Innrykk" "Bruk inline-kodeformat" "Angi lenke" + "Aktiver/deaktiver nummerert liste" + "Åpne skrivealternativer" + "Slå på sitat" "Fjern lenke" "Fjern innrykk" "Lenke" + "Hold for å ta opp" diff --git a/libraries/textcomposer/impl/src/main/res/values-pl/translations.xml b/libraries/textcomposer/impl/src/main/res/values-pl/translations.xml index e47afe0fe12..121bc2f4d8f 100644 --- a/libraries/textcomposer/impl/src/main/res/values-pl/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-pl/translations.xml @@ -5,7 +5,9 @@ "Zamknij opcje formatowania" "Przełącz blok kodu" "Dodaj opis" + "Wiadomość szyfrowana…" "Wiadomość…" + "Niezaszyfrowana wiadomość…" "Utwórz link" "Edytuj link" "Zastosuj pogrubienie" diff --git a/libraries/textcomposer/impl/src/main/res/values-pt-rBR/translations.xml b/libraries/textcomposer/impl/src/main/res/values-pt-rBR/translations.xml index 592050a625d..81fc4693011 100644 --- a/libraries/textcomposer/impl/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-pt-rBR/translations.xml @@ -4,11 +4,18 @@ "Alternar lista de marcadores" "Fechar opções de formatação" "Alternar bloco de código" + "Adicionar uma legenda" + "Mensagem criptografada…" "Mensagem…" + "Mensagem não criptografada…" "Criar um link" "Editar link" + "%1$s, estado: %2$s" "Aplicar negrito" "Aplicar formato itálico" + "desativado" + "desligado" + "ligado" "Aplicar formato tachado" "Aplicar sublinhado" "Alternar o modo de tela cheia" diff --git a/libraries/textcomposer/impl/src/main/res/values-pt/translations.xml b/libraries/textcomposer/impl/src/main/res/values-pt/translations.xml index c26355bf355..2eb61ac739b 100644 --- a/libraries/textcomposer/impl/src/main/res/values-pt/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-pt/translations.xml @@ -2,14 +2,20 @@ "Adicionar anexo" "Ativar/desativar lista de pontos" - "Fechar opções de formatação" + "Cancelar e fechar opções de formatação" "Ativar/desativar bloco de código" "Legenda opcional…" + "Mensagem encriptada…" "Mensagem…" + "Mensagem não encriptada…" "Criar uma ligação" "Editar ligação" + "%1$s, estado: %2$s" "Aplicar negrito" "Aplicar itálico" + "desativado" + "desligado" + "ligado" "Aplicar rasura" "Aplicar sublinhado" "Entrar/sair do modo de ecrã inteiro" diff --git a/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml index 22ad2db0b30..b49a7670ff6 100644 --- a/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-ru/translations.xml @@ -5,7 +5,9 @@ "Закрыть параметры форматирования" "Переключить блок кода" "Необязательный заголовок…" + "Зашифрованное сообщение…" "Сообщение…" + "Незашифрованное сообщение…" "Создать ссылку" "Редактировать ссылку" "Применить жирный шрифт" diff --git a/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml b/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml index 028e12f0850..7b466240110 100644 --- a/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-sk/translations.xml @@ -2,14 +2,20 @@ "Pridať prílohu" "Prepnúť zoznam odrážok" - "Zatvoriť možnosti formátovania" + "Zrušiť a zatvoriť formátovanie textu" "Prepnúť blok kódu" "Voliteľný titulok…" + "Šifrovaná správa…" "Správa…" + "Nešifrovaná správa…" "Vytvoriť odkaz" "Upraviť odkaz" + "%1$s, stav: %2$s" "Použiť tučný formát" "Použiť formát kurzívy" + "zakázané" + "vypnuté" + "zapnuté" "Použiť formát prečiarknutia" "Použiť formát podčiarknutia" "Prepnúť režim celej obrazovky" diff --git a/libraries/textcomposer/impl/src/main/res/values-sv/translations.xml b/libraries/textcomposer/impl/src/main/res/values-sv/translations.xml index 02f5ede9f5c..721812d60c0 100644 --- a/libraries/textcomposer/impl/src/main/res/values-sv/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-sv/translations.xml @@ -4,7 +4,10 @@ "Växla punktlista" "Stäng formateringsalternativ" "Växla kodblock" + "Lägg till en bildtext" + "Krypterat meddelande …" "Meddelande …" + "Okrypterat meddelande …" "Skapa en länk" "Redigera länk" "Använd fetstil" diff --git a/libraries/textcomposer/impl/src/main/res/values-tr/translations.xml b/libraries/textcomposer/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..95796c5174e --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,26 @@ + + + "Ek ekle" + "Madde işaretli listeyi aç/kapat" + "Biçimlendirme seçeneklerini kapat" + "Kod Bloğunu Aç/Kapat" + "Açıklama ekle" + "Mesaj…" + "Bir bağlantı oluştur" + "Bağlantıyı Düzenle" + "Kalın biçimi uygula" + "İtalik biçimi uygula" + "Üstü çizili biçimi uygula" + "Altı çizili biçimi uygula" + "Tam ekran modunu aç/kapat" + "Girinti" + "Satır içi kod biçimini uygula" + "Bağlantıyı ayarla" + "Numaralı listeyi aç/kapat" + "Oluşturma seçeneklerini aç" + "Alıntıyı Aç/Kapat" + "Bağlantıyı kaldır" + "Girintiyi kaldır" + "Bağlantı" + "Kaydetmek için basılı tut" + diff --git a/libraries/textcomposer/impl/src/main/res/values-uk/translations.xml b/libraries/textcomposer/impl/src/main/res/values-uk/translations.xml index 90216252134..6a386e9d74b 100644 --- a/libraries/textcomposer/impl/src/main/res/values-uk/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-uk/translations.xml @@ -5,11 +5,15 @@ "Закрити параметри форматування" "Перемкнути блок коду" "Необов\'язковий підпис…" + "Зашифроване повідомлення…" "Повідомлення…" + "Незашифроване повідомлення…" "Створити посилання" "Редагувати посилання" + "%1$s, стан: %2$s" "Жирний формат" "Курсивний формат" + "вимкнено" "Застосувати формат закреслення" "Застосувати формат підкреслення" "Перемкнути повноекранний режим" diff --git a/libraries/textcomposer/impl/src/main/res/values-ur/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..9ed333b924c --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,25 @@ + + + "منسلکہ شامل کریں" + "گولئی فہرست تبدیل کریں" + "تنسیق کے اختیارات بند کریں" + "رمز بلاک تبدیل کریں" + "پیغام…" + "ایک ربط بنائیں" + "ربط میں ترمیم کریں" + "جلی وضع لاگو کریں" + "ترچھی وضع لاگو کریں" + "مشطوب وضع لاگو کریں" + "مسطر وضع لاگو کریں" + "مکمل نمایشگر وضع تبدیل کریں" + "حاشیہ" + "درخط رمز وضع تبدیل کریں" + "ربط متعین کریں" + "عددی فہرست تبدیل کریں" + "تحریر کے اختیارات کھولیں" + "حوالہ تبدیل کریں" + "ربط ہٹائیں" + "غیر حاشیہ" + "ربط" + "ثبت کرنے کیلئے دبا کر رکھیں" + diff --git a/libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml index 6ff65e94f63..f9cdfe25d74 100644 --- a/libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-zh-rTW/translations.xml @@ -5,7 +5,9 @@ "關閉格式化選項" "切換程式碼區塊" "新增標題" + "已加密的訊息……" "訊息" + "未加密的訊息……" "建立連結" "編輯連結" "套用粗體" 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 4040ca511da..7fbdc32c2ea 100644 --- a/libraries/textcomposer/impl/src/main/res/values-zh/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-zh/translations.xml @@ -5,11 +5,17 @@ "关闭格式化选项" "切换代码块" "可选的标题……" + "加密信息…" "消息…" + "未加密的消息…" "创建链接" "编辑链接" + "%1$s,状态:%2$s" "应用粗体格式" "应用斜体格式" + "已禁用" + "关" + "开" "应用删除线格式" "应用下划线格式" "切换全屏模式" diff --git a/libraries/textcomposer/impl/src/main/res/values/localazy.xml b/libraries/textcomposer/impl/src/main/res/values/localazy.xml index fcc763db308..89331da7ed7 100644 --- a/libraries/textcomposer/impl/src/main/res/values/localazy.xml +++ b/libraries/textcomposer/impl/src/main/res/values/localazy.xml @@ -2,14 +2,20 @@ "Add attachment" "Toggle bullet list" - "Close formatting options" + "Cancel and close text formatting" "Toggle code block" "Add a caption" + "Encrypted message…" "Message…" + "Unencrypted message…" "Create a link" "Edit link" + "%1$s, state: %2$s" "Apply bold format" "Apply italic format" + "disabled" + "off" + "on" "Apply strikethrough format" "Apply underline format" "Toggle full screen mode" 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 9fdc4e22464..fa003b74d87 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 @@ -14,16 +14,16 @@ 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 +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.textcomposer.components.markdown.MarkdownTextInput +import io.element.android.libraries.textcomposer.impl.mentions.aMentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpan -import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState import io.element.android.libraries.textcomposer.model.Suggestion @@ -146,7 +146,6 @@ class MarkdownTextInputTest { @Test fun `inserting a mention replaces the existing text with a span`() = runTest { val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(A_SESSION_ID) }) - val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/$A_SESSION_ID") }) val state = aMarkdownTextEditorState(initialText = "@", initialFocus = true) state.currentSuggestion = Suggestion(0, 1, SuggestionType.Mention, "") rule.setMarkdownTextInput(state = state) @@ -155,8 +154,7 @@ class MarkdownTextInputTest { editor = it.findEditor() state.insertSuggestion( ResolvedSuggestion.Member(roomMember = aRoomMember()), - MentionSpanProvider(permalinkParser = permalinkParser), - permalinkBuilder, + aMentionSpanProvider(permalinkParser), ) } rule.awaitIdle() @@ -170,7 +168,6 @@ class MarkdownTextInputTest { private fun AndroidComposeTestRule.setMarkdownTextInput( state: MarkdownTextEditorState = aMarkdownTextEditorState(), - subcomposing: Boolean = false, onTyping: (Boolean) -> Unit = {}, onSuggestionReceived: (Suggestion?) -> Unit = {}, ) { @@ -178,7 +175,8 @@ class MarkdownTextInputTest { val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus) MarkdownTextInput( state = state, - subcomposing = subcomposing, + placeholder = "Placeholder", + placeholderColor = ElementTheme.colors.textSecondary, onTyping = onTyping, onReceiveSuggestion = onSuggestionReceived, richTextEditorStyle = style, diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/IntentionalMentionSpanProviderTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/IntentionalMentionSpanProviderTest.kt index 50ac074078f..de4b9bc64a0 100644 --- a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/IntentionalMentionSpanProviderTest.kt +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/IntentionalMentionSpanProviderTest.kt @@ -14,8 +14,7 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData 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.MentionSpan -import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.mentions.MentionType import io.element.android.tests.testutils.WarmUpRule import org.junit.Rule import org.junit.Test @@ -28,22 +27,22 @@ class IntentionalMentionSpanProviderTest { val warmUpRule = WarmUpRule() private val permalinkParser = FakePermalinkParser() - private val mentionSpanProvider = MentionSpanProvider( - permalinkParser = permalinkParser, - ) + private val mentionSpanProvider = aMentionSpanProvider(permalinkParser) @Test fun `getting mention span for a user returns a MentionSpan of type USER`() { permalinkParser.givenResult(PermalinkData.UserLink(A_USER_ID)) val mentionSpan = mentionSpanProvider.getMentionSpanFor("@me:matrix.org", "https://matrix.to/#/${A_USER_ID.value}") - assertThat(mentionSpan.type).isEqualTo(MentionSpan.Type.USER) + assertThat(mentionSpan?.type).isInstanceOf(MentionType.User::class.java) + val userType = mentionSpan?.type as MentionType.User + assertThat(userType.userId).isEqualTo(A_USER_ID) } @Test fun `getting mention span for everyone in the room returns a MentionSpan of type EVERYONE`() { permalinkParser.givenResult(PermalinkData.FallbackLink(uri = Uri.EMPTY)) val mentionSpan = mentionSpanProvider.getMentionSpanFor("@room", "#") - assertThat(mentionSpan.type).isEqualTo(MentionSpan.Type.EVERYONE) + assertThat(mentionSpan?.type).isEqualTo(MentionType.Everyone) } @Test @@ -54,6 +53,8 @@ class IntentionalMentionSpanProviderTest { ) ) val mentionSpan = mentionSpanProvider.getMentionSpanFor("#room:matrix.org", "https://matrix.to/#/#room:matrix.org") - assertThat(mentionSpan.type).isEqualTo(MentionSpan.Type.ROOM) + assertThat(mentionSpan?.type).isInstanceOf(MentionType.Room::class.java) + val roomType = mentionSpan?.type as MentionType.Room + assertThat(roomType.roomIdOrAlias).isEqualTo(RoomAlias("#room:matrix.org").toRoomIdOrAlias()) } } diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanFormatterTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanFormatterTest.kt new file mode 100644 index 00000000000..2d0156a3dfc --- /dev/null +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanFormatterTest.kt @@ -0,0 +1,135 @@ +/* + * 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.textcomposer.impl.mentions + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.room.aRoomSummary +import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache +import io.element.android.libraries.matrix.ui.messages.RoomNamesCache +import io.element.android.libraries.textcomposer.mentions.DefaultMentionSpanFormatter +import io.element.android.libraries.textcomposer.mentions.MentionType +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MentionSpanFormatterTest { + private val roomMemberProfilesCache = RoomMemberProfilesCache() + private val roomNamesCache = RoomNamesCache() + private val formatter = DefaultMentionSpanFormatter( + roomMemberProfilesCache = roomMemberProfilesCache, + roomNamesCache = roomNamesCache, + ) + + @Test + fun `formatDisplayText - formats user mention with empty cache`() = runTest { + val userId = A_USER_ID + val mentionType = MentionType.User(userId) + val result = formatter.formatDisplayText(mentionType) + assertThat(result.toString()).isEqualTo(userId.value) + } + + @Test + fun `formatDisplayText - formats user mention with filled cache`() = runTest { + val userId = A_USER_ID + val roomMember = aRoomMember(userId, displayName = "alice") + roomMemberProfilesCache.replace(listOf(roomMember)) + val mentionType = MentionType.User(userId) + val result = formatter.formatDisplayText(mentionType) + assertThat(result.toString()).isEqualTo("@alice") + } + + @Test + fun `formatDisplayText - formats room mention with empty cache`() = runTest { + val roomAlias = A_ROOM_ALIAS + val mentionType = MentionType.Room(roomAlias.toRoomIdOrAlias()) + + val result = formatter.formatDisplayText(mentionType) + + assertThat(result.toString()).isEqualTo(roomAlias.value) + } + + @Test + fun `formatDisplayText - formats room mention with filled cache`() = runTest { + val roomAlias = A_ROOM_ALIAS + val roomSummary = aRoomSummary( + canonicalAlias = roomAlias, + name = "my room" + ) + roomNamesCache.replace(listOf(roomSummary)) + val mentionType = MentionType.Room(roomAlias.toRoomIdOrAlias()) + + val result = formatter.formatDisplayText(mentionType) + + assertThat(result.toString()).isEqualTo("#my room") + } + + @Test + fun `formatDisplayText - formats room mention with room id and empty cache`() = runTest { + val roomId = A_ROOM_ID + val mentionType = MentionType.Room(roomId.toRoomIdOrAlias()) + + val result = formatter.formatDisplayText(mentionType) + + assertThat(result.toString()).isEqualTo(roomId.value) + } + + @Test + fun `formatDisplayText - formats room mention with room id and filled cache`() = runTest { + val roomId = A_ROOM_ID + val roomSummary = aRoomSummary( + roomId = roomId, + name = "my room" + ) + roomNamesCache.replace(listOf(roomSummary)) + + val mentionType = MentionType.Room(roomId.toRoomIdOrAlias()) + val result = formatter.formatDisplayText(mentionType) + + assertThat(result.toString()).isEqualTo("#my room") + } + + @Test + fun `formatDisplayText - formats message mention with empty cache`() = runTest { + val roomId = A_ROOM_ID + val mentionType = MentionType.Message(roomId.toRoomIdOrAlias(), eventId = AN_EVENT_ID) + + val result = formatter.formatDisplayText(mentionType) + + assertThat(result.toString()).isEqualTo("💬 > ${roomId.value}") + } + + @Test + fun `formatDisplayText - formats message mention with filled cache`() = runTest { + val roomId = A_ROOM_ID + val roomSummary = aRoomSummary( + roomId = roomId, + name = "my room" + ) + roomNamesCache.replace(listOf(roomSummary)) + + val mentionType = MentionType.Message(roomId.toRoomIdOrAlias(), eventId = AN_EVENT_ID) + + val result = formatter.formatDisplayText(mentionType) + + assertThat(result.toString()).isEqualTo("💬 > #my room") + } + + @Test + fun `formatDisplayText - formats everyone mention`() = runTest { + val mentionType = MentionType.Everyone + + val result = formatter.formatDisplayText(mentionType) + + assertThat(result.toString()).isEqualTo("@room") + } +} diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanProviderFixture.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanProviderFixture.kt new file mode 100644 index 00000000000..cc40a56bf42 --- /dev/null +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanProviderFixture.kt @@ -0,0 +1,32 @@ +/* + * 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.textcomposer.impl.mentions + +import io.element.android.libraries.matrix.api.permalink.PermalinkParser +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.MentionSpanFormatter +import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme +import io.element.android.libraries.textcomposer.mentions.MentionType + +fun aMentionSpanProvider( + permalinkParser: PermalinkParser = FakePermalinkParser(), + mentionSpanFormatter: MentionSpanFormatter = object : MentionSpanFormatter { + override fun formatDisplayText(mentionType: MentionType): CharSequence { + return mentionType.toString() + } + }, + mentionSpanTheme: MentionSpanTheme = MentionSpanTheme(A_USER_ID), +): MentionSpanProvider { + return MentionSpanProvider( + permalinkParser = permalinkParser, + mentionSpanFormatter = mentionSpanFormatter, + mentionSpanTheme = mentionSpanTheme, + ) +} diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/model/MarkdownTextEditorStateTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/model/MarkdownTextEditorStateTest.kt index 33156add6fc..2c7905e2dae 100644 --- a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/model/MarkdownTextEditorStateTest.kt +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/model/MarkdownTextEditorStateTest.kt @@ -12,6 +12,8 @@ import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.IntentionalMention @@ -20,8 +22,9 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.textcomposer.impl.mentions.aMentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpan -import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.mentions.MentionType import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType @@ -35,9 +38,8 @@ class MarkdownTextEditorStateTest { fun `insertMention - room alias - getMentions return empty list`() { val state = aMarkdownTextEditorState(initialText = "Hello @", initialFocus = true) val suggestion = aRoomAliasSuggestion() - val permalinkBuilder = FakePermalinkBuilder() val mentionSpanProvider = aMentionSpanProvider() - state.insertSuggestion(suggestion, mentionSpanProvider, permalinkBuilder) + state.insertSuggestion(suggestion, mentionSpanProvider) assertThat(state.getMentions()).isEmpty() } @@ -48,9 +50,8 @@ class MarkdownTextEditorStateTest { } val suggestion = aRoomAliasSuggestion() val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) }) - val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.failure(IllegalStateException("Failed")) }) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) - state.insertSuggestion(suggestion, mentionSpanProvider, permalinkBuilder) + state.insertSuggestion(suggestion, mentionSpanProvider) } @Test @@ -60,9 +61,8 @@ class MarkdownTextEditorStateTest { } val suggestion = aRoomAliasSuggestion() val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) }) - val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.success("https://matrix.to/#/${A_ROOM_ALIAS.value}") }) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) - state.insertSuggestion(suggestion, mentionSpanProvider, permalinkBuilder) + state.insertSuggestion(suggestion, mentionSpanProvider) } @Test @@ -70,31 +70,11 @@ class MarkdownTextEditorStateTest { val state = aMarkdownTextEditorState(initialText = "Hello @", initialFocus = true) val member = aRoomMember() val mention = ResolvedSuggestion.Member(member) - val permalinkBuilder = FakePermalinkBuilder() val mentionSpanProvider = aMentionSpanProvider() - - state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder) - + state.insertSuggestion(mention, mentionSpanProvider) assertThat(state.getMentions()).isEmpty() } - @Test - fun `insertSuggestion - with member but failed PermalinkBuilder result`() { - val state = aMarkdownTextEditorState(initialText = "Hello @", initialFocus = true).apply { - currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Mention, text = "") - } - val member = aRoomMember() - val mention = ResolvedSuggestion.Member(member) - val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(member.userId) }) - val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.failure(IllegalStateException("Failed")) }) - val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) - - state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder) - - val mentions = state.getMentions() - assertThat(mentions).isEmpty() - } - @Test fun `insertSuggestion - with member`() { val state = aMarkdownTextEditorState(initialText = "Hello @", initialFocus = true).apply { @@ -103,10 +83,9 @@ class MarkdownTextEditorStateTest { val member = aRoomMember() val mention = ResolvedSuggestion.Member(member) val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(member.userId) }) - val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/${member.userId}") }) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) - state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder) + state.insertSuggestion(mention, mentionSpanProvider) val mentions = state.getMentions() assertThat(mentions).isNotEmpty() @@ -119,11 +98,10 @@ class MarkdownTextEditorStateTest { currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Mention, text = "") } val mention = ResolvedSuggestion.AtRoom - val permalinkBuilder = FakePermalinkBuilder() val permalinkParser = FakePermalinkParser(result = { PermalinkData.FallbackLink(Uri.EMPTY, false) }) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) - state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder) + state.insertSuggestion(mention, mentionSpanProvider) val mentions = state.getMentions() assertThat(mentions).isNotEmpty() @@ -177,16 +155,10 @@ class MarkdownTextEditorStateTest { assertThat(mentions.lastOrNull()).isInstanceOf(IntentionalMention.Room::class.java) } - private fun aMentionSpanProvider( - permalinkParser: FakePermalinkParser = FakePermalinkParser(), - ): MentionSpanProvider { - return MentionSpanProvider(permalinkParser) - } - private fun aMarkdownTextWithMentions(): CharSequence { - val userMentionSpan = MentionSpan("@Alice", "@alice:matrix.org", MentionSpan.Type.USER) - val atRoomMentionSpan = MentionSpan("@room", "@room", MentionSpan.Type.EVERYONE) - val roomMentionSpan = MentionSpan("#room:domain.org", "#room:domain.org", MentionSpan.Type.ROOM) + val userMentionSpan = MentionSpan(MentionType.User(UserId("@alice:matrix.org"))) + val atRoomMentionSpan = MentionSpan(MentionType.Everyone) + val roomMentionSpan = MentionSpan(MentionType.Room(RoomAlias("#room:domain.org").toRoomIdOrAlias())) return buildSpannedString { append("Hello ") inSpans(userMentionSpan) { diff --git a/libraries/troubleshoot/api/build.gradle.kts b/libraries/troubleshoot/api/build.gradle.kts index 003c32ec2cf..6ce2d3e9f85 100644 --- a/libraries/troubleshoot/api/build.gradle.kts +++ b/libraries/troubleshoot/api/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) implementation(libs.androidx.corektx) implementation(libs.coroutines.core) } diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/PushHistoryEntryPoint.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/PushHistoryEntryPoint.kt new file mode 100644 index 00000000000..088fb387da0 --- /dev/null +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/PushHistoryEntryPoint.kt @@ -0,0 +1,30 @@ +/* + * 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.troubleshoot.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.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId + +interface PushHistoryEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onDone() + fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) + } +} diff --git a/libraries/troubleshoot/impl/build.gradle.kts b/libraries/troubleshoot/impl/build.gradle.kts index e4e5928eb7c..f88ccc8f85a 100644 --- a/libraries/troubleshoot/impl/build.gradle.kts +++ b/libraries/troubleshoot/impl/build.gradle.kts @@ -28,6 +28,8 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.designsystem) implementation(projects.libraries.di) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.uiStrings) api(projects.libraries.troubleshoot.api) api(projects.libraries.push.api) implementation(projects.services.analytics.api) @@ -40,6 +42,7 @@ dependencies { testImplementation(libs.coroutines.test) testImplementation(projects.services.analytics.test) testImplementation(projects.tests.testutils) + testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) testImplementation(libs.androidx.compose.ui.test.junit) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt index 5278de50569..5a7aded0747 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt @@ -98,7 +98,7 @@ private fun ColumnScope.TroubleshootTestView( Icon( contentDescription = null, modifier = Modifier.size(24.dp), - imageVector = CompoundIcons.Error(), + imageVector = CompoundIcons.ErrorSolid(), tint = ElementTheme.colors.textCriticalPrimary ) } diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/DefaultPushHistoryEntryPoint.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/DefaultPushHistoryEntryPoint.kt new file mode 100644 index 00000000000..dd8f6634144 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/DefaultPushHistoryEntryPoint.kt @@ -0,0 +1,35 @@ +/* + * 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.troubleshoot.impl.history + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultPushHistoryEntryPoint @Inject constructor() : PushHistoryEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PushHistoryEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : PushHistoryEntryPoint.NodeBuilder { + override fun callback(callback: PushHistoryEntryPoint.Callback): PushHistoryEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryEvents.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryEvents.kt new file mode 100644 index 00000000000..c18a480899d --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryEvents.kt @@ -0,0 +1,14 @@ +/* + * 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.troubleshoot.impl.history + +sealed interface PushHistoryEvents { + data class SetShowOnlyErrors(val showOnlyErrors: Boolean) : PushHistoryEvents + data class Reset(val requiresConfirmation: Boolean) : PushHistoryEvents + data object ClearDialog : PushHistoryEvents +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryNode.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryNode.kt new file mode 100644 index 00000000000..347a1c27001 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryNode.kt @@ -0,0 +1,57 @@ +/* + * 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.troubleshoot.impl.history + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.MobileScreen +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope +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.troubleshoot.api.PushHistoryEntryPoint +import io.element.android.services.analytics.api.ScreenTracker + +@ContributesNode(SessionScope::class) +class PushHistoryNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: PushHistoryPresenter, + private val screenTracker: ScreenTracker, +) : Node(buildContext, plugins = plugins) { + private fun onDone() { + plugins().forEach { + it.onDone() + } + } + + private fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) { + plugins().forEach { + it.onItemClick(sessionId, roomId, eventId) + } + } + + @Composable + override fun View(modifier: Modifier) { + screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot) + val state = presenter.present() + PushHistoryView( + state = state, + onBackClick = ::onDone, + onItemClick = ::onItemClick, + modifier = modifier, + ) + } +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenter.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenter.kt new file mode 100644 index 00000000000..ec6f2f0abb8 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenter.kt @@ -0,0 +1,74 @@ +/* + * 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.troubleshoot.impl.history + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.push.api.PushService +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +class PushHistoryPresenter @Inject constructor( + private val pushService: PushService, +) : Presenter { + @Composable + override fun present(): PushHistoryState { + val coroutineScope = rememberCoroutineScope() + val pushCounter by pushService.pushCounter.collectAsState(0) + var showOnlyErrors: Boolean by remember { mutableStateOf(false) } + val pushHistory by remember(showOnlyErrors) { + pushService.getPushHistoryItemsFlow().map { + if (showOnlyErrors) { + it.filter { item -> item.hasBeenResolved.not() } + } else { + it + } + } + }.collectAsState(emptyList()) + var resetAction: AsyncAction by remember { mutableStateOf(AsyncAction.Uninitialized) } + + fun handleEvents(event: PushHistoryEvents) { + when (event) { + is PushHistoryEvents.SetShowOnlyErrors -> { + showOnlyErrors = event.showOnlyErrors + } + is PushHistoryEvents.Reset -> { + if (event.requiresConfirmation) { + resetAction = AsyncAction.ConfirmingNoParams + } else { + resetAction = AsyncAction.Loading + coroutineScope.launch { + pushService.resetPushHistory() + resetAction = AsyncAction.Uninitialized + } + } + } + PushHistoryEvents.ClearDialog -> { + resetAction = AsyncAction.Uninitialized + } + } + } + + return PushHistoryState( + pushCounter = pushCounter, + pushHistoryItems = pushHistory.toImmutableList(), + showOnlyErrors = showOnlyErrors, + resetAction = resetAction, + eventSink = ::handleEvents + ) + } +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryState.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryState.kt new file mode 100644 index 00000000000..fda9c6e479c --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryState.kt @@ -0,0 +1,20 @@ +/* + * 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.troubleshoot.impl.history + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.push.api.history.PushHistoryItem +import kotlinx.collections.immutable.ImmutableList + +data class PushHistoryState( + val pushCounter: Int, + val pushHistoryItems: ImmutableList, + val showOnlyErrors: Boolean, + val resetAction: AsyncAction, + val eventSink: (PushHistoryEvents) -> Unit, +) diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryStateProvider.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryStateProvider.kt new file mode 100644 index 00000000000..da37700a931 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryStateProvider.kt @@ -0,0 +1,80 @@ +/* + * 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.troubleshoot.impl.history + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction +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.push.api.history.PushHistoryItem +import kotlinx.collections.immutable.toImmutableList + +open class PushHistoryStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPushHistoryState(), + aPushHistoryState( + pushCounter = 123, + pushHistoryItems = listOf( + aPushHistoryItem( + hasBeenResolved = false, + comment = "An error description" + ), + aPushHistoryItem( + pushDate = 1, + providerInfo = "providerInfo2", + eventId = EventId("\$anEventId"), + roomId = RoomId("!roomId:domain"), + sessionId = SessionId("@alice:server.org"), + hasBeenResolved = true, + comment = "A comment" + ) + ) + ), + aPushHistoryState( + resetAction = AsyncAction.ConfirmingNoParams, + ), + ) +} + +fun aPushHistoryState( + pushCounter: Int = 0, + pushHistoryItems: List = emptyList(), + showOnlyErrors: Boolean = false, + resetAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (PushHistoryEvents) -> Unit = {}, +) = PushHistoryState( + pushCounter = pushCounter, + pushHistoryItems = pushHistoryItems.toImmutableList(), + showOnlyErrors = showOnlyErrors, + resetAction = resetAction, + eventSink = eventSink, +) + +fun aPushHistoryItem( + pushDate: Long = 0, + formattedDate: String = "formattedDate", + providerInfo: String = "providerInfo", + eventId: EventId? = null, + roomId: RoomId? = null, + sessionId: SessionId? = null, + hasBeenResolved: Boolean = false, + comment: String? = null, +): PushHistoryItem { + return PushHistoryItem( + pushDate = pushDate, + formattedDate = formattedDate, + providerInfo = providerInfo, + eventId = eventId, + roomId = roomId, + sessionId = sessionId, + hasBeenResolved = hasBeenResolved, + comment = comment + ) +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt new file mode 100644 index 00000000000..2cd2e6dc202 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt @@ -0,0 +1,276 @@ +/* + * 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.troubleshoot.impl.history + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets +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.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.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.designsystem.components.async.AsyncActionView +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.list.ListItemContent +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.DropdownMenu +import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.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.push.api.history.PushHistoryItem +import io.element.android.libraries.troubleshoot.impl.R +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PushHistoryView( + state: PushHistoryState, + onBackClick: () -> Unit, + onItemClick: (SessionId, RoomId, EventId) -> Unit, + modifier: Modifier = Modifier, +) { + var showMenu by remember { mutableStateOf(false) } + + Scaffold( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + contentWindowInsets = WindowInsets.statusBars, + topBar = { + TopAppBar( + navigationIcon = { + BackButton(onClick = onBackClick) + }, + titleStr = stringResource(R.string.screen_push_history_title), + actions = { + IconButton(onClick = { showMenu = !showMenu }) { + Icon( + imageVector = CompoundIcons.OverflowVertical(), + contentDescription = stringResource(id = CommonStrings.a11y_user_menu), + ) + } + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + ) { + DropdownMenuItem( + text = { Text("Show only errors") }, + trailingIcon = if (state.showOnlyErrors) { + { + Icon( + imageVector = CompoundIcons.CheckCircleSolid(), + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + } + } else { + null + }, + onClick = { + showMenu = false + state.eventSink(PushHistoryEvents.SetShowOnlyErrors(state.showOnlyErrors.not())) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(id = CommonStrings.action_reset)) }, + onClick = { + showMenu = false + state.eventSink(PushHistoryEvents.Reset(requiresConfirmation = true)) + }, + ) + } + } + ) + }, + ) { padding -> + PushHistoryContent( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding), + state = state, + onItemClick = onItemClick, + ) + } + + AsyncActionView( + async = state.resetAction, + onSuccess = {}, + confirmationDialog = { + ConfirmationDialog( + content = "", + title = stringResource(CommonStrings.dialog_title_confirmation), + submitText = stringResource(CommonStrings.action_reset), + cancelText = stringResource(CommonStrings.action_cancel), + onSubmitClick = { state.eventSink(PushHistoryEvents.Reset(requiresConfirmation = false)) }, + onDismiss = { state.eventSink(PushHistoryEvents.ClearDialog) }, + ) + }, + onErrorDismiss = {}, + ) +} + +@Composable +private fun PushHistoryContent( + state: PushHistoryState, + onItemClick: (SessionId, RoomId, EventId) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth() + ) { + ListItem( + headlineContent = { Text("Total number of received push") }, + trailingContent = ListItemContent.Text(state.pushCounter.toString()), + ) + LazyColumn( + modifier = Modifier.fillMaxWidth() + ) { + items( + items = state.pushHistoryItems, + key = { + it.pushDate.toString() + it.sessionId + it.roomId + it.eventId + }, + ) { pushHistory -> + PushHistoryItem( + pushHistory, + onClick = { + val sessionId = pushHistory.sessionId + val roomId = pushHistory.roomId + val eventId = pushHistory.eventId + if (sessionId != null && roomId != null && eventId != null) { + onItemClick(sessionId, roomId, eventId) + } + } + ) + } + } + } +} + +@Composable +private fun PushHistoryItem( + pushHistoryItem: PushHistoryItem, + onClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable { + onClick() + }, + ) { + HorizontalDivider() + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + ) { + Text( + text = pushHistoryItem.formattedDate, + color = ElementTheme.colors.textPrimary, + ) + Text( + text = pushHistoryItem.providerInfo, + color = ElementTheme.colors.textPrimary, + ) + Text( + modifier = Modifier.padding(start = 8.dp, top = 8.dp), + text = pushHistoryItem.sessionId?.value ?: "No sessionId", + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyMdRegular, + ) + Text( + modifier = Modifier.padding(start = 8.dp), + text = pushHistoryItem.roomId?.value ?: "No roomId", + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyMdRegular, + ) + Text( + modifier = Modifier.padding(start = 8.dp), + text = pushHistoryItem.eventId?.value ?: "No eventId", + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyMdRegular, + ) + pushHistoryItem.comment?.let { + Text( + modifier = Modifier.padding(top = 8.dp), + text = it, + color = if (pushHistoryItem.hasBeenResolved) { + ElementTheme.colors.textSecondary + } else { + ElementTheme.colors.textCriticalPrimary + }, + style = ElementTheme.typography.fontBodyMdRegular, + ) + } + } + if (pushHistoryItem.hasBeenResolved) { + Icon( + imageVector = CompoundIcons.CheckCircleSolid(), + modifier = Modifier.size(24.dp), + tint = ElementTheme.colors.iconSuccessPrimary, + contentDescription = null, + ) + } else { + Icon( + imageVector = CompoundIcons.Error(), + modifier = Modifier.size(24.dp), + tint = ElementTheme.colors.iconCriticalPrimary, + contentDescription = null, + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun PushHistoryViewPreview( + @PreviewParameter(PushHistoryStateProvider::class) state: PushHistoryState, +) = ElementPreview { + PushHistoryView( + state = state, + onBackClick = {}, + onItemClick = { _, _, _ -> }, + ) +} diff --git a/libraries/troubleshoot/impl/src/main/res/values-cs/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-cs/translations.xml index 17e47cf56f7..5e4a4f17576 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-cs/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-cs/translations.xml @@ -1,5 +1,6 @@ + "Historie push oznámení" "Spustit testy" "Spustit testy znovu" "Některé testy selhaly. Zkontrolujte prosím podrobnosti." diff --git a/libraries/troubleshoot/impl/src/main/res/values-cy/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..8ea2bd11e3a --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,12 @@ + + + "Hanes gwthio" + "Rhedeg profion" + "Rhedeg profion eto" + "Methodd rhai profion. Gwiriwch y manylion." + "Rhedwch y profion i ganfod unrhyw broblem yn eich ffurfweddiad a allai wneud i hysbysiadau beidio ag ymddwyn yn ôl y disgwyl." + "Ceisio trwsio" + "Pasiwyd pob prawf yn llwyddiannus." + "Hysbysiadau datrys problemau" + "Mae rhai profion angen eich sylw. Gwiriwch y manylion." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-da/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..c6c913dfa41 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,12 @@ + + + "Push-historik" + "Kør test" + "Kør test igen" + "Nogle tests mislykkedes. Tjek venligst detaljerne." + "Kør testene for at registrere de problemer i din konfiguration, der kan medføre, at meddelelser ikke opfører sig som forventet." + "Forsøg på at reparere" + "Alle tests bestået med succes." + "Fejlfinding af meddelelser" + "Nogle tests kræver din opmærksomhed. Tjek venligst detaljerne." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-de/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-de/translations.xml index 98b8c579a99..88a99f0f66f 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-de/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-de/translations.xml @@ -1,11 +1,12 @@ + "Verlauf pushen" "Tests durchführen" "Tests erneut durchführen" "Einige Tests sind fehlgeschlagen. Bitte überprüfe die Details." - "Führe die Tests durch, um Probleme zu erkennen, die dazu führen können, dass sich die Benachrichtigungen nicht wie erwartet verhalten." - "Versuche das Problem zu beheben" + "Führen Sie Tests durch, um Probleme in Ihren Einstellungen zu erkennen, die zu fehlerhaften Benachrichtigungen führen könnten." + "Problem beheben" "Alle Tests wurden erfolgreich bestanden." - "Fehlerbehebung für Benachrichtigungen" - "Einige Tests erfordern deine Aufmerksamkeit. Bitte überprüfe die Details." + "Beheben Sie die Fehler bei Benachrichtigungen" + "Einige Tests erfordern Ihre Aufmerksamkeit. Bitte überprüfen Sie die Angaben." diff --git a/libraries/troubleshoot/impl/src/main/res/values-el/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-el/translations.xml index 115d526e2d3..fcac4c0e50e 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-el/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-el/translations.xml @@ -1,5 +1,6 @@ + "Ιστορικό push" "Εκτέλεση δοκιμών" "Επανεκτέλεση δοκιμών" "Ορισμένες δοκιμές απέτυχαν. Παρακαλώ έλεγξε τις λεπτομέρειες." diff --git a/libraries/troubleshoot/impl/src/main/res/values-es/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-es/translations.xml new file mode 100644 index 00000000000..9a2e946c6ed --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,12 @@ + + + "Historial de notificaciones push" + "Ejecutar pruebas" + "Volver a ejecutar pruebas" + "Algunas pruebas fallaron. Por favor, verifica los detalles." + "Ejecuta las pruebas para detectar cualquier problema en tu configuración que pueda hacer que las notificaciones no se comporten como es debido." + "Intentar arreglar" + "Todas las pruebas se han superado con éxito." + "Solucionar problemas con las notificaciones" + "Algunas pruebas requieren tu atención. Por favor, verifica los detalles." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-et/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-et/translations.xml index b5e3e8bff7f..e2983c6d402 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-et/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-et/translations.xml @@ -1,5 +1,6 @@ + "Tõuketeadete ajalugu" "Käivita testid" "Käivita testid uuesti" "Mõned testid tuvastasid vigu. Palun vaata üksikasjalikku teavet." diff --git a/libraries/troubleshoot/impl/src/main/res/values-eu/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..f609e427c2b --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,9 @@ + + + "Exekutatu testak" + "Egin probak berriro" + "Proba batzuek huts egin dute. Aztertu xehetasunak." + "Saiatu konpontzen" + "Proba guztiak arrakastaz gainditu dira." + "Proba batzuek zure arreta eskatzen dute. Aztertu xehetasunak." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-fa/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-fa/translations.xml index 6df27f96f28..b25450d420e 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-fa/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-fa/translations.xml @@ -1,5 +1,6 @@ + "تاریخچهٔ فرستادن" "اجرای آزمون‌ها" "اجرای دوبارهٔ آزمون‌ها" "برخی آزمون‌ها شکست خوردند. بررسی جزییات." diff --git a/libraries/troubleshoot/impl/src/main/res/values-fi/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-fi/translations.xml index 527607dd4fd..9241534da0c 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-fi/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-fi/translations.xml @@ -1,5 +1,6 @@ + "Push-historia" "Suorita testit" "Suorita testit uudelleen" "Osa testeistä epäonnistui. Tarkista tiedot." diff --git a/libraries/troubleshoot/impl/src/main/res/values-fr/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-fr/translations.xml index 497b3e3b567..c920d3ff262 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-fr/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,6 @@ + "Historique des Push" "Exécuter les tests" "Relancer les tests" "Certains tests ont échoué. Veuillez vérifier les détails." diff --git a/libraries/troubleshoot/impl/src/main/res/values-hu/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-hu/translations.xml index 211b1f4a1d3..6f359ecb22c 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-hu/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-hu/translations.xml @@ -1,5 +1,6 @@ + "Leküldési értesítés előzmények" "Tesztek futtatása" "Tesztek újbóli futtatása" "Egyes tesztek sikertelenek voltak. Ellenőrizze a részleteket." diff --git a/libraries/troubleshoot/impl/src/main/res/values-in/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-in/translations.xml new file mode 100644 index 00000000000..a2a6039f745 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,12 @@ + + + "Riwayat dorongan" + "Jalankan tes" + "Jalankan tes lagi" + "Beberapa tes gagal. Silakan periksa detailnya." + "Jalankan pengujian untuk mendeteksi masalah apa pun dalam konfigurasi Anda yang mungkin membuat notifikasi tidak berperilaku seperti yang diharapkan." + "Mencoba untuk memperbaiki" + "Semua tes berhasil dilalui." + "Pecahkan masalah notifikasi" + "Beberapa tes membutuhkan perhatian Anda. Silakan periksa detailnya." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-it/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-it/translations.xml index 4c359a81405..946915fe693 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-it/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-it/translations.xml @@ -1,5 +1,6 @@ + "Cronologia push" "Esegui i test" "Esegui nuovamente i test" "Alcuni test sono falliti. Si prega di controllare i dettagli." diff --git a/libraries/troubleshoot/impl/src/main/res/values-ka/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ka/translations.xml new file mode 100644 index 00000000000..347301a7767 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-ka/translations.xml @@ -0,0 +1,11 @@ + + + "ტესტების გაშვება" + "ტესტების ისევ გაშვება" + "ზოგიერთი ტესტი წარუმატებელია. გთხოვთ შეამოწმოთ დეტალები." + "ჩაატარეთ ტესტები თქვენს კონფიგურაციაში იმ პრობლემების აღმოსაჩენად, რომლებიც გამოიწვევენ შეტყობინებების არასწორ ქცევას." + "შეკეთების მცდელობა" + "ყველა ტესტი წარმატებით დასრულდა." + "პრობლემების გადაჭრის შეტყობინებები" + "ზოგიერთი ტესტი ითხოვს თქვენს ყურადღებას. გთხოვთ შეამოწმოთ დეტალები." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-nb/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-nb/translations.xml index fdd8ac10f97..92b31f77658 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-nb/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-nb/translations.xml @@ -1,5 +1,12 @@ + "Push-historikk" + "Kjør tester" + "Kjør tester igjen" + "Noen tester mislyktes. Vennligst sjekk detaljene." + "Kjør testene for å avdekke eventuelle problemer i konfigurasjonen som kan føre til at varslene ikke oppfører seg som forventet." "Forsøk å fikse" "Alle testene ble bestått." + "Feilsøk varsler" + "Noen tester krever din oppmerksomhet. Vennligst sjekk detaljene." diff --git a/libraries/troubleshoot/impl/src/main/res/values-pl/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-pl/translations.xml index 31d165d3d67..0a023639070 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-pl/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-pl/translations.xml @@ -1,5 +1,6 @@ + "Historia powiadomień Push" "Uruchom testy" "Uruchom testy ponownie" "Niektóre testy się nie powiodły. Sprawdź szczegóły." diff --git a/libraries/troubleshoot/impl/src/main/res/values-pt-rBR/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-pt-rBR/translations.xml index 66f6e46cb3a..6990ca5dbe3 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-pt-rBR/translations.xml @@ -1,4 +1,12 @@ + "Histórico de push" "Execute testes" + "Execute os testes novamente" + "Alguns testes falharam. Verifique os detalhes." + "Execute os testes para detetar qualquer problema na sua configuração que possa fazer com que as notificações não se comportem como esperado." + "Tentativa de corrigir" + "Todos os testes foram aprovados com sucesso." + "Solucionar problemas de notificações" + "Alguns testes exigem sua atenção. Verifique os detalhes." diff --git a/libraries/troubleshoot/impl/src/main/res/values-pt/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-pt/translations.xml index d6972ebc6ae..d82bbd2e15c 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-pt/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-pt/translations.xml @@ -1,5 +1,6 @@ + "Histórico de notificações" "Correr testes" "Correr testes novamente" "Alguns testes falharam. Por favor, verifica os detalhes." diff --git a/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml index 943652fc86d..a37b3c8e132 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml @@ -1,5 +1,6 @@ + "История уведомлений" "Выполнение тестов" "Повторное выполнение тестов" "Некоторые тесты провалились. Пожалуйста, проверьте детали." diff --git a/libraries/troubleshoot/impl/src/main/res/values-sk/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-sk/translations.xml index 3b7a08c2f0c..e6616609981 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-sk/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,6 @@ + "História push oznámení" "Spustiť testy" "Spustiť testy znova" "Niektoré testy zlyhali. Skontrolujte prosím podrobnosti." diff --git a/libraries/troubleshoot/impl/src/main/res/values-sv/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-sv/translations.xml index 77f07c48f47..28ff461baca 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-sv/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-sv/translations.xml @@ -1,5 +1,6 @@ + "Push-historik" "Kör tester" "Kör tester igen" "Vissa tester misslyckades. Kontrollera detaljerna." diff --git a/libraries/troubleshoot/impl/src/main/res/values-tr/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..24dba503e1a --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,11 @@ + + + "Testleri çalıştır" + "Testleri yeniden çalıştır" + "Bazı testler başarısız oldu. Lütfen ayrıntıları kontrol edin." + "Yapılandırmanızda bildirimlerin beklendiği gibi davranmamasına neden olabilecek herhangi bir sorunu algılamak için testleri çalıştırın." + "Düzeltmeye çalışın" + "Tüm testler başarıyla geçti." + "Sorun Giderme Bildirimleri" + "Bazı testlere bakmanız gerekiyor. Lütfen ayrıntıları kontrol edin." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-uk/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-uk/translations.xml index 63e887af010..879ce096077 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-uk/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-uk/translations.xml @@ -1,5 +1,6 @@ + "Історія push-сповіщень" "Запустити тести" "Запустити тести знову" "Деякі тести не пройдено. Перегляньте подробиці." diff --git a/libraries/troubleshoot/impl/src/main/res/values-ur/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 00000000000..509d332d3d2 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,11 @@ + + + "جانچہا چلائیں" + "جانچہا دوبارہ چلائیں" + "کچھ جانچیں ناکام۔ برائے مہربانی تفصیلات پڑتال کریں۔" + "اپنی تکوین میں کسی بھی مسئلے کا کھوج لگانے کے لیے جانچ ہا چلائیں جس سے اطلاعات توقع کے مطابق برتاؤ نہ کریں۔" + "ٹھیک کرنے کی کوشش کریں" + "تمام جانچیں کامیابی سے گزاریں ہیں۔" + "اطلاعات کا ازالہ کریں" + "کچھ جانچوں کیلئے آپکی توجہ درکار ہے۔ برائے مہربانی تفصیلات پڑتال کریں۔" + diff --git a/libraries/troubleshoot/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-zh-rTW/translations.xml index 64362a6281e..4249f8ae5b6 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,5 +1,6 @@ + "推播通知歷史紀錄" "執行測試" "再次執行測試" "部份測試失敗。請檢查詳細資訊。" diff --git a/libraries/troubleshoot/impl/src/main/res/values/localazy.xml b/libraries/troubleshoot/impl/src/main/res/values/localazy.xml index eee100d7111..0427392db67 100644 --- a/libraries/troubleshoot/impl/src/main/res/values/localazy.xml +++ b/libraries/troubleshoot/impl/src/main/res/values/localazy.xml @@ -1,5 +1,6 @@ + "Push history" "Run tests" "Run tests again" "Some tests failed. Please check the details." diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenterTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenterTest.kt new file mode 100644 index 00000000000..313735b6e66 --- /dev/null +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenterTest.kt @@ -0,0 +1,129 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.troubleshoot.impl.history + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.test.FakePushService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PushHistoryPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPushHistoryPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.pushCounter).isEqualTo(0) + assertThat(initialState.pushHistoryItems).isEmpty() + assertThat(initialState.showOnlyErrors).isFalse() + assertThat(initialState.resetAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - updating state`() = runTest { + val pushService = FakePushService() + val presenter = createPushHistoryPresenter( + pushService = pushService, + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.pushCounter).isEqualTo(0) + assertThat(initialState.pushHistoryItems).isEmpty() + pushService.emitPushCounter(1) + assertThat(awaitItem().pushCounter).isEqualTo(1) + val item = aPushHistoryItem() + pushService.emitPushHistoryItems(listOf(item)) + assertThat(awaitItem().pushHistoryItems).containsExactly(item) + } + } + + @Test + fun `present - reset and cancel`() = runTest { + val resetPushHistoryResult = lambdaRecorder { } + val pushService = FakePushService( + resetPushHistoryResult = resetPushHistoryResult, + ) + val presenter = createPushHistoryPresenter( + pushService = pushService, + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(PushHistoryEvents.Reset(requiresConfirmation = true)) + assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.ConfirmingNoParams) + initialState.eventSink(PushHistoryEvents.ClearDialog) + assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.Uninitialized) + resetPushHistoryResult.assertions().isNeverCalled() + } + } + + @Test + fun `present - reset and confirm`() = runTest { + val resetPushHistoryResult = lambdaRecorder { } + val pushService = FakePushService( + resetPushHistoryResult = resetPushHistoryResult, + ) + val presenter = createPushHistoryPresenter( + pushService = pushService, + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(PushHistoryEvents.Reset(requiresConfirmation = true)) + assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.ConfirmingNoParams) + initialState.eventSink(PushHistoryEvents.Reset(requiresConfirmation = false)) + assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.Loading) + assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.Uninitialized) + resetPushHistoryResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - set show only errors`() = runTest { + val pushService = FakePushService() + val presenter = createPushHistoryPresenter( + pushService = pushService, + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.showOnlyErrors).isFalse() + val item = aPushHistoryItem(hasBeenResolved = true) + val itemError = aPushHistoryItem(hasBeenResolved = false) + pushService.emitPushHistoryItems(listOf(item, itemError)) + awaitItem().let { state -> + assertThat(state.pushHistoryItems).containsExactly(item, itemError) + state.eventSink(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = true)) + } + skipItems(1) + awaitItem().let { state -> + assertThat(state.showOnlyErrors).isTrue() + assertThat(state.pushHistoryItems).containsExactly(itemError) + state.eventSink(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = false)) + } + skipItems(1) + awaitItem().let { state -> + assertThat(state.showOnlyErrors).isFalse() + assertThat(state.pushHistoryItems).containsExactly(item, itemError) + } + } + } + + private fun createPushHistoryPresenter( + pushService: PushService = FakePushService(), + ): PushHistoryPresenter { + return PushHistoryPresenter( + pushService = pushService, + ) + } +} 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 new file mode 100644 index 00000000000..5c98b2c21a4 --- /dev/null +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt @@ -0,0 +1,142 @@ +/* + * 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.troubleshoot.impl.history + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +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.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_FORMATTED_DATE +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.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithThreeParams +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +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`() { + val eventsRecorder = EventsRecorder() + rule.setPushHistoryView( + aPushHistoryState( + pushCounter = 123, + eventSink = eventsRecorder, + ), + ) + 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 + rule.onNodeWithText("123").assertExists() + } + + @Test + fun `clicking on show only errors sends a PushHistoryEvents(true)`() { + val eventsRecorder = EventsRecorder() + rule.setPushHistoryView( + aPushHistoryState( + showOnlyErrors = false, + eventSink = eventsRecorder, + ), + ) + val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) + rule.onNodeWithContentDescription(menuContentDescription).performClick() + rule.onNodeWithText("Show only errors").performClick() + eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = true)) + } + + @Test + fun `clicking on show only errors sends a PushHistoryEvents(false)`() { + val eventsRecorder = EventsRecorder() + rule.setPushHistoryView( + aPushHistoryState( + showOnlyErrors = true, + eventSink = eventsRecorder, + ), + ) + val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) + rule.onNodeWithContentDescription(menuContentDescription).performClick() + rule.onNodeWithText("Show only errors").performClick() + eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = false)) + } + + @Test + fun `clicking on an invalid event has no effect`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setPushHistoryView( + aPushHistoryState( + pushHistoryItems = listOf( + aPushHistoryItem( + formattedDate = A_FORMATTED_DATE, + ) + ), + eventSink = eventsRecorder, + ), + ) + rule.onNodeWithText(A_FORMATTED_DATE).performClick() + // No callback invoked + } + + @Test + fun `clicking on a valid event invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val onItemClick = lambdaRecorder { _, _, _ -> } + rule.setPushHistoryView( + aPushHistoryState( + pushHistoryItems = listOf( + aPushHistoryItem( + formattedDate = A_FORMATTED_DATE, + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + sessionId = A_SESSION_ID, + ) + ), + eventSink = eventsRecorder, + ), + onItemClick = onItemClick, + ) + rule.onNodeWithText(A_FORMATTED_DATE).performClick() + onItemClick.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) + } +} + +private fun AndroidComposeTestRule.setPushHistoryView( + state: PushHistoryState, + onBackClick: () -> Unit = EnsureNeverCalled(), + onItemClick: (SessionId, RoomId, EventId) -> Unit = EnsureNeverCalledWithThreeParams(), +) { + setContent { + PushHistoryView( + state = state, + onBackClick = onBackClick, + onItemClick = onItemClick, + ) + } +} diff --git a/libraries/ui-strings/build.gradle.kts b/libraries/ui-strings/build.gradle.kts index 2f6a51497d6..7e34891c668 100644 --- a/libraries/ui-strings/build.gradle.kts +++ b/libraries/ui-strings/build.gradle.kts @@ -11,4 +11,8 @@ plugins { android { namespace = "io.element.android.libraries.ui.strings" + + lint { + disable += "Typos" + } } 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 fbea21c5126..0a508dfd5ea 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -1,5 +1,6 @@ + "Аватар" "Выдаліць" "Уведзеная лічба %1$d" @@ -7,6 +8,7 @@ "Уведзена %1$d лічб" "Схаваць пароль" + "Далучыцца да выкліку" "Перайсці ўніз" "Толькі згадкі" "Гук адключаны" @@ -26,11 +28,12 @@ "Прачытана %1$s" "Націсніце, каб паказаць усе" - "Выдаліць рэакцыю з %1$s" + "Выдаліць рэакцыю з %1$s" "Адправіць файлы" "Паказаць пароль" "Пазваніць" "Меню карыстальніка" + "Паглядзець падрабязнасці" "Запісаць галасавое паведамленне." "Спыніць запіс" "Прыняць" @@ -57,6 +60,7 @@ "Выдаліць апытанне" "Адключыць" "Адмяніць" + "Aдхіліць" "Гатова" "Рэдагаваць" "Рэдагаваць апытанне" @@ -122,6 +126,8 @@ "Прагляд у хроніцы" "Прагляд зыходнага кода" "Так" + "Ваш сервер зараз падтрымлівае новы, хутчэйшы пратакол. Выйдзіце з сістэмы і зноў увайдзіце, каб абнавіць яе. Гэта дапаможа вам пазбегнуць прымусовага выхаду з сістэмы, калі стары пратакол будзе пазней выдалены." + "Даступна абнаўленне" "Аб праграме" "Палітыка дапушчальнага выкарыстання" "Пашыраныя налады" @@ -166,6 +172,11 @@ "Светлая" "Спасылка скапіравана ў буфер абмену" "Загрузка…" + + "%d іншы" + "%d іншыя" + "%d іншых" + "%1$d удзельнік" "%1$d удзельнікі" @@ -296,6 +307,8 @@ "Замацаваныя паведамленні" "Адклікаць праверку і адправіць" "Усё роўна адправіць паведамленне" + "%1$sвыкарыстоўвае адну або некалькі неправераных прылад. Вы можаце адправіць паведамленне ў любым выпадку, або вы можаце адмяніць зараз і паўтарыць спробу пазней, калі %2$s праверыць усе свае прылады." + "Ваша паведамленне не было адпраўлена, таму што%1$s не праверыў усе прылады" "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." "Не ўдалося атрымаць інфармацыю пра карыстальніка" "%1$s з %2$s" @@ -309,6 +322,7 @@ "Адкрыць у Google Maps" "Адкрыць у OpenStreetMap" "Падзяліцеся гэтым месцазнаходжаннем" + "Паведамленне не адпраўлена таму што%1$s не праверыў усе прылады." "Месцазнаходжанне" "Версія: %1$s (%2$s)" "be" 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 dbe0fa17dcc..f6f999a1a1b 100644 --- a/libraries/ui-strings/src/main/res/values-bg/translations.xml +++ b/libraries/ui-strings/src/main/res/values-bg/translations.xml @@ -23,10 +23,11 @@ "Прочетено от %1$s и %2$d други" "Прочетено от %1$s" - "Премахване на реакция с %1$s" + "Премахване на реакция с %1$s" "Изпращане на файлове" "Показване на паролата" "Потребителско меню" + "Вижте подробности" "Приемане" "Назад" "Отказ" @@ -39,9 +40,13 @@ "Копиране" "Копиране на връзката" "Копиране на връзката към съобщението" + "Копиране на текста" "Създаване" "Създаване на стая" + "Деактивиране" + "Деактивиране на акаунта" "Отхвърляне" + "Отхвърляне и блокиране" "Изтриване на анкетата" "Деактивиране" "Готово" @@ -52,6 +57,7 @@ "Въведете PIN" "Забравена парола?" "Препращане" + "Игнориране" "Поканване" "Поканване на хора" "Поканване на хора в %1$s" @@ -64,16 +70,19 @@ "Напускане на стаята" "Зареждане на още" "Управление на профила" - "Управление на устройства" + "Управление на устройствата" + "Съобщение" "Не" "Не сега" "Добре" "Настройки" "Отваряне с" + "Закачане" "Бърз отговор" "Цитат" "Реакция" "Премахване" + "Премахване на съобщението" "Отговор" "Отговор в нишка" "Докладване на съдържанието" @@ -85,6 +94,7 @@ "Изпращане на съобщение" "Споделяне" "Споделяне на връзката" + "Показване" "Влизане отново" "Изход" "Излизане въпреки това" @@ -97,6 +107,7 @@ "Повторен опит" "Преглед на източника" "Да" + "Да, опитай отново" "Относно" "Разширени настройки" "Статистика" @@ -109,22 +120,34 @@ "Грешка при разшифроване" "Опции за разработчици" "Директен чат" + "Не показвай това отново" + "Изтегля се" "(редактирано)" "Редактиране" "* %1$s %2$s" + "Празен файл" + "Шифроване" "Шифроването е включено" "Въведете своя PIN" "Грешка" + "Всеки" "Фаворизиране" "Файл" + "Файлът е изтрит" + "Файлът е запазен" "Препращане на съобщението" "GIF" "Изображение" "В отговор на %1$s" "Инсталиране на APK" + "Стаята се напуска" "Светъл" "Връзката е копирана в клипборда" "Зарежда се…" + + "%d друг" + "%d други" + "%1$d член" "%1$d членове" @@ -135,12 +158,14 @@ "Модерно" "Заглушаване" "Няма резултати" + "Без шифроване" "Офлайн" "или" "Парола" "Хора" "Постоянна връзка" "Разрешение" + "Моля, изчакайте…" "Сигурни ли сте, че искате да приключите тази анкета?" "Анкета: %1$s" "Общо гласове: %1$s" @@ -152,6 +177,7 @@ "Политика за поверителност" "Реакция" "Реакции" + "Причина" "Ключ за възстановяване" "Съобщаване за грешка" "Съобщаване за проблем" @@ -159,6 +185,7 @@ "Стая" "Име на стаята" "напр. името на вашия проект" + "Запазва се" "Заключване на екрана" "Търсене на някого" "Резултати от търсенето" @@ -171,6 +198,7 @@ "Настройки" "Споделено местоположение" "Излизате" + "Възникна проблем. Моля, опитайте отново." "Започване на чат…" "Успешно" "Предложения" @@ -188,16 +216,23 @@ "Потребителско име" "Потвърждаването е отменено" "Потвърждаването е завършено" + "Неуспешно потвърждаване" + "Потвърден" "Потвърждаване на устройството" + "Потвърждаване на самоличността" + "Потвърждаване на потребителя" "Видео" "Гласово съобщение" "В очакване на това съобщение" + "Вие" "Грешка" "Успешно" "Внимание" + "Без шифроване" "🔐️ Присъединете се към мен в %1$s" "Хей, говорете с мен в %1$s: %2$s" "%1$s Android" + "Закачени съобщения" "Споделяне на местоположение" "Споделяне на моето местоположение" "Отваряне в Apple Maps" @@ -207,4 +242,6 @@ "Местоположение" "Версия: %1$s (%2$s)" "bg" + "Трябва да потвърдите това устройство за да достъпите исторически съобщения" + "Съобщението не може да се разшифрова" 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 43806c92150..7091d73b8bf 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -1,5 +1,7 @@ + "Přidat reakci: %1$s" + "Profilový obrázek" "Smazat" "zadána %1$d číslice" @@ -7,11 +9,13 @@ "zadáno %1$d číslic" "Skrýt heslo" + "Připojit se k hovoru" "Přejít dolů" "Pouze zmínky" "Ztišeno" "Strana %1$d" "Pozastavit" + "Hlasová zpráva, délka: %1$s, aktuální pozice: %2$s" "Pole pro PIN" "Přehrát" "Hlasování" @@ -26,12 +30,14 @@ "%1$s přečetl(a)" "Klepnutím zobrazíte vše" - "Odstraňit reakci s %1$s" + "Odstraňit reakci s %1$s" + "Odstranit reakci pomocí %1$s" "Odeslat soubory" "Zobrazit heslo" "Zahájit hovor" "Uživatelské menu" "Zobrazit podrobnosti" + "Hlasová zpráva, délka: %1$s" "Nahrajte hlasovou zprávu." "Zastavit nahrávání" "Přijmout" @@ -58,9 +64,11 @@ "Deaktivovat" "Deaktivovat účet" "Odmítnout" + "Odmítnout a zablokovat" "Odstranit hlasování" "Zakázat" "Vyřadit" + "Zavřít" "Hotovo" "Upravit" "Upravit titulek" @@ -102,8 +110,11 @@ "Odstranit zprávu" "Odpovědět" "Odpovědět ve vlákně" + "Nahlásit" "Nahlásit chybu" "Nahlásit obsah" + "Nahlásit konverzaci" + "Nahlásit místnost" "Obnovit" "Obnovit identitu" "Zkusit znovu" @@ -131,10 +142,13 @@ "Zobrazit zdroj" "Ano" "Ano, zkusit znovu" + "Váš server nyní podporuje nový, rychlejší protokol. Chcete-li upgradovat, odhlaste se a znovu se přihlaste. Pokud to uděláte nyní, pomůže vám vyhnout se nucenému odhlášení, když bude starý protokol později odstraněn." + "Upgrade k dispozici" "O aplikaci" "Zásady používání" "Přidání titulku" "Pokročilá nastavení" + "obrázek" "Analytika" "Vzhled" "Zvuk" @@ -145,7 +159,9 @@ "Zkopírováno do schránky" "Autorská práva" "Vytváření místnosti…" - "Opustit místnost" + "Žádost zrušena" + "Místnost opuštěna" + "Pozvánka odmítnuta" "Tmavé" "Chyba dešifrování" "Možnosti pro vývojáře" @@ -158,6 +174,7 @@ "Úpravy" "Úprava titulku" "* %1$s %2$s" + "Prázdný soubor" "Šifrování" "Šifrování povoleno" "Zadejte svůj PIN" @@ -182,9 +199,15 @@ 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" "Světlý" + "Řádek zkopírován do schránky" "Odkaz zkopírován do schránky" "Načítání…" "Načítání dalších…" + + "%d další" + "%d další" + "%d dalších" + "%1$d člen" "%1$d členové" @@ -196,8 +219,10 @@ Důvod: %1$s." "Zpráva byla odstraněna" "Moderní" "Ztlumit" + "%1$s (%2$s)" "Žádné výsledky" "Žádný název místnosti" + "Nešifrováno" "Offline" "Licence s otevřeným zdrojovým kódem" "nebo" @@ -206,6 +231,7 @@ Důvod: %1$s." "Trvalý odkaz" "Oprávnění" "Připnuto" + "Zkontrolujte prosím své připojení k internetu" "Počkejte prosím…" "Opravdu chcete ukončit toto hlasování?" "Hlasování: %1$s" @@ -219,8 +245,12 @@ Důvod: %1$s." "Zásady ochrany osobních údajů" "Reakce" "Reakce" + "Důvod" "Klíč pro obnovení" "Obnovování…" + + "%1$d odpovědí" + "Odpověď na %1$s" "Nahlásit chybu" "Nahlásit problém" @@ -240,12 +270,14 @@ Důvod: %1$s." "Odesílání…" "Odeslání se nezdařilo" "Odesláno" + ". " "Server není podporován" "URL serveru" "Nastavení" "Sdílená poloha" "Odhlašování" - "Něco se pokazilo" + "Něco se nepovedlo" + "Narazili jsme na problém. Zkuste to prosím znovu." "Zahajování chatu…" "Nálepka" "Úspěch" @@ -273,17 +305,25 @@ Důvod: %1$s." "Ověření se nezdařilo" "Ověřeno" "Ověřit zařízení" - "Ověření totožnosti" + "Ověření identity" + "Ověřit uživatele" "Video" "Hlasová zpráva" "Čekání…" "Čekání na dešifrovací klíč" "Vy" - "Zdá se, že se identita %1$s změnila. %2$s" - "Zdá se, že identita %1$s %2$s se změnila. %3$s" + "Identita uživatele %1$s se změnila. %2$s" + "Identita uživatele %1$s %2$s se změnila. %3$s" "(%1$s)" - "Ověřená identita uživatele %1$s %2$s se změnila. %3$s" + "Identita uživatele %1$s se změnila." + "Identita uživatele %1$s %2$s se změnila. %3$s" "Zrušit ověření" + "Odkaz %1$s vás přesměruje na jinou stránku %2$s + +Opravdu chcete pokračovat?" + "Zkontrolujte tento odkaz" + "Místnost nahlášena" + "Nahlášen a opustil místnost" "Potvrzení" "Chyba" "Úspěch" @@ -300,10 +340,12 @@ Důvod: %1$s." "%1$s nemá oprávnění k přístupu k vaší poloze. Přístup můžete povolit v Nastavení." "%1$s nemá oprávnění k přístupu k vaší poloze. Povolit přístup níže." "%1$s nemá oprávnění k přístupu k mikrofonu. Povolte přístup k nahrávání hlasové zprávy." + "Může to být způsobeno problémy se sítí nebo serverem." "Tato adresa místnosti již existuje, zkuste prosím upravit pole adresy místnosti nebo změnit název místnosti" "Některé znaky nejsou povoleny. Podporovány jsou pouze písmena, číslice a následující symboly ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Některé zprávy nebyly odeslány" "Omlouváme se, došlo k chybě" + "Odesílatel události se neshoduje s vlastníkem zařízení, které ji odeslalo." "Autenticitu této zašifrované zprávy nelze na tomto zařízení zaručit." "Zašifrováno dříve ověřeným uživatelem." "Není zašifrováno." @@ -314,6 +356,10 @@ Důvod: %1$s." "Ahoj, ozvi se mi na %1$s: %2$s" "%1$s Android" "Zatřeste zařízením pro nahlášení chyby" + "%1$s: %2$s" + "Možnosti" + "Odstranit %1$s" + "Nastavení" "Výběr média se nezdařil, zkuste to prosím znovu." "Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace." "Nahrání média se nezdařilo, zkuste to prosím znovu." @@ -338,12 +384,15 @@ Důvod: %1$s." "Vaše zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení" "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nepodařilo se načíst údaje o uživateli" + "Zpráva v %1$s" + "Rozbalit" + "Zmenšit" + "Již si prohlížíte tuto místnost!" "%1$s z %2$s" "%1$s Připnuté zprávy" "Načítání zprávy…" "Zobrazit vše" "Chat" - "Žádost o vstup odeslána" "Sdílet polohu" "Sdílet moji polohu" "Otevřít v Mapách Apple" @@ -360,5 +409,5 @@ Důvod: %1$s." "Pro přístup k historickým zprávám musíte toto zařízení ověřit" "Nemáte přístup k této zprávě" "Nelze dešifrovat zprávu" - "Tato zpráva byla zablokována buď proto, že jste neověřili své zařízení, nebo proto, že odesílatel potřebuje ověřit vaši totožnost." + "Tato zpráva byla zablokována buď proto, že jste neověřili své zařízení, nebo proto, že odesílatel potřebuje ověřit vaši identitu." diff --git a/libraries/ui-strings/src/main/res/values-cy/translations.xml b/libraries/ui-strings/src/main/res/values-cy/translations.xml new file mode 100644 index 00000000000..6dc6b9237cc --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-cy/translations.xml @@ -0,0 +1,436 @@ + + + "Ychwanegu adwaith: %1$s" + "Afatar" + "Dileu" + + "%1$d nodau wedi eu cynnig" + "%1$d nod wedi ei gynnig" + "%1$d nod wedi eu cynnig" + "%1$d nod wedi eu cynnig" + "%1$d nod wedi eu cynnig" + "%1$d nod wedi eu cynnig" + + "Cuddio cyfrinair" + "Ymuno â galwad" + "Symud i\'r gwaelod" + "Crybwylliadau\'n unig" + "Wedi\'i Dewi" + "Afatar defnyddiwr arall" + "Tudalen %1$d" + "Oedi" + "Neges llais, hyd: %1$s, safle presennol: %2$s" + "Maes PIN" + "Chwarae" + "Pôl" + "Mae\'r bleidlais wedi gorffen" + "Ymateb gyda %1$s" + "Ymateb gydag emojis eraill" + "Wedi\'i ddarllen gan %1$s a %2$s" + + "Wedi\'i ddarllen gan %1$s a %2$d arall" + "Wedi\'i ddarllen gan %1$s a %2$d arall" + "Wedi\'i ddarllen gan %1$s a %2$d arall" + "Wedi\'i ddarllen gan %1$s a %2$d arall" + "Wedi\'i ddarllen gan %1$s a %2$d arall" + "Wedi\'i ddarllen gan %1$s a %2$d arall" + + "Wedi\'i ddarllen gan %1$s" + "Tapio i ddangos y cyfan" + "Wedi dileu adwaith gyda %1$s" + "Wedi dileu adwaith gyda %1$s" + "Afatar ystafell" + "Anfon ffeiliau" + "Dangos y cyfrinair" + "Cychwyn galwad" + "Afatar defnyddiwr" + "Dewislen defnyddiwr" + "Gweld afatar" + "Manylion" + "Neges llais, hyd: %1$s" + "Recordio neges llais." + "Stopio recordio" + "Eich afatar" + "Derbyn" + "Ychwanegu capsiwn" + "Ychwanegu at y llinell amser" + "Nôl" + "Galw" + "Nôl" + "Diddymu am y tro" + "Dewis llun" + "Clirio" + "Cau" + "Cwblhau\'r gwirio" + "Cadarnhau" + "Cadarnhau cyfrinair" + "Parhau" + "Copïo" + "Copïo capsiwn" + "Copïo dolen" + "Copïo dolen i\'r neges" + "Copïo testun" + "Creu" + "Creu ystafell" + "Dadweithredu" + "Cau cyfrif" + "Gwrthod" + "Gwrthod a rhwystro" + "Dileu Pleidlais" + "Analluogi" + "Dileu" + "Cau" + "Gorffen" + "Golygu" + "Golygu capsiwn" + "Golygu pleidlais" + "Galluogi" + "Gorffen pleidlais" + "Rhoi\'r PIN" + "Wedi anghofio\'ch cyfrinair?" + "Ymlaen" + "Mynd nôl" + "Anwybyddu" + "Gwahodd" + "Gwahodd pobl" + "Gwahodd pobl i %1$s" + "Gwahodd pobl i %1$s" + "Gwahoddiadau" + "Ymuno" + "Dysgu rhagor" + "Gadael" + "Gadael y sgwrs" + "Gadael yr ystafell" + "Llwytho rhagor" + "Rheoli cyfrif" + "Rheoli dyfeisiau" + "Neges" + "Nesaf" + "Na" + "Nid nawr" + "Iawn" + "Gosodiadau" + "Agor gyda" + "Pinio" + "Ymateb cyflym" + "Dyfyniad" + "Ymateb" + "Gwrthod" + "Tynnu" + "Dileu capsiwn" + "Dileu neges" + "Ateb" + "Ateb mewn edefyn" + "Adroddiadau" + "Adrodd ar wall" + "Adrodd ar gynnwys" + "Adrodd ar sgwrs" + "Adrodd ar ystafell" + "Ailosod" + "Ailosod hunaniaeth" + "Ceisio eto" + "Cynnig arall ar ddadgryptio" + "Cadw" + "Chwilio" + "Anfon" + "Anfonwch neges" + "Rhannu" + "Rhannwch ddolen" + "Dangos" + "Mewngofnodi eto" + "Allgofnodi" + "Allgofnodi beth bynnag" + "Hepgor" + "Cychwyn" + "Dechrau sgwrs" + "Dechrau dilysu" + "Tapio i lwytho map" + "Cymryd llun" + "Tapio am ddewisiadau" + "Ceisiwch eto" + "Dad-binio" + "Golwg" + "Gweld yn yr amserlen" + "Gweld ffynhonnell" + "Iawn" + "Iawn, ceisiwch eto" + "Mae eich gweinydd nawr yn cefnogi protocol newydd, cynt. Allgofnodwch a mewngofnodi\'n ôl i uwchraddio nawr. Bydd gwneud hyn nawr yn eich helpu i osgoi allgofnodi gorfodol pan fydd yr hen brotocol yn cael ei ddileu cyn hir." + "Uwchraddiad ar gael" + "Ynghylch" + "Polisi defnydd derbyniol" + "Ychwanegu capsiwn" + "Gosodiadau uwch" + "Delwedd wedi\'i thocio." + "Dadansoddeg" + "Gwedd" + "Sain" + "Defnyddwyr wedi\'u rhwystro" + "Swigod" + "Galwad wedi dechrau" + "Sgwrs wrth gefn" + "Wedi\'i gopïo i\'r clipfwrdd" + "Hawlfraint" + "Wrthi\'n creu ystafell…" + "Cais wedi\'i ddiddymu" + "Wedi gadael yr ystafell" + "Wedi gwrthod y gwahoddiad" + "Tywyll" + "Gwall dadgryptio" + "Dewisiadau datblygwr" + "ID dyfais" + "Sgwrs uniongyrchol" + "Peidio â dangos hyn eto" + "Methodd y llwytho i lawr" + "Yn llwytho i lawr" + "(golygwyd)" + "Golygu" + "Wrthi\'n golygu capsiwn" + "* %1$s %2$s" + "Ffeil wag" + "Amgryptiad" + "Amgryptio wedi\'i alluogi" + "Rhowch eich PIN" + "Gwall" + "Digwyddodd gwall, efallai fyddwch chi ddim yn derbyn hysbysiadau ar gyfer negeseuon newydd. Ceisiwch ddatrys y problemau hysbysiadau o\'r gosodiadau. + +Rheswm: %1$s." + "Pawb" + "Wedi methu" + "Ffefryn" + "Ffafriwyd" + "Ffeil" + "Ffeil wedi\'i dileu" + "Ffeil wedi\'i chadw" + "Ffeil wedi\'i chadw i\'r Llwythi" + "Anfonwyd neges ymlaen" + "Defnydd cyffredin" + "GIF" + "Delwedd" + "Mewn ymateb i %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…" + "Wrthi\'n llwytho mwy…" + + "%d arall" + "%d arall" + "%d arall" + "%d arall" + "%d arall" + "%d arall" + + + "%1$d aelodau" + "%1$d aelod" + "%1$d aelod" + "%1$d aelod" + "%1$d aelod" + "%1$d aelod" + + "Neges" + "Gweithredoedd neges" + "Cynllun y neges" + "Neges wedi\'i thynnu" + "Cyfoes" + "Tewi" + "%1$s (%2$s)" + "Dim canlyniadau" + "Dim enw i\'r ystafell" + "Heb ei amgryptio" + "All-lein" + "Trwyddedau cod agored" + "neu" + "Cyfrinair" + "Pobl" + "Dolen Barhaol" + "Caniatâd" + "Piniwyd" + "Gwiriwch eich cysylltiad rhyngrwyd" + "Arhoswch…" + "Ydych chi\'n siŵr eich bod am ddod â\'r pôl hwn i ben?" + "Pleidlais: %1$s" + "Cyfanswm y pleidleisiau: %1$s" + "Bydd y canlyniadau\'n dangos ar ôl i\'r bleidlais ddod i ben" + + "%d pleidleisiau" + "%d pleidlais" + "%d pleidlais" + "%d pleidlais" + "%d pleidlais" + "%d pleidlais" + + "Polisi preifatrwydd" + "Adwaith" + "Adweithiau" + "Rheswm" + "Allwedd adfer" + "Wrthi\'n adnewyddu…" + "Yn ymateb i %1$s" + "Adrodd ar wall" + "Adrodd am broblem" + "Adroddiad wedi ei gyflwyno" + "Golygydd testun cyfoethog" + "Ystafell" + "Enw\'r ystafell" + "e.e. enw eich project" + "Newidiadau wedi\'u cadw" + "Cadw" + "Clo sgrin" + "Chwilio am rywun" + "Canlyniadau chwilio" + "Diogelwch" + "Wedi\'i weld gan" + "Anfon at" + "Yn anfon…" + "Methodd anfon" + "Anfonwyd" + ". " + "Nid yw\'r gweinydd yn cael ei gynnal" + "URL gweinydd" + "Gosodiadau" + "Lleoliad yn cael ei rannu" + "Allgofnodi" + "Aeth rhywbeth o\'i le" + "Wedi canfod mater. Ceisiwch eto." + "Dechrau sgwrs…" + "Sticer" + "Llwyddiant" + "Awgrymiadau" + "Cydweddu" + "System" + "Testun" + "Hysbysiadau trydydd parti" + "Edefyn" + "Pwnc" + "Am beth mae\'r ystafell hon?" + "Methu dadgryptio" + "Wedi\'i anfon o ddyfais anniogel" + "Does gennych chi ddim mynediad i\'r neges hon" + "Cafodd hunaniaeth yr anfonwr ei hailosod" + "Doedd dim modd anfon gwahoddiadau at un neu fwy o ddefnyddwyr." + "Methu anfon gwahoddiad(au)" + "Datgloi" + "Dad-dewi" + "Galwad heb ei chefnogi" + "Digwyddiad heb ei gefnogi" + "Enw defnyddiwr" + "Dilysiad wedi\'i ddiddymu" + "Gwiriad wedi\'i gwblhau" + "Methodd y gwirio" + "Wedi ei wirio" + "Gwirio dyfais" + "Gwirio hunaniaeth" + "Gwirio defnyddiwr" + "Fideo" + "Neges llais" + "Yn aros…" + "Yn aros am y neges hon" + "Chi" + "Cafodd hunaniaeth %1$s ei ailosod. %2$s" + "Cafodd hunaniaeth %2$s %1$s ei ailosod. %3$s" + "(%1$s)" + "Cafodd hunaniaeth %1$s ei ailosod." + "Cafodd hunaniaeth %2$s %1$s ei ailosod. %3$s" + "Tynnu\'r gwiriad yn ôl" + "Mae\'r ddolen %1$s yn mynd â chi i wefan arall %2$s + +Ydych chi\'n siŵr eich bod am barhau?" + "Gwnewch yn siŵr fod y ddolen hon yn iawn" + "Adroddwyd am yr ystafell" + "Adroddwyd a gadael yr ystafell" + "Cadarnhad" + "Gwall" + "Llwyddiant" + "Rhybudd" + "Dyw eich newidiadau heb gael eu cadw. Ydych chi\'n siŵr eich bod am fynd nôl?" + "Cadw\'r newidiadau?" + "Mae angen uwchraddio eich gweinydd cartref i gefnogi Gwasanaeth Dilysu Matrix a chreu cyfrif." + "Wedi methu creu\'r ddolen barhaol" + "Methodd %1$s â llwytho\'r map. Ceisiwch eto yn nes ymlaen." + "Wedi methu llwytho negeseuon" + "Doedd dim modd i %1$s gael mynediad i\'ch lleoliad. Ceisiwch eto yn nes ymlaen." + "Wedi methu llwytho eich neges llais." + "Nid yw\'r ystafell yn bodoli mwyach neu nid yw\'r gwahoddiad yn ddilys mwyach." + "Neges heb ei chanfod" + "Does gan %1$s ddim caniatâd i gael mynediad i\'ch lleoliad. Gallwch alluogi mynediad yn y Gosodiadau." + "Does gan %1$s ddim caniatâd i gael mynediad i\'ch lleoliad. Galluogwch fynediad isod." + "Does gan %1$s ddim caniatâd i gael mynediad i\'ch meicroffon. Galluogwch fynediad i recordio neges llais." + "Gall hyn fod oherwydd problemau rhwydwaith neu weinydd." + "Mae\'r cyfeiriad ystafell hwn yn bodoli eisoes. Ceisiwch olygu\'r maes cyfeiriad ystafell neu newid enw\'r ystafell" + "Dyw rhai nodau ddim yn cael eu caniatáu. Dim ond llythrennau, digidau a\'r symbolau canlynol sy\'n cael eu cefnogi ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" + "Nid yw rhai negeseuon wedi\'u hanfon" + "Ymddiheuriadau, mae gwall wedi digwydd" + "Nid yw anfonwr y digwyddiad yn cyfateb i berchennog y ddyfais a\'i hanfonodd." + "Nid oes modd gwarantu dilysrwydd y neges hon sydd wedi\'i hamgryptio ar y ddyfais hon." + "Wedi\'i amgryptio gan ddefnyddiwr a wiriwyd gynt." + "Heb ei amgryptio." + "Wedi\'i amgryptio gan ddyfais anhysbys neu wedi\'i dileu." + "Wedi\'i amgryptio gan ddyfais nad yw wedi\'i wirio gan ei pherchennog." + "Wedi\'i amgryptio gan ddefnyddiwr heb ei wirio." + "🔐️ Ymunwch â mi ar %1$s" + "Hei, siaradwch â mi ar %1$s: %2$s" + "Android %1$s" + "Rageshake i adrodd gwall" + "%1$s: %2$s" + "Dewisiadau" + "Tynnu %1$s" + "Gosodiadau" + "Wedi methu dewis cyfrwng, ceisiwch eto." + "Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn." + "Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto." + "Wedi methu llwytho cyfryngau, ceisiwch eto." + "Pwyswch ar neges a dewis “%1$s” i\'w cynnwys yma." + "Pinio negeseuon pwysig fel y mae modd eu darganfod yn hawdd" + + "%1$d Negeseuon wedi\'u pinio" + "%1$d Neges wedi\'i phinio" + "%1$d Neges wedi\'i phinio" + "%1$d Neges wedi\'i phinio" + "%1$d Neges wedi\'i phinio" + "%1$d Neges wedi\'i phinio" + + "Negeseuon wedi\'u pinio" + "Rydych chi ar fin mynd i\'ch cyfrif %1$s i ailosod eich hunaniaeth. Wedi hynny byddwch yn cael eich tywys yn ôl i\'r ap." + "Methu cadarnhau? Ewch i\'ch cyfrif i ailosod eich hunaniaeth." + "Tynnu\'r dilysiad yn ôl a\'i anfon" + "Gallwch dynnu\'ch dilysiad yn ôl ac anfon y neges hon beth bynnag, neu gallwch ei diddymu am y tro a rhoi cynnig arall arni yn nes ymlaen ar ôl dilysu %1$s." + "Nid yw eich neges wedi\'i hanfon oherwydd ailosodwyd hunaniaeth ddilys %1$s" + "Anfonwch neges beth bynnag" + "Mae %1$s yn defnyddio un neu fwy o ddyfeisiau heb eu gwirio. Gallwch anfon y neges beth bynnag, neu gallwch ei diddymu am y tro a cheisio eto yn nes ymlaen ar ôl i %2$s ddilysu ei holl ddyfeisiau." + "Dyw eich neges heb ei hanfon oherwydd nid yw %1$s wedi gwirio pob dyfais" + "Mae un neu fwy o\'ch dyfeisiau heb eu gwirio. Gallwch anfon y neges beth bynnag, neu gallwch ei diddymu am y tro a cheisio eto yn nes ymlaen ar ôl i chi ddilysu eich holl ddyfeisiau." + "Nid yw eich neges wedi\'i hanfon oherwydd nad ydych wedi gwirio un neu fwy o\'ch dyfeisiau" + "Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto." + "Methu â nôl manylion defnyddiwr" + "Neges yn %1$s" + "Dangos" + "Lleihau" + "Eisoes yn gweld yr ystafell hon!" + "%1$s o %2$s" + "%1$s Neges wedi\'u pinio" + "Wrthi\'n llwytho neges…" + "Dangos y Cyfan" + "Sgwrs" + "Rhannu lleoliad" + "Rhannu fy lleoliad" + "Agor yn Apple Maps" + "Agor yn Google Maps" + "Agor yn OpenStreetMap" + "Rhannu\'r lleoliad hwn" + "Heb anfon y neges oherwydd bod hunaniaeth wedi \'i ddilysu %1$s wedi\'i ailosod." + "Heb anfon y neges oherwydd nid yw %1$s wedi gwirio pob dyfais." + "Heb anfon y neges oherwydd nad ydych wedi gwirio un neu fwy o\'ch dyfeisiau." + "Lleoliad" + "Fersiwn: %1$s (%2$s)" + "cy" + "cy" + "Nid yw negeseuon hanesyddol ar gael ar y ddyfais hon" + "Mae angen i chi wirio\'r ddyfais hon i gael mynediad at negeseuon hanesyddol" + "Does gennych chi ddim mynediad i\'r neges hon" + "Methu dadgryptio\'r neges" + "Cafodd y neges hon ei rhwystro naill ai oherwydd nad ydych wedi ddilysu\'ch dyfais neu oherwydd bod angen i\'r anfonwr ddilysu\'ch hunaniaeth chi." + diff --git a/libraries/ui-strings/src/main/res/values-da/translations.xml b/libraries/ui-strings/src/main/res/values-da/translations.xml new file mode 100644 index 00000000000..bad79e2d161 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-da/translations.xml @@ -0,0 +1,409 @@ + + + "Tilføj reaktion: %1$s" + "Avatar" + "Slet" + + "%1$d ciffer indtastet" + "%1$d cifre indtastet" + + "Skjul adgangskode" + "Deltag i opkald" + "Hop til bunden" + "Kun omtaler" + "Lyd slået fra" + "Side %1$d" + "Pausér" + "Talebesked, varighed: %1$s, aktuel position: %2$s" + "PIN-felt" + "Afspil" + "Afstemning" + "Afsluttet afstemning" + "Reager med%1$s" + "Reager med andre emojis" + "Læs af %1$s og %2$s" + + "Læst af %1$s og %2$d andre" + "Læst af %1$s og %2$d andre" + + "Læst af%1$s" + "Tryk for at vise alle" + "Fjern reaktion med%1$s" + "Fjern reaktion med %1$s" + "Send filer" + "Vis adgangskode" + "Start et opkald" + "Brugermenu" + "Se avatar" + "Se detaljer" + "Talebesked, varighed: %1$s" + "Optag talebesked." + "Stop optagelsen" + "Accepter" + "Tilføj billedtekst" + "Føj til tidslinje" + "Tilbage" + "Opkald" + "Anullér" + "Spring over ind til videre" + "Vælg billede" + "Ryd" + "Luk" + "Fuldfør verifikation" + "Bekræft" + "Bekræft adgangskode" + "Fortsæt" + "Kopiér" + "Kopiér billedtekst" + "Kopiér link" + "Kopier link til besked" + "Kopiér tekst" + "Opret" + "Opret et rum" + "Deaktiver" + "Deaktiver konto" + "Afvis" + "Afvis og blokér" + "Slet afstemning" + "Deaktiver" + "Kassér" + "Afvis" + "Færdig" + "Redigér" + "Rediger billedtekst" + "Redigér afstemning" + "Slå til" + "Afslut afstemning" + "Indtast PIN-kode" + "Har du glemt din adgangskode?" + "Videresend" + "Gå tilbage" + "Ignorér" + "Invitér" + "Invitér folk" + "Invitér folk til %1$s" + "Invitér folk til %1$s" + "Invitationer" + "Deltag" + "Få mere at vide" + "Forlad" + "Forlad samtalen" + "Forlad rum" + "Indlæs mere" + "Administrer konto" + "Administrer enheder" + "Besked" + "Næste" + "Nej" + "Ikke nu" + "OK" + "Indstillinger" + "Åbn med" + "Fastgør" + "Hurtigt svar" + "Citér" + "Reagér" + "Afvis" + "Fjern" + "Fjern billedtekst" + "Fjern besked" + "Svar" + "Svar i tråd" + "Anmeld" + "Anmeld fejl" + "Anmeld indhold" + "Anmeld samtale" + "Anmeld rummet" + "Nulstil" + "Nulstil identitet" + "Prøv igen" + "Prøv at dekryptere igen" + "Gem" + "Søg" + "Send" + "Send besked" + "Del" + "Del link" + "Vis" + "Log ind igen" + "Log ud" + "Log ud alligevel" + "Spring over" + "Start" + "Start samtale" + "Begynd verifikation" + "Tryk for at indlæse kort" + "Tag billede" + "Tryk for indstillinger" + "Prøv igen" + "Frigør" + "Se i tidslinjen" + "Se kilde" + "Ja" + "Ja, prøv igen" + "Din server understøtter nu en ny, hurtigere protokol. Log ud og log ind igen for at opgradere nu. Hvis du gør dette nu, vil du undgå en tvungen logout, når den gamle protokol bliver fjerne senere." + "Opgradering tilgængelig" + "Om" + "Politik for acceptabel brug" + "Tilføjelse af billedtekst" + "Avancerede indstillinger" + "et billede" + "Analyse-værktøj" + "Udseende" + "Lyd" + "Blokerede brugere" + "Bobler" + "Opkald startet" + "Backup af samtale" + "Kopieret til udklipsholder" + "Ophavsret" + "Opretter rum…" + "Anmodning annulleret" + "Forlod rummet" + "Invitationen blev afvist" + "Mørkt tema" + "Fejl under dekryptering" + "Indstillinger for udviklere" + "Enheds-ID" + "Direkte samtale" + "Vis ikke dette igen" + "Download mislykkedes" + "Downloader" + "(redigeret)" + "Redigering" + "Redigering af billedtekst" + "* %1$s %2$s" + "Tom fil" + "Kryptering" + "Kryptering aktiveret" + "Indtast din PIN-kode" + "Fejl" + "Der opstod en fejl, du modtager muligvis ikke meddelelser om nye meddelelser. Fejlfinding af meddelelser fra indstillingerne. + +Årsag: %1$s." + "Alle" + "Mislykkedes" + "Favorit" + "Favoritmarkeret" + "Fil" + "Fil slettet" + "Fil gemt" + "Fil gemt i Downloads" + "Videresend besked" + "Ofte brugt" + "GIF" + "Billede" + "Som svar på %1$s" + "Installer APK" + "Dette Matrix-ID kan ikke findes, så invitationen modtages muligvis ikke." + "Forlader rummet" + "Lyst tema" + "Linje kopieret til udklipsholder" + "Linket er kopieret til udklipsholderen" + "Indlæser…" + "Indlæser flere…" + + "%d anden" + "%d andre" + + + "%1$d medlem" + "%1$d medlemmer" + + "Besked" + "Beskedhandlinger" + "Layout på beskeder" + "Beskeden er fjernet" + "Moderne" + "Lydløs" + "%1$s (%2$s)" + "Ingen resultater" + "Intet rumnavn" + "Ikke krypteret" + "Offline" + "Open Source-licenser" + "eller" + "Adgangskode" + "Mennesker" + "Permalink" + "Tilladelse" + "Fastgjort" + "Tjek venligst din internetforbindelse" + "Vent venligst…" + "Er du sikker på, at du vil afslutte denne afstemning?" + "Afstemning: %1$s" + "Stemmer i alt: %1$s" + "Resultaterne vil blive vist, når afstemningen er afsluttet" + + "%d stemme" + "%d stemmer" + + "Privatlivspolitik" + "Reaktion" + "Reaktioner" + "Årsag" + "Gendannelsesnøgle" + "Opdaterer…" + + "%1$d svar" + + "Svarer til %1$s" + "Rapportér en fejl" + "Anmeld et problem" + "Anmeldelsen er indsendt" + "Rich text editor" + "Rum" + "Navn på rum" + "f.eks. navnet på dit projekt" + "Gemte ændringer" + "Gemmer" + "Skærmlås" + "Søg efter nogen" + "Søgeresultater" + "Sikkerhed" + "Set af" + "Send til" + "Sender…" + "Afsendelse mislykkedes" + "Sendt" + ". " + "Serveren er ikke understøttet" + "Server URL" + "Indstillinger" + "Delt placering" + "Logger ud" + "Noget gik galt" + "Vi stødte på et problem. Prøv venligst igen." + "Starter samtale…" + "Klistermærke" + "Succes" + "Forslag" + "Synkroniserer" + "System" + "Tekst" + "Tredjepartsmeddelelser" + "Tråd" + "Emne" + "Hvad handler det her rum om?" + "Ude af stand til at dekryptere" + "Sendt fra en usikker enhed" + "Du har ikke adgang til denne meddelelse" + "Afsenderens verificerede identitet blev nulstillet" + "Invitationer kunne ikke sendes til en eller flere brugere." + "Kan ikke sende invitation(er)" + "Lås op" + "Slå lyden til" + "Ikke-understøttet opkald" + "Ikke-understøttet begivenhed" + "Brugernavn" + "Bekræftelse annulleret" + "Bekræftelse fuldført" + "Verifikation mislykkedes" + "Verificeret" + "Bekræft enhed" + "Verificér identitet" + "Verificér bruger" + "Video" + "Talebesked" + "Venter…" + "Venter på denne besked" + "Dig" + "%1$ss identitet blev nulstillet. %2$s" + "%1$ss %2$s identitet blev nulstillet. %3$s" + "(%1$s)" + "%1$ss identitet blev nulstillet." + "%1$ss %2$s identitet blev nulstillet. %3$s" + "Tilbagetræk verifikation" + "Linket %1$s fører dig til et andet websted %2$s + +Er du sikker på, at du vil fortsætte?" + "Dobbelttjek dette link" + "Rummet er anmeldt" + "Anmeldte og forlod rummet" + "Bekræftelse" + "Fejl" + "Succes" + "Advarsel" + "Dine ændringer er ikke blevet gemt. Er du sikker på, at du vil gå tilbage?" + "Gem ændringer?" + "Din hjemmeserver skal opgraderes for at understøtte Matrix Authentication Service og kontooprettelse." + "Oprettelse af permalink mislykkedes" + "%1$s kunne ikke indlæse kortet. Prøv igen senere." + "Fejl under indlæsning af beskeder" + "%1$s kunne ikke få adgang til din placering. Prøv igen senere." + "Kunne ikke uploade din talebesked." + "Rummet findes ikke længere, eller invitationen er ikke længere gyldig." + "Meddelelsen blev ikke fundet" + "%1$s har ikke tilladelse til at få adgang til din placering. Du kan aktivere adgang i Indstillinger." + "%1$s har ikke tilladelse til at se din placering. Aktivér adgang nedenfor." + "%1$s har ikke tilladelse til at få adgang til din mikrofon. Aktivér adgang for at optage en stemmemeddelelse." + "Dette kan skyldes netværks- eller serverproblemer." + "Denne rumadresse er allerede taget. Prøv at redigere rummets adressefelt eller at skifte rummets navn" + "Nogle tegn er ikke tilladt. Kun bogstaver, cifre og følgende symboler understøttes! $ & \'() * +/; =? @ [] - . _" + "Nogle beskeder er ikke blevet sendt" + "Beklager, der opstod en fejl" + "Afsenderen af begivenheden matcher ikke ejeren af den enhed, der sendte den." + "Ægtheden af denne krypterede besked kan ikke garanteres på denne enhed." + "Krypteret af en tidligere verificeret bruger." + "Ikke krypteret." + "Krypteret af en ukendt eller slettet enhed." + "Krypteret af en enhed, der ikke er verificeret af sin ejer." + "Krypteret af en ikke-verificeret bruger." + "🔐️ Kom med mig til %1$s" + "Hej, lad os snakkes på %1$s: %2$s" + "%1$s Android" + "Ryst enheden i frustration for at anmelde en fejl" + "%1$s: %2$s" + "Valgmuligheder" + "Fjern %1$s" + "Indstillinger" + "Det lykkedes ikke at vælge medie. Prøv igen." + "Billedtekster er muligvis ikke synlige for personer, der bruger ældre apps." + "Det lykkedes ikke at behandle medier til upload. Prøv venligst igen." + "Upload af medier mislykkedes. Prøv igen." + "Tryk på en besked og vælg \"%1$s\" for at inkludere den her." + "Fastgør vigtige beskeder, så de let kan opdages" + + "%1$d Fastgjort besked" + "%1$d Fastgjorte beskeder" + + "Fastgjorte beskeder" + "Du er ved at gå til din %1$s konto for at nulstille din identitet. Derefter vil du blive ført tilbage til appen." + "Kan du ikke bekræfte? Gå til din konto for at nulstille din identitet." + "Træk verifikationen tilbage og send" + "Du kan trække din verifikation tilbage og sende denne meddelelse alligevel, eller du kan annullere for nu og prøve igen senere efter at have gen-verificeret. %1$s" + "Din besked blev ikke sendt, fordi %1$s\'s verificerede identitet er blevet nulstillet" + "Send besked alligevel" + "%1$s bruger en eller flere uverificerede enheder. Du kan sende beskeden alligevel, eller du kan annullere for nu og prøve igen senere, når %2$s har bekræftet alle deres enheder." + "Din besked blev ikke sendt, fordi %1$s ikke har bekræftet alle enheder" + "En eller flere af dine enheder er ikke verificeret. Du kan sende beskeden alligevel, eller du kan annullere for nu og prøve igen senere, når du har verificeret alle dine enheder." + "Din besked blev ikke sendt, fordi du ikke har verificeret en eller flere af dine enheder" + "Det lykkedes ikke at behandle medier til upload. Prøv venligst igen." + "Kunne ikke hente brugeroplysninger" + "Besked i %1$s" + "Udvid" + "Reducér" + "Du ser allerede dette rum!" + "%1$s af %2$s" + "%1$s Fastgjorte beskeder" + "Indlæser besked…" + "Se alle" + "Samtale" + "Del lokation" + "Del min lokation" + "Åbn i Apple Maps" + "Åbn i Google Maps" + "Åbn i OpenStreetMap" + "Del denne lokation" + "Beskeden blev ikke sendt fordi %1$s s bekræftede identitet blev nulstillet." + "Meddelelsen er ikke sendt, fordi %1$s ikke har bekræftet alle enheder." + "Beskeden er ikke sendt, fordi du ikke har verificeret en eller flere af dine enheder." + "Lokation" + "Version: %1$s (%2$s)" + "da" + "Historiske beskeder er ikke tilgængelige på denne enhed" + "Du skal verificere denne enhed for at få adgang til historiske beskeder" + "Du har ikke adgang til denne besked" + "Kan ikke dekryptere beskeden" + "Denne besked blev blokeret, enten fordi du ikke verificerede din enhed, eller fordi afsenderen skal have verificeret din identitet." + 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 d9c55b2819e..c49a3ceaef1 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -1,16 +1,21 @@ + "Reaktion hinzufügen: %1$s" + "Avatar" "Löschen" "%1$d eingegebene Ziffer" "%1$d eingegebene Ziffern" "Passwort verbergen" + "Anruf beitreten" "Nach unten springen" "Nur Erwähnungen" "Stummgeschaltet" + "Avatar des anderen Nutzers" "Seite %1$d" "Pausieren" + "Sprachnachricht, Dauer:%1$s, aktuelle Position: %2$s" "PIN-Feld" "Abspielen" "Umfrage" @@ -24,21 +29,27 @@ "Gelesen von %1$s" "Tippe, um alle anzuzeigen" - "Reaktion mit %1$s entfernen" + "Reaktion mit %1$s entfernen" + "Entferne Reaktionen mit %1$s" + "Raum-Avatar" "Dateien senden" "Passwort anzeigen" "Anruf starten" + "Benutzer-Avatar" "Benutzermenü" + "Avatar ansehen" "Details anzeigen" + "Sprachnachricht, Dauer: %1$s" "Sprachnachricht aufnehmen." "Aufnahme beenden" + "Dein Avatar" "Akzeptieren" "Bildunterschrift hinzufügen" "Zum Nachrichtenverlauf hinzufügen" "Zurück" "Anruf" "Abbrechen" - "Jetzt abbrechen" + "Vorerst abbrechen" "Foto auswählen" "Löschen" "Schließen" @@ -54,11 +65,13 @@ "Erstellen" "Raum erstellen" "Deaktivieren" - "Benutzerkonto deaktivieren" + "Nutzerkonto deaktivieren" "Ablehnen" + "Ablehnen und blockieren" "Umfrage löschen" "Deaktivieren" "Verwerfen" + "Schließen" "Erledigt" "Bearbeiten" "Bildunterschrift bearbeiten" @@ -71,8 +84,8 @@ "Zurück" "Ignorieren" "Einladen" - "Personen einladen" - "Zu %1$s einladen" + "Nutzer einladen" + "Lade Personen in %1$s ein" "Lade Personen in %1$s ein" "Einladungen" "Beitreten" @@ -80,7 +93,7 @@ "Verlassen" "Unterhaltung verlassen" "Verlassen" - "Mehr laden …" + "Mehr laden…" "Konto verwalten" "Geräte verwalten" "Nachricht" @@ -88,6 +101,7 @@ "Nein" "Später" "Ok" + "Kontextmenü öffnen" "Einstellungen öffnen" "Öffnen mit" "Fixieren" @@ -100,8 +114,11 @@ "Nachricht löschen" "Antworten" "Im Thread antworten" + "Bericht" "Fehler melden" "Inhalt melden" + "Konversation melden" + "Chatroom melden" "Zurücksetzen" "Identität zurücksetzen" "Erneut versuchen" @@ -109,7 +126,9 @@ "Speichern" "Suchen" "Senden" + "Bearbeitete Nachricht senden" "Nachricht senden" + "Sprachnachricht senden" "Teilen" "Link teilen" "Zeige" @@ -125,25 +144,31 @@ "Für Optionen tippen" "Erneut versuchen" "Lösen" - "Im Chatverlauf anzeigen" + "Ansicht" + "Im Nachrichtenverlauf anzeigen" "Quellcode anzeigen" "Ja" "Ja, versuchen Sie es noch einmal" + "Ihr Server unterstützt jetzt ein neues, schnelleres Protokoll. Melden Sie sich ab und wieder an, um die Verbindung zu aktualisieren. Wenn Sie das jetzt tun, vermeiden Sie eine erzwungene Abmeldung, wenn das alte Protokoll später entfernt wird." + "Aktualisierung verfügbar" "Über" "Nutzungsrichtlinie" "Hinzufügen einer Bildunterschrift" "Erweiterte Einstellungen" + "ein Bild" "Analysedaten" "Erscheinungsbild" "Audio" "Blockierte Nutzer" "Sprechblasen" - "Aufruf gestartet" + "Anruf gestartet" "Chat-Backup" "In die Zwischenablage kopiert" "Copyright" "Raum wird erstellt…" + "Anfrage storniert" "Hat den Raum verlassen" + "Einladung abgelehnt" "Dunkel" "Dekodierungsfehler" "Entwickleroptionen" @@ -154,13 +179,14 @@ "Herunterladen" "(bearbeitet)" "Bearbeitung" - "Bearbeitung der Bildunterschrift" + "Bildunterschrift bearbeiten" "* %1$s %2$s" + "Leere Datei" "Verschlüsselung" "Verschlüsselung aktiviert" "PIN eingeben" "Fehler" - "Es ist ein Fehler aufgetreten, du erhältst möglicherweise keine Benachrichtigungen für neue Nachrichten. Bitte behebe das Problem mit den Benachrichtigungen in den Einstellungen. + "Es ist ein Fehler aufgetreten. Sie erhalten eventuell keine Benachrichtigungen für neue Nachrichten. Bitte diagnostizieren Sie das Benachrichtigungsproblem in den Einstellungen. Grund: %1$s." "Alle" @@ -177,12 +203,17 @@ Grund: %1$s." "Bild" "Als Antwort auf %1$s" "APK installieren" - "Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen." + "Diese Matrix Kennung wurde nicht gefunden, daher wird die Einladung möglicherweise nicht empfangen." "Raum verlassen" "Hell" + "Zeile in die Zwischenablage kopiert" "Link in die Zwischenablage kopiert" "Laden…" "Mehr wird geladen…" + + "%d weitere" + "%d weitere" + "%1$d Mitglied" "%1$d Mitglieder" @@ -193,8 +224,10 @@ Grund: %1$s." "Nachricht entfernt" "Modern" "Stumm" + "%1$s(%2$s)" "Keine Ergebnisse" "Kein Raumname" + "Nicht verschlüsselt" "Offline" "Open-Source-Lizenzen" "oder" @@ -203,11 +236,12 @@ Grund: %1$s." "Permalink" "Erlaubnis" "Fixiert" - "Bitte warten …" - "Bist du sicher, dass du diese Umfrage beenden möchtest?" + "Bitte überprüfen Sie Ihre Internetverbindung" + "Bitte warten…" + "Sind Sie sicher, dass Sie diese Umfrage beenden möchten?" "Umfrage: %1$s" "Stimmen insgesamt: %1$s" - "Die Ergebnisse werden nach Ende der Umfrage angezeigt" + "Ergebnisse werden nach Ende der Umfrage angezeigt" "%d Stimme" "%d Stimmen" @@ -215,8 +249,13 @@ Grund: %1$s." "Datenschutz­erklärung" "Reaktion" "Reaktionen" + "Grund" "Wiederherstellungsschlüssel" "Wird erneuert…" + + "%1$d Antwort" + "%1$d Antworten" + "%1$s antworten" "Einen Fehler melden" "Ein Problem melden" @@ -236,12 +275,14 @@ Grund: %1$s." "Wird gesendet…" "Senden fehlgeschlagen" "Gesendet" + ". " "Server wird nicht unterstützt" "Server-URL" "Einstellungen" "Geteilter Standort" "Abmelden" "Es ist ein Fehler aufgetreten." + "Wir sind auf ein Problem gestoßen. Bitte versuchen Sie es erneut." "Chat wird gestartet…" "Sticker" "Erfolg" @@ -255,7 +296,7 @@ Grund: %1$s." "Worum geht es in diesem Raum?" "Entschlüsselung nicht möglich" "Von einem ungesicherten Gerät gesendet" - "Du hast kein Recht diese Nachricht zu lesen." + "Sie haben keinen Zugriff auf diese Nachricht" "Die verifizierte Identität des Senders hat sich geändert" "Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden." "Einladung(en) konnte(n) nicht gesendet werden" @@ -270,21 +311,29 @@ Grund: %1$s." "Verifiziert" "Gerät verifizieren" "Identität überprüfen" + "Benutzer verifizieren" "Video" "Sprachnachricht" "Warten…" "Warte auf diese Nachricht" "Sie" - "%1$s\'s Identität has sich offenbar geändert. %2$s" - "%1$s\'s %2$s Identität hat sich offenbar geändert. %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 verifizierte Identität von %1$s\'s %2$s hat sich geändert. %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" + "Der Link%1$s führt Sie zu einer anderen Seite%2$s. + +Möchten Sie wirklich fortfahren?" + "Überprüfen Sie diesen Link noch einmal" + "Chatroom gemeldet" + "Gemeldet und Zimmer verlassen" "Bestätigung" "Fehler" "Erfolg" "Warnung" - "Deine Änderungen wurden nicht gespeichert. Bist du sicher, dass du zurückgehen willst?" + "Ihre Änderungen wurden nicht gespeichert. Sind Sie sicher, dass Sie zurückgehen wollen?" "Änderungen speichern?" "Dein Homeserver muss aktualisiert werden, um den Matrix Authentication Services und die Erstellung von Konten zu unterstützen." "Fehler beim Erstellen des Permalinks" @@ -292,14 +341,17 @@ Grund: %1$s." "Fehler beim Laden der Nachrichten" "%1$s konnte nicht auf deinen Standort zugreifen. Bitte versuche es später erneut." "Fehler beim Hochladen der Sprachnachricht." + "Der Raum existiert nicht mehr oder die Einladung ist nicht mehr gültig." "Nachricht nicht gefunden" "%1$s hat keine Erlaubnis, auf deinen Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren." "%1$s hat keine Erlaubnis, auf deinen Standort zuzugreifen. Aktiviere unten den Zugriff." "%1$s hat nicht die Erlaubnis auf dein Mikrofon zuzugreifen. Aktiviere den Zugriff, um eine Sprachnachricht aufzunehmen." + "Dies kann auf Netzwerk- oder Serverprobleme zurückzuführen sein." "Diese Chatroomadresse existiert bereits. Bitte versuchen Sie, das Adressenfeld des Chatrooms zu bearbeiten oder den Namen des Chatrooms zu ändern" "Einige Zeichen sind nicht erlaubt. Es werden nur Buchstaben, Ziffern und die folgenden Symbole unterstützt: ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Einige Nachrichten wurden nicht gesendet" "Entschuldigung, es ist ein Fehler aufgetreten" + "Der Absender des Ereignisses stimmt nicht mit dem Besitzer des Gerätes überein, das es gesendet hat." "Die Authentizität dieser verschlüsselten Nachricht kann auf diesem Gerät nicht garantiert werden." "Verschlüsselt von einem zuvor verifizierten Nutzer." "Unverschlüsselt." @@ -310,6 +362,10 @@ Grund: %1$s." "Hey, sprich mit mir auf %1$s: %2$s" "%1$s Android" "Schüttel heftig zum Melden von Fehlern" + "%1$s: %2$s" + "Optionen" + "Entferne %1$s" + "Einstellungen" "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar." "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." @@ -321,24 +377,27 @@ Grund: %1$s." "%1$d fixierte Nachrichten" "Fixierte Nachrichten" - "Du wirst jetzt zu deinem %1$s Konto geleitet, um deine Identität zurückzusetzen. Danach wirst du zur App zurückgebracht." - "Kannst du das nicht bestätigen? Gehe zu deinem Konto, um deine Identität zurückzusetzen." + "Sie werden jetzt zu Ihrem %1$s Konto geleitet, um Ihre Identität zurückzusetzen. Danach werden Sie zur App zurückgebracht." + "Bestätigung unmöglich? Gehen Sie zu Ihrem Konto, um Ihre Identität zurückzusetzen." "Verifizierung zurückziehen und senden" - "Du kannst deine Verifizierung zurückziehen und diese Nachricht trotzdem senden, oder du kannst sie vorerst abbrechen und es später noch einmal versuchen, nachdem du %1$s erneut verifiziert hast." - "Deine Nachricht wurde nicht gesendet, da sich die verifizierte Identität von %1$s geändert hat" + "Sie können Ihre Verifizierung widerrufen und diese Nachricht trotzdem senden. Oder Sie brechen vorerst ab und versuchen es noch einmal, nachdem Sie %1$s erneut verifiziert haben." + "Ihre Nachricht wurde nicht gesendet, da die verifizierte Identität von %1$s zurückgesetzt wurde" "Nachricht trotzdem senden" - "%1$s verwendet wenigstens ein nicht verifiziertes Gerät. Du kannst die Nachricht trotzdem verschicken, oder vorerst abbrechen und später erneut versuchen, nachdem %2$s alle Geräte verifiziert hat." + "%1$s verwendet mindestens ein nicht verifiziertes Gerät. Sie können die Nachricht trotzdem verschicken, oder Sie können vorerst abbrechen und es später erneut versuchen, nachdem %2$s alle Geräte verifiziert hat." "Deine Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat" "Mindestens eines Ihrer Geräte ist nicht verifiziert worden. Sie können die Nachricht trotzdem senden, oder den Vorgang zunächst abbrechen und es später erneut versuchen, nachdem Sie alle Ihrer Geräte verifiziert haben." "Ihre Nachricht wurde nicht geschickt, da Sie eines oder mehrere Ihrer Geräte nicht verifiziert haben." "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Benutzerdetails konnten nicht abgerufen werden" + "Nachricht in %1$s" + "Erweitern" + "Verkleinern" + "Du siehst diesen Raum bereits!" "%1$s von %2$s" "%1$s fixierte Nachrichten" "Nachricht wird geladen…" "Alle anzeigen" "Chat" - "Beitrittsanfrage gesendet" "Standort teilen" "Meinen Standort teilen" "In Apple Maps öffnen" @@ -346,13 +405,13 @@ Grund: %1$s." "In OpenStreetMap öffnen" "Diesen Standort teilen" "Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat." - "Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat" + "Die Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat." "Die Nachricht wurde nicht gesendet, weil Sie eines oder mehrere Ihrer Geräte nicht verifiziert haben." "Standort" "Version: %1$s (%2$s)" "en" "Der Nachrichtenverlauf ist auf diesem Gerät nicht verfügbar" - "Sie müssen dieses Gerät verifizieren, um auf den Nachrichtenverlauf zugreifen zu können" + "Für den Zugriff auf den kompletten Nachrichtenverlauf müssen Sie dieses Gerät verifizieren" "Sie haben keinen Zugriff auf diese Nachricht" "Nachricht kann nicht entschlüsselt werden" "Diese Nachricht wurde entweder blockiert, weil Ihr Gerät nicht verifiziert ist oder weil der Absender Ihre Identität überprüfen 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 61e583fc1c7..a9b1e15f7a8 100644 --- a/libraries/ui-strings/src/main/res/values-el/translations.xml +++ b/libraries/ui-strings/src/main/res/values-el/translations.xml @@ -1,16 +1,20 @@ + "Προσθήκη αντίδρασης: %1$s" + "Εικόνα Προφίλ" "Διαγραφή" "%1$d ψηφίο εισήχθη" "%1$d ψηφία εισήχθησαν" "Απόκρυψη κωδικού πρόσβασης" + "Συμμετοχή στην κλήση" "Άλμα προς τα κάτω" "Αναφορές μόνο" "Σε σίγαση" "Σελίδα %1$d" "Παύση" + "Φωνητικό μήνυμα, διάρκεια:%1$s, τρέχουσα θέση: %2$s" "Πεδίο PIN" "Αναπαραγωγή" "Δημοσκόπηση" @@ -24,11 +28,14 @@ "Διαβάστηκε από %1$s" "Πάτα για εμφάνιση όλων" - "Αφαίρεση αντίδρασης με %1$s" + "Αφαίρεση αντίδρασης με %1$s" + "Αφαιρέστε την αντίδραση με %1$s" "Αποστολή αρχείων" "Εμφάνιση κωδικού πρόσβασης" "Ξεκίνησε μια κλήση" "Μενού χρήστη" + "Προβολή λεπτομερειών" + "Φωνητικό μήνυμα, διάρκεια: %1$s" "Εγγραφή φωνητικού μηνύματος." "Διακοπή καταγραφής" "Αποδοχή" @@ -51,13 +58,15 @@ "Αντιγραφή συνδέσμου στο μήνυμα" "Αντιγραφή κειμένου" "Δημιουργία" - "Δημιούργησε ένα δωμάτιο" + "Δημιουργία αίθουσας" "Απενεργοποίηση" "Απενεργοποίηση λογαριασμού" "Απόρριψη" + "Απόρριψη και αποκλεισμός" "Διαγραφή Δημοσκόπησης" "Απενεργοποίηση" "Απόρριψη" + "Παράβλεψη" "Έγινε" "Επεξεργασία" "Επεξεργασία λεζάντας" @@ -72,13 +81,13 @@ "Πρόσκληση" "Πρόσκληση ατόμων" "Πρόσκληση ατόμων στο %1$s" - "Προσκάλεσε χρήστες στο %1$s" + "Πρόσκληση ατόμων στο %1$s" "Προσκλήσεις" "Συμμετοχή" "Μάθε περισσότερα" "Αποχώρηση" "Αποχώρηση από τη συζήτηση" - "Αποχώρηση από το δωμάτιο" + "Αποχώρηση από την αίθουσα" "Φόρτωσε περισσότερα" "Διαχείριση λογαριασμού" "Διαχείριση συσκευών" @@ -99,8 +108,11 @@ "Αφαίρεση μηνύματος" "Απάντηση" "Απάντηση στο θέμα" + "Αναφορά" "Αναφορά σφάλματος" "Αναφορά περιεχομένου" + "Αναφορά συνομιλίας" + "Αναφορά αίθουσας" "Επαναφορά" "Επαναφορά ταυτότητας" "Επανάληψη" @@ -127,10 +139,14 @@ "Προβολή στο χρονοδιάγραμμα" "Προβολή πηγής" "Ναι" + "Ναι, δοκιμή ξανά" + "Ο διακομιστής σου υποστηρίζει τώρα ένα νέο, ταχύτερο πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για αναβάθμιση τώρα. Κάνοντας αυτό τώρα θα σε βοηθήσει να αποφύγεις μια αναγκαστική αποσύνδεση όταν το παλιό πρωτόκολλο καταργηθεί αργότερα." + "Διαθέσιμη αναβάθμιση" "Σχετικά" "Πολιτική αποδεκτής χρήσης" "Η λεζάντα προστίθεται" "Ρυθμίσεις για προχωρημένους" + "μια εικόνα" "Στατιστικά στοιχεία" "Εμφάνιση" "Ήχος" @@ -140,19 +156,23 @@ "Αντίγραφο ασφαλείας συνομιλίας" "Αντιγράφηκε στο πρόχειρο" "Πνευματικά δικαιώματα" - "Δημιουργία δωματίου…" - "Αποχώρησε από το δωμάτιο" + "Δημιουργία αίθουσας…" + "Το αίτημα ακυρώθηκε" + "Αποχώρησε από την αίθουσα" + "Η πρόσκληση απορρίφθηκε" "Σκοτεινό" "Σφάλμα αποκρυπτογράφησης" "Επιλογές προγραμματιστή" "ID συσκευής" "Άμεση συνομιλία" "Να μην εμφανιστεί ξανά" + "Η λήψη απέτυχε" "Γίνεται λήψη" "(επεξεργάστηκε)" "Επεξεργάζεται" "Η λεζάντα επεξεργάζεται" "* %1$s %2$s" + "Κενό αρχείο" "Κρυπτογράφηση" "Η κρυπτογράφηση ενεργοποιήθηκε" "Εισήγαγε το PIN σου" @@ -165,6 +185,8 @@ "Αγαπημένο" "Είναι αγαπημένο" "Αρχείο" + "Το αρχείο διαγράφηκε" + "Το αρχείο αποθηκεύτηκε" "Αρχείο αποθηκευμένο σε Λήψεις" "Προώθηση μηνύματος" "Χρησιμοποιείται συχνά" @@ -173,10 +195,16 @@ "Σε απάντηση στον χρήστη %1$s" "Εγκατάσταση APK" "Αυτό το Matrix ID δεν μπορεί να βρεθεί, επομένως η πρόσκληση ενδέχεται να μην ληφθεί." - "Αποχώρηση από το δωμάτιο" + "Αποχώρηση από την αίθουσα" "Φωτεινό" + "Η γραμμή αντιγράφηκε στο πρόχειρο" "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο" "Φόρτωση…" + "Φόρτωση περισσότερων…" + + "%d ακόμη" + "%d ακόμη" + "%1$d μέλος" "%1$d μέλη" @@ -187,8 +215,10 @@ "Το μήνυμα αφαιρέθηκε" "Σύγχρονη" "Σίγαση" + "%1$s (%2$s)" "Κανένα αποτέλεσμα" - "Χωρίς όνομα δωματίου" + "Δεν υπάρχει όνομα αίθουσας" + "Χωρίς κρυπτογράφηση" "Εκτός σύνδεσης" "Άδειες ανοιχτού κώδικα" "ή" @@ -197,6 +227,7 @@ "Μόνιμος σύνδεσμος" "Αδεια" "Καρφιτσωμένο" + "Παρακαλώ ελέγξτε τη σύνδεσή σας στο διαδίκτυο" "Παρακαλώ περίμενε…" "Θες σίγουρα να τερματίσεις αυτή τη δημοσκόπηση;" "Δημοσκόπηση: %1$s" @@ -209,15 +240,20 @@ "Πολιτική απορρήτου" "Αντίδραση" "Αντιδράσεις" + "Αιτιολογία" "Κλειδί ανάκτησης" "Ανανέωση…" + + "%1$d απάντηση" + "%1$d απαντήσεις" + "Απάντηση σε %1$s" "Αναφορά σφάλματος" "Αναφορά προβλήματος" "Η αναφορά υποβλήθηκε" "Επεξεργαστής εμπλουτισμένου κειμένου" - "Δωμάτιο" - "Όνομα δωματίου" + "Αίθουσα" + "Όνομα αίθουσας" "πχ. το όνομα του έργου σου" "Αποθηκευμένες αλλαγές" "Αποθηκεύεται" @@ -230,12 +266,14 @@ "Αποστολή…" "Αποτυχία αποστολής" "Εστάλη" + ". " "Ο διακομιστής δεν υποστηρίζεται" "URL διακομιστή" "Ρυθμίσεις" "Κοινόχρηστη τοποθεσία" "Αποσύνδεση" "Κάτι πήγε στραβά" + "Αντιμετωπίσαμε ένα πρόβλημα. Παρακαλώ προσπαθήστε ξανά." "Έναρξη συνομιλίας…" "Αυτοκόλλητο" "Επιτυχία" @@ -246,11 +284,11 @@ "Ειδοποιήσεις τρίτων" "Νήμα" "Θέμα" - "Τί αφορά το δωμάτιο;" + "Τι αφορά αυτή η αίθουσα;" "Δεν είναι δυνατή η αποκρυπτογράφηση" "Στάλθηκε από μια μη ασφαλής συσκευή" "Δεν έχεις πρόσβαση σε αυτό το μήνυμα" - "Η επαληθευμένη ταυτότητα του αποστολέα έχει αλλάξει" + "Η επαληθευμένη ταυτότητα του αποστολέα έχει επαναφερθεί" "Δεν ήταν δυνατή η αποστολή προσκλήσεων σε έναν ή περισσότερους χρήστες." "Δεν είναι δυνατή η αποστολή προσκλήσεων" "Ξεκλείδωμα" @@ -264,14 +302,24 @@ "Επαληθεύτηκε" "Επαλήθευση συσκευής" "Επαλήθευση ταυτότητας" + "Επαλήθευση χρήστη" "Βίντεο" "Φωνητικό μήνυμα" "Αναμονή…" "Αναμονή για αυτό το μήνυμα" "Εσύ" - "Η ταυτότητα του χρήστη %1$s φαίνεται να έχει αλλάξει. %2$s" - "Η ταυτότητα του %1$s %2$s φαίνεται να έχει αλλάξει. %3$s" + "Η ταυτότητα του χρήστη %1$s επαναφέρθηκε. %2$s" + "Η ταυτότητα του %1$s %2$s επαναφέρθηκε. %3$s" "(%1$s)" + "Η ταυτότητα του χρήστη %1$s επαναφέρθηκε." + "Η ταυτότητα του χρήστη %1$s %2$s επαναφέρθηκε. %3$s" + "Ανάκληση επαλήθευσης" + "Ο σύνδεσμος %1$s σας μεταφέρει σε άλλο ιστότοπο %2$s + +Είστε βέβαιοι ότι θέλετε να συνεχίσετε;" + "Ελέγξτε ξανά αυτόν τον σύνδεσμο" + "Η αίθουσα αναφέρθηκε" + "Αναφέρθηκε και αποχωρήσατε από την αίθουσα" "Επιβεβαίωση" "Σφάλμα" "Επιτυχία" @@ -284,14 +332,17 @@ "Αποτυχία φόρτωσης μηνυμάτων" "Το %1$s δεν μπόρεσε να αποκτήσει πρόσβαση στην τοποθεσία σου. Προσπάθησε ξανά αργότερα." "Αποτυχία μεταφόρτωσης του φωνητικού σου μηνύματος." + "Η αίθουσα δεν υπάρχει πλέον ή η πρόσκληση δεν ισχύει πλέον." "Το μήνυμα δεν βρέθηκε" "Το %1$s δεν έχει άδεια πρόσβασης στην τοποθεσία σου. Μπορείς να ενεργοποιήσεις την πρόσβαση στις Ρυθμίσεις." "Ο χρήστης %1$s δεν έχει άδεια πρόσβασης στην τοποθεσία σου. Ενεργοποίησε την πρόσβαση παρακάτω." "Το %1$s δεν έχει άδεια πρόσβασης στο μικρόφωνό σου. Ενεργοποίησε την πρόσβαση για εγγραφή φωνητικού μηνύματος." - "Αυτή η διεύθυνση δωματίου υπάρχει ήδη, δοκίμασε να επεξεργαστείς το πεδίο διεύθυνσης δωματίου ή να αλλάξεις το όνομα δωματίου" + "Αυτό μπορεί να οφείλεται σε προβλήματα δικτύου ή διακομιστή." + "Αυτή η διεύθυνση αίθουσας υπάρχει ήδη. Παρακαλώ δοκιμάστε να επεξεργαστείτε το πεδίο διεύθυνσης αίθουσας ή αλλάξτε το όνομα της αίθουσας" "Ορισμένοι χαρακτήρες δεν επιτρέπονται. Υποστηρίζονται μόνο γράμματα, ψηφία και τα ακόλουθα σύμβολα ! $ & \'() * +/; = ? @ [] - . _" "Ορισμένα μηνύματα δεν έχουν σταλεί" "Λυπούμαστε, παρουσιάστηκε σφάλμα" + "Ο αποστολέας του συμβάντος δεν ταιριάζει με τον κάτοχο της συσκευής που το έστειλε." "Η αυθεντικότητα αυτού του κρυπτογραφημένου μηνύματος δεν είναι εγγυημένη σε αυτήν τη συσκευή." "Κρυπτογραφημένο από έναν προηγουμένως επαληθευμένο χρήστη." "Μη κρυπτογραφημένο." @@ -302,6 +353,10 @@ "Γεια, μίλα μου στην εφαρμογή %1$s :%2$s" "%1$s Android" "Κούνησε δυνατά τη συσκευή σου για να αναφέρεις κάποιο σφάλμα" + "%1$s: %2$s" + "Επιλογές" + "Αφαίρεση %1$s" + "Ρυθμίσεις" "Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά." "Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές." "Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά." @@ -317,7 +372,7 @@ "Δεν μπορείς να επιβεβαιώσεις; Πήγαινε στον λογαριασμό σου για να επαναφέρεις την ταυτότητά σου." "Ανάκληση επαλήθευσης και αποστολή" "Μπορείτε να ανακαλέσεις την επαλήθευσή σου και να στείλεις αυτό το μήνυμα όπως και να \'χει ή μπορείς να το ακυρώσεις προς το παρόν και να προσπαθήσεις ξανά αργότερα μετά την επαλήθευση του χρήστη %1$s." - "Το μήνυμά σου δεν στάλθηκε επειδή η επαληθευμένη ταυτότητα του χρήστη %1$s έχει αλλάξει" + "Το μήνυμά σου δεν στάλθηκε επειδή η επαληθευμένη ταυτότητα του χρήστη %1$s έχει επαναφερθεί" "Αποστολή μηνύματος ούτως ή άλλως" "Ο χρήστης %1$s χρησιμοποιεί τουλάχιστον μία μη επαληθευμένη συσκευή. Μπορείς να στείλεις το μήνυμα όπως και να \'χει ή μπορείς να το ακυρώσεις προς το παρόν και να δοκιμάσεις ξανά αργότερα αφού ο χρήστης %2$s επαληθεύσει όλες τις συσκευές του." "Το μήνυμά σου δεν στάλθηκε επειδή ο χρήστης %1$s δεν έχει επαληθεύσει όλες τις συσκευές" @@ -325,25 +380,29 @@ "Το μήνυμά σου δεν στάλθηκε επειδή δεν έχεις επαληθεύσει τουλάχιστον μία από τις συσκευές σου" "Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά." "Δεν ήταν δυνατή η ανάκτηση στοιχείων χρήστη" + "Μήνυμα στο %1$s" + "Επέκταση" + "Μείωση" + "Βλέπετε ήδη αυτήν την αίθουσα!" "%1$s από %2$s" "%1$s Καρφιτσωμένα μηνύματα" "Φόρτωση μηνύματος…" "Προβολή Όλων" "Συνομιλία" - "Το αίτημα συμμετοχής στάλθηκε" "Κοινή χρήση τοποθεσίας" "Κοινή χρήση της τοποθεσίας μου" "Άνοιγμα στο Apple Maps" "Άνοιγμα στο Google Maps" "Άνοιγμα στο OpenStreetMap" "Κοινή χρήση αυτής της τοποθεσίας" - "Το μήνυμα δεν στάλθηκε επειδή η επαληθευμένη ταυτότητα του χρήστη %1$s έχει αλλάξει." + "Το μήνυμα δεν στάλθηκε γιατί έγινε επαναφορά της επαληθευμένης ταυτότητας του χρήστη %1$s." "Το μήνυμα δεν στάλθηκε επειδή ο χρήστης %1$s δεν έχει επαληθεύσει όλες τις συσκευές." "Το μήνυμα δεν στάλθηκε επειδή δεν έχεις επαληθεύσει τουλάχιστον μία από τις συσκευές σου." "Τοποθεσία" "Έκδοση: %1$s (%2$s)" "el" "Τα ιστορικά μηνύματα δεν είναι διαθέσιμα σε αυτήν τη συσκευή" + "Πρέπει να επαληθεύσετε αυτήν τη συσκευή για πρόσβαση σε μηνύματα ιστορικού" "Δεν έχεις πρόσβαση σε αυτό το μήνυμα" "Δεν είναι δυνατή η αποκρυπτογράφηση μηνύματος" "Αυτό το μήνυμα αποκλείστηκε είτε επειδή δεν επαλήθευσες τη συσκευή σου είτε επειδή ο αποστολέας πρέπει να επαληθεύσει την ταυτότητά σου." 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 688c36348d3..06b02705254 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -1,16 +1,19 @@ + "Avatar" "Borrar" "%1$d dígito introducido" "%1$d dígitos introducidos" "Ocultar contraseña" + "Unirse a la llamada" "Ir al final" "Sólo menciones" "Silenciado" "Página %1$d" "Pausar" + "Mensaje de voz, duración: %1$s, posición actual: %2$s" "Campo PIN" "Reproducir" "Encuesta" @@ -24,18 +27,22 @@ "Leído por %1$s" "Pulsa para mostrar todo" - "Elimina la reacción con %1$s" + "Elimina la reacción con %1$s" "Enviar archivos" "Mostrar contraseña" "Iniciar llamada" "Menú de usuario" "Mostrar detalles" + "Mensaje de voz, duración: %1$s" "Grabar mensaje de voz" "Detener grabación" "Aceptar" + "Agregar leyenda" "Añadir a la cronología" "Atrás" + "Llamar" "Cancelar" + "Cancelar por ahora" "Elegir foto" "Borrar" "Cerrar" @@ -44,17 +51,23 @@ "Confirmar contraseña" "Continuar" "Copiar" + "Copiar leyenda" "Copiar enlace" "Copiar enlace al mensaje" "Copiar texto" "Crear" "Crear una sala" + "Desactivar" + "Desactivar cuenta" "Rechazar" + "Rechazar y bloquear" "Eliminar encuesta" "Desactivar" "Descartar" + "Descartar" "Hecho" "Editar" + "Editar leyenda" "Editar encuesta" "Activar" "Finalizar encuesta" @@ -76,21 +89,30 @@ "Cargar más" "Gestionar cuenta" "Administrar dispositivos" + "Enviar mensaje" "Siguiente" "No" "Ahora no" "OK" "Ajustes" "Abrir con" + "Fijar" "Respuesta rápida" "Citar" "Reaccionar" + "Rechazar" "Eliminar" + "Eliminar leyenda" + "Eliminar mensaje" "Responder" "Responder en el hilo" + "Denunciar" "Informar de un error" - "Reportar Contenido" + "Denunciar contenido" + "Denunciar conversación" + "Denunciar sala" "Restablecer" + "Restablecer identidad" "Reintentar" "Reintentar descifrado" "Guardar" @@ -99,6 +121,7 @@ "Enviar mensaje" "Compartir" "Compartir enlace" + "Mostrar" "Inicia sesión de nuevo" "Cerrar sesión" "Cerrar sesión de todos modos" @@ -109,47 +132,76 @@ "Pulsa para cargar el mapa" "Hacer foto" "Toca para ver opciones" - "Inténtalo de nuevo" - "Ver Fuente" + "Intentar de nuevo" + "Desprender" + "Ver en la cronología" + "Ver fuente" "Sí" + "Sí, intentar de nuevo" + "Tu servidor es ahora compatible con un protocolo nuevo y más rápido. Cierra la sesión y vuelve a iniciarla para actualizar ahora. Haciéndolo ahora, evitarás tener que cerrar la sesión cuando se elimine el protocolo antiguo." + "Actualización disponible" "Acerca de" "Política de uso aceptable" + "Añadiendo leyenda" "Ajustes avanzados" "Estadísticas" "Apariencia" "Sonido" "Usuarios bloqueados" "Burbujas" + "Llamada iniciada" "Copia de seguridad del chat" + "Copiado al portapapeles" "Derechos de autor" "Creando sala…" + "Solicitud cancelada" "Saliste de la sala" + "Invitación rechazada" "Oscuro" "Error de descifrado" "Opciones de desarrollador" + "ID de dispositivo" "Chat directo" + "No mostrar de nuevo" + "Descarga fallida" + "Descargando" "(editado)" "Edición" + "Editando leyenda" "* %1$s %2$s" + "Archivo vacío" + "Cifrado" "Cifrado activado" "Introduce tu PIN" "Error" + "Se ha producido un error, es posible que no recibas notificaciones de mensajes nuevos. Soluciona los problemas con las notificaciones desde los ajustes. + +Motivo: %1$s." "Todos" "Falló" "Favorito" "Marcado como favorito" "Archivo" + "Archivo eliminado" + "Archivo guardado" "Archivo guardado en Descargas" "Reenviar mensaje" + "Usado frecuentemente" "GIF" "Imagen" "En respuesta a %1$s" "Instalar APK" "No se encontró este ID de Matrix, por lo que es posible que no se reciba la invitación." - "Abandonando la sala" + "Saliendo de la sala" "Claro" + "Línea copiada al portapapeles" "Enlace copiado al portapapeles" "Cargando…" + "Cargando más…" + + "%d otro" + "%d otros" + "%1$d miembro" "%1$d miembros" @@ -160,13 +212,20 @@ "Mensaje eliminado" "Moderno" "Silenciar" + "%1$s (%2$s)" "No hay resultados" + "Sala sin nombre" + "No cifrado" "Sin conexión" + "Licencias de código abierto" "o" "Contraseña" "Personas" "Enlace permanente" "Permiso" + "Fijado" + "Comprueba tu conexión a Internet" + "Espera, por favor…" "¿Estás seguro de que quieres finalizar esta encuesta?" "Encuesta: %1$s" "Total de votos: %1$s" @@ -178,6 +237,7 @@ "Política de privacidad" "Reacción" "Reacciones" + "Motivo" "Clave de recuperación" "Recargando…" "Respondiendo a %1$s" @@ -195,6 +255,7 @@ "Buscar resultados" "Seguridad" "Visto por" + "Enviar a" "Enviando…" "Fallo al enviar" "Enviado" @@ -203,6 +264,8 @@ "Ajustes" "Ubicación compartida" "Cerrando sesión" + "Algo salió mal" + "Hemos encontrado un problema. Inténtalo de nuevo." "Iniciando chat…" "Sticker" "Terminado" @@ -216,51 +279,114 @@ "¿De qué trata esta sala?" "No se puede descifrar" "Enviado desde un dispositivo no seguro" + "No tienes acceso a este mensaje" + "Se restableció la identidad verificada del remitente" "Las invitaciones no se pudieron enviar a uno o más usuarios." "No se pudo enviar la(s) invitación(es)" "Desbloquear" "Dejar de silenciar" + "Llamada no compatible" "Evento no compatible" "Usuario" "Verificación cancelada" "Verificación completada" + "Verificación fallida" + "Verificado" "Verificar dispositivo" + "Verificar identidad" + "Verificar usuario" "Vídeo" "Mensaje de voz" "Esperando…" "Esperando este mensaje" + "Tú" + "Se restableció la identidad de %1$s. %2$s" + "Se restableció la identidad de %1$s %2$s. %3$s" + "(%1$s)" + "Se restableció la identidad de %1$s." + "Se restableció la identidad de %1$s %2$s. %3$s" + "Retirar la verificación" + "El enlace %1$s te está llevando a otro sitio %2$s + +¿Estás seguro de que quieres continuar?" + "Revisa este enlace" + "Sala denunciada" + "Denunciaste y saliste de la sala" "Confirmar" "Error" "Terminado" "Atención" "Tus cambios no se han guardado. ¿Estás seguro de que quieres volver atrás?" "¿Guardar cambios?" + "Tu servidor base debe actualizarse para admitir Matrix Authentication Service y la creación de cuentas." "No se pudo crear el enlace permanente" "%1$s no pudo cargar el mapa. Por favor vuelve a intentarlo más tarde." "Error al cargar mensajes" "%1$s no ha podido acceder a tu ubicación. Por favor vuelve a intentarlo más tarde." "No se pudo cargar tu mensaje de voz." + "Mensaje no encontrado" "%1$s no tiene permiso para acceder a tu ubicación. Puedes habilitar el acceso en Ajustes." "%1$s no tiene permiso para acceder a tu ubicación. Habilita el acceso a continuación." "%1$s no tiene permiso para acceder al micrófono. Habilita el acceso para grabar un mensaje de voz." + "Esto puede deberse a problemas de red o del servidor." + "Esta dirección de sala ya existe. Intenta editar el campo de dirección de sala o cambia el nombre de la sala" + "No se permiten algunos caracteres. Solo se admiten letras, dígitos y los siguientes símbolos ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Algunos mensajes no se han enviado" "Lo siento, se ha producido un error" + "La autenticidad de este mensaje cifrado no puede ser garantizada en este dispositivo." + "Cifrado por un usuario verificado anteriormente." + "No cifrado." + "Cifrado por un dispositivo desconocido o eliminado." + "Cifrado por un dispositivo no verificado por su propietario." + "Cifrado por un usuario no verificado." "🔐️ Únete a mí en %1$s" "Hola, puedes hablar conmigo en %1$s: %2$s" "%1$s Android" "Agitar con fuerza para informar de un error" "Error al seleccionar archivos multimedia, por favor inténtalo de nuevo." + "Es posible que las leyendas no sean visibles para las personas que usan aplicaciones más antiguas." "Error al procesar el contenido multimedia, por favor inténtalo de nuevo." "Error al subir el contenido multimedia, por favor inténtalo de nuevo." + "Presiona sobre un mensaje y selecciona «%1$s» para incluirlo aquí." + "Fija los mensajes importantes para que se puedan descubrir fácilmente" + + "%1$d mensaje fijado" + "%1$d mensajes fijados" + + "Mensajes fijados" + "Estás a punto de ir a tu cuenta de %1$s para restablecer tu identidad. Posteriormente volverás a la aplicación." + "¿No puedes confirmar? Ve a tu cuenta para restablecer tu identidad." + "Retirar la verificación y enviar" + "Puedes retirar tu verificación y enviar este mensaje de todos modos, o puedes cancelarlo por ahora e intentarlo de nuevo más tarde después de volver a verificar a %1$s." + "Tu mensaje no se envió porque la identidad verificada de %1$s fue restablecida" + "Enviar mensaje de todos modos" + "%1$s utiliza uno o más dispositivos no verificados. Puedes enviar el mensaje de todos modos, o puedes cancelarlo por ahora y volver a intentarlo más tarde, una vez %2$s haya verificado todos sus dispositivos." + "Tu mensaje no se envió porque %1$s no ha verificado todos los dispositivos" + "Uno o más de tus dispositivos no están verificados. Puedes enviar el mensaje de todos modos, o puedes cancelarlo por ahora e intentarlo de nuevo más tarde, una vez hayas verificado todos tus dispositivos." + "Tu mensaje no se envió porque no has verificado uno o más de tus dispositivos" "Error al procesar el contenido multimedia, por favor inténtalo de nuevo." "No se pudieron recuperar los detalles del usuario" + "Mensaje en %1$s" + "%1$s de %2$s" + "%1$s mensajes fijados" + "Cargando mensaje…" + "Ver todos" + "Chat" "Compartir ubicación" "Compartir mi ubicación" "Abrir en Apple Maps" "Abrir en Google Maps" "Abrir en OpenStreetMap" "Compartir esta ubicación" + "Mensaje no enviado porque la identidad verificada de %1$s fue restablecida." + "Mensaje no enviado porque %1$s no ha verificado todos los dispositivos." + "Mensaje no enviado porque no has verificado uno o más de tus dispositivos." "Ubicación" "Versión: %1$s (%2$s)" "es" + "Los mensajes históricos no están disponibles en este dispositivo" + "Debes verificar este dispositivo para acceder a los mensajes históricos" + "No tienes acceso a este mensaje" + "No se ha podido descifrar el mensaje" + "Este mensaje fue bloqueado bien sea porque no verificaste tu dispositivo o porque el remitente necesita verificar tu identidad." 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 47b4bcae03f..805e30ec94b 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -1,16 +1,20 @@ + "Reageeri: %1$s" + "Tunnuspilt" "Kustuta" "%1$d number sisestatud" "%1$d numbrit sisestatud" "Peida salasõna" + "Liitu kõnega" "Mine lõppu" "Ainult mainimised" "Summutatud" "%1$d. lehekülg" "Peata" + "Häälsõnum, kestus:%1$s, praegune asukoht: %2$s" "PIN-koodi väli" "Esita" "Küsitlus" @@ -24,13 +28,16 @@ "Seda luges %1$s" "Vaata kõiki" - "Eemalda reageerimine %1$s emotikoniga" + "Eemalda reageerimine %1$s emotikoniga" + "Eemalda reageerimine: %1$s" "Saada faile" "Näita salasõna" "Helista" "Kasutajamenüü" + "Vaata tunnuspilti" "Vaata üksikasju" - "Salvesta häälsõnum" + "Häälsõnum, kestus:%1$s" + "Salvesta häälsõnum." "Lõpeta salvestamine" "Nõustu" "Lisa selgitus" @@ -56,9 +63,11 @@ "Eemalda konto" "Eemalda konto kasutusest" "Keeldu" + "Keeldu ja blokeeri" "Kustuta küsitlus" "Lülita välja" "Loobu" + "Lõpeta" "Valmis" "Muuda" "Muuda selgitust" @@ -100,8 +109,11 @@ "Eemalda sõnum" "Vasta" "Vasta jutulõngas" + "Teata" "Teata veast" "Teata sisust haldurile" + "Teata vestlusest" + "Teata jututoast" "Lähtesta" "Lähtesta oma identiteet" "Proovi uuesti" @@ -129,10 +141,13 @@ "Vaata lähtekoodi" "Jah" "Jah, proovi uuesti" + "Sinu koduserver toetab uut ja kiiremat protokolli. Uuendamiseks logi korraks rakendusest välja ja siis tagasi. Mingil hetkel tulevikus vana protokoll eemaldatakse kasutusest ja tehes uuenduse nüüd, väldid hilisemat sundkorras uuendust." + "Saadaval on uuendus" "Rakenduse teave" "Vastuvõetava kasutamise põhimõtted" "Lisame selgitust" "Täiendavad seadistused" + "pilt" "Analüütika" "Välimus" "Heli" @@ -143,7 +158,9 @@ "Kopeeritud lõikelauale" "Autoriõigused" "Loome jututoa…" + "Päring on tühistatud" "Lahkus jututoast" + "Keeldusid kutsest" "Tume" "Dekrüptimisviga" "Arendaja valikud" @@ -156,6 +173,7 @@ "Muutmine" "Muudame selgitust" "* %1$s %2$s" + "Tühi fail" "Krüptimine" "Krüptimine on kasutusel" "Sisesta oma PIN-kood" @@ -180,9 +198,14 @@ Põhjus: %1$s." "Sellist Matrix\'i kasutajatunnust ei õnnestu leida, seega sõnumit ilmselt keegi kätte ei saa." "Oled lahkumas jututoast" "Hele" + "Rida on kopeeritud lõikelauale" "Link on kopeeritud lõikelauale" "Laadime…" "Laadime veel…" + + "%d muu" + "%d muud" + "%1$d liige" "%1$d liiget" @@ -193,8 +216,10 @@ Põhjus: %1$s." "Sõnum on eemaldatud" "Kaasaegne" "Summutatud" + "%1$s (%2$s)" "Otsingul pole tulemusi" "Jututoal puudub nimi" + "Krüptimata" "Võrgust väljas" "Avatud lähtekoodiga litsentsid" "või" @@ -203,6 +228,7 @@ Põhjus: %1$s." "Püsilink" "Õigus" "Esiletõstetud" + "Palun kontrolli oma nutiseadme internetiühendust" "Palun oota…" "Kas oled kindel, et soovid selle küsitluse lõpetada?" "Küsitlus: %1$s" @@ -215,8 +241,13 @@ Põhjus: %1$s." "Privaatsuspoliitika" "Reaktsioon" "Reaktsioonid" + "Põhjus" "Taastevõti" "Värskendame andmeid…" + + "%1$d vastus" + "%1$d vastust" + "Vastates kasutajale %1$s" "Teata veast" "Teata veast" @@ -236,12 +267,14 @@ Põhjus: %1$s." "Saadame…" "Saatmine ei õnnestunud" "Saadetud" + ". " "Server pole toetatud" "Serveri URL" "Seadistused" "Jagatud asukoht" "Logime välja" "Midagi läks valesti" + "Tekkis viga. Palun proovi uuesti." "Alustame vestlust…" "Kleeps" "Õnnestus" @@ -256,7 +289,7 @@ Põhjus: %1$s." "Dekrüptimine ei olnud võimalik" "Saadetud ebaturvalisest seadmest" "Sul pole ligipääsu antud sõnumile" - "Saatja verifitseeritud identiteet on muutunud" + "Saatja verifitseeritud identiteet on lähtestatud" "Kutset polnud võimalik saata ühele või enamale kasutajale." "Kutse(te) saatmine ei õnnestunud" "Eemalda lukustus" @@ -270,16 +303,24 @@ Põhjus: %1$s." "Verifitseeritud" "Verifitseeri seade" "Verifitseeri võrguidentiteet" + "Verifitseeri kasutaja" "Video" "Häälsõnum" "Ootame…" "Ootame selle sõnumi dekrüptimisvõtit" "Sina" - "Kasutaja %1$s võrguidentiteet tundub olema muutunud. %2$s" - "Kasutaja %1$s %2$s võrguidentiteet tundub olema muutunud. %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 (%2$s kasutajanimi) verifitseeritud identiteet on muutunud. %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" + "%1$s link viib sind teise veebisaiti %2$s + +Kas sa oled kindel, et soovid jätkata?" + "Palun kontrolli seda linki mõttega" + "Teatasid jututoast" + "Teatasid jututoast ja lahkusid sealt" "Kinnitus" "Viga" "Õnnestus" @@ -292,14 +333,17 @@ Põhjus: %1$s." "Sõnumite laadimine ei õnnestunud" "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." "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." "Rakendusel %1$s puudub õigus sinu nutiseadme mikrofoni kasutada. Järgnevalt anna õigused heli salvestamiseks." + "See võib olla põhjustatud võrgu- või serverivigadest." "Selline jututoa aadress on juba olemas. Palun proovi muuta kas aadressi või jututoa nime" "Mõned tähemärgid pole lubatud. Kasuta vaid tähti, numbreid ja neid kirjavahemärke ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Mõned sõnumid on saatmata" "Vabandust, ilmnes viga" + "Sündmuse saatja ja seadme omanik pole vastavuses." "Selle krüptitud sõnumi tõepärasus pole selles seadmes tagatud." "Krüptitud varem verifitseeritud kasutaja poolt" "Pole krüptitud." @@ -310,6 +354,10 @@ Põhjus: %1$s." "Hei, suhtle minuga %1$s võrgus: %2$s" "%1$s Android" "Veast teatamiseks raputa nutiseadet ägedalt" + "%1$s: %2$s" + "Valikud" + "Kustuta: %1$s" + "Seadistused" "Meediafaili valimine ei õnnestunud. Palun proovi uuesti." "Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele." "Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti." @@ -325,7 +373,7 @@ Põhjus: %1$s." "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 verifitseeritud identiteet on muutunud." + "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" @@ -333,19 +381,22 @@ Põhjus: %1$s." "Kuna sul on üks või enam verifitseerimata seadet, siis sinu sõnum jäi saatmata" "Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti." "Kasutaja andmete laadimine ei õnnestunud" + "Sõnum jututoas %1$s" + "Näita rohkem" + "Näita vähem" + "Sa juba vaatad seda jututuba!" "%1$s / %2$s" "%1$s esiletõstetud sõnumit" "Laadime sõnumit…" "Näita kõiki" "Vestlus" - "Liitumispäring on saadetud" "Jaga asukohta" "Jaga minu asukohta" "Ava Apple Mapsis" "Ava Google Mapsis" "Ava OpenStreetMapis" "Jaga seda asukohta" - "Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on muutunud." + "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" diff --git a/libraries/ui-strings/src/main/res/values-eu/translations.xml b/libraries/ui-strings/src/main/res/values-eu/translations.xml new file mode 100644 index 00000000000..38cd755a78e --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-eu/translations.xml @@ -0,0 +1,344 @@ + + + "Abatarra" + "Ezabatu" + "Ezkutatu pasahitza" + "Batu deira" + "Joan behera" + "Aipamenak soilik" + "Mutututa" + "%1$d. orria" + "Eten" + "Ahots-mezua, iraupena: %1$s, uneko posizioa: %2$s" + "PINaren eremua" + "Erreproduzitu" + "Inkesta" + "Amaitutako inkesta" + "Erreakzionatu %1$s(e)kin" + "Erreakzionatu beste emoji batzuekin" + "%1$s(e)k eta %2$s(e)k irakurri dute" + + "%1$s(e)k eta beste %2$dek irakurri dute" + "%1$s(e)k eta beste %2$d(e)k irakurri dute" + + "%1$s(e)k irakurri du" + "Egin tap guztiak ikusteko" + "Kendu %1$s erreakzioa" + "Bidali fitxategiak" + "Erakutsi pasahitza" + "Hasi dei bat" + "Erabiltzailea-menua" + "Ikusi xehetasunak" + "Ahots-mezua, iraupena: %1$s" + "Grabatu ahots-mezua." + "Utzi grabatzeari" + "Onartu" + "Gehitu testua" + "Gehitu denbora-lerrora" + "Atzera" + "Deia" + "Utzi" + "Utzi oraingoz" + "Aukeratu argazkia" + "Garbitu" + "Itxi" + "Burutu egiaztapena" + "Berretsi" + "Berretsi pasahitza" + "Jarraitu" + "Kopiatu" + "Kopiatu testua" + "Kopiatu esteka" + "Kopiatu esteka mezura" + "Kopiatu testua" + "Sortu" + "Sortu gela" + "Desaktibatu" + "Desaktibatu kontua" + "Ukatu" + "Baztertu eta blokeatu" + "Ezabatu inkesta" + "Desgaitu" + "Baztertu" + "Baztertu" + "Eginda" + "Editatu" + "Editatu testua" + "Editatu inkesta" + "Gaitu" + "Amaitu inkesta" + "Sartu PINa" + "Pasahitza ahaztu duzu?" + "Birbidali" + "Joan atzera" + "Ezikusi" + "Gonbidatu" + "Gonbidatu jendea" + "Gonbidatu jendea %1$s(e)ra" + "Gonbidatu jendea %1$s(e)ra" + "Gonbidapenak" + "Elkartu" + "Informazio gehiago" + "Atera" + "Utzi elkarrizketa" + "Atera gelatik" + "Kargatu gehiago" + "Kudeatu kontua" + "Kudeatu gailuak" + "Bidali mezua" + "Hurrengoa" + "Ez" + "Orain ez" + "Ados" + "Ezarpenak" + "Ireki honekin…" + "Finkatu" + "Erantzun azkarra" + "Aipatu" + "Erreakzioa" + "Baztertu" + "Kendu" + "Kendu testua" + "Kendu mezua" + "Erantzun" + "Erantzun harian" + "Salatu" + "Eman errore baten berri" + "Salatu edukia" + "Salatu elkarrizketa" + "Salatu gela" + "Berrezarri" + "Berrezarri identitatea" + "Saiatu berriro" + "Saiatu berriro deszifratzen" + "Gorde" + "Bilatu" + "Bidali" + "Bidali mezua" + "Partekatu" + "Partekatu esteka" + "Erakutsi" + "Hasi saioa berriro" + "Amaitu saioa" + "Amaitu saioa edonola ere" + "Saltatu" + "Hasi" + "Hasi txata" + "Hasi egiaztapena" + "Sakatu mapa kargatzeko" + "Egin argazkia" + "Sakatu aukerak ikusteko" + "Saiatu berriro" + "Utzi finkatzeari" + "Ikusi denbora-lerroan" + "Ikusi iturburua" + "Bai" + "Bai, saiatu berriro" + "Orain zure zerbitzaria protokolo berri eta azkarrago batekin da bateragarria. Amaitu saioa eta hasi berriro protokolo berria erabiltzeko. Orain eginez gero, etorkizunean protokolo zaharra kentzen denean ez zaizu behartutako saioa ixtera." + "Bertsio-berritzea eskuragarri" + "Honi buruz" + "Testua gehitzen" + "Ezarpen aurreratuak" + "Estatistikak" + "Itxura" + "Audioa" + "Blokeatutako erabiltzaileak" + "Bunbuiloak" + "Deia hasi da" + "Txataren babeskopia" + "Arbelean kopiatu da" + "Copyrighta" + "Gela sortzen…" + "Eskaera bertan behera utzi da" + "Gelatik atera da" + "Gonbidapenari uko egin zaio" + "Iluna" + "Deszifratze-errorea" + "Garapen aukerak" + "Gailuaren IDa" + "Txata zuzena" + "Ez erakutsi berriro" + "Deskargak huts egin du" + "Deskargatzen" + "(Editatua)" + "Editatzen" + "Testua editatzen" + "Fitxategia hutsik dago" + "Zifratzea" + "Zifratzea gaituta" + "Sartu zure PINa" + "Errorea" + "Errore bat gertatu da, litekeena da mezu berrien jakinarazpenik ez jasotzea. Aztertu jakinarazpenen ezarpenak. + +Arrazoia: %1$s." + "Guztiak" + "Huts egin du" + "Gogokoa" + "Gogoko eginda" + "Fitxategia" + "Fitxategia ezabatu da" + "Fitxategia gorde da" + "Fitxategia Deskargak atalean gorde da" + "Birbidali mezua" + "Maiz erabilitakoa" + "GIFa" + "Irudia" + "%1$s(r)i erantzunez" + "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…" + "Gehiago kargatzen…" + + "beste %d" + "beste %d" + + + "Kide %1$d" + "%1$d kide" + + "Mezua" + "Mezuen ekintzak" + "Mezuen antolaketa" + "Mezua kendu da" + "Modernoa" + "Mututu" + "Emaitzarik ez" + "Gelak ez du izenik" + "Zifratu gabe" + "Deskonektatuta" + "Kode irekiko lizentziak" + "edo" + "Pasahitza" + "Jendea" + "Esteka iraunkorra" + "Baimena" + "Finkatuta" + "Egiaztatu Interneteko konexioa" + "Itxaron…" + "Ziur inkesta hau amaitu nahi duzula?" + "Inkesta:%1$s" + "Boto guztira: %1$s" + "Emaitzak inkesta amaitu ondoren erakutsiko dira" + + "Boto %d" + "%d boto" + + "Pribatutasun-politika" + "Erreakzioa" + "Erreakzioak" + "Arrazoia" + "Berreskuratze-gakoa" + "Freskatzen…" + "%1$s(r)i erantzuten" + "Eman akats baten berri" + "Eman arazo baten berri" + "Salaketa bidali da" + "Testu aberatsaren editorea" + "Gela" + "Gelaren izena" + "adibidez, zure proiektuaren izena" + "Gordetako aldaketak" + "Gordetzen" + "Blokeo-pantaila" + "Bilatu norbait" + "Bilaketaren emaitzak" + "Segurtasuna" + "Ikusi du" + "Bidali" + "Bidaltzen…" + "Bidalketak huts egin du" + "Bidalita" + "Zerbitzaria ez da bateragarria" + "Zerbitzariaren URLa" + "Ezarpenak" + "Partekatutako kokapena" + "Saioa amaitzen" + "Arazoren bat egon da" + "Txata hasten…" + "Pegatina" + "Arrakasta" + "Iradokizunak" + "Sinkronizatzen" + "Sistema" + "Testua" + "Hirugarrenei buruzko oharrak" + "Haria" + "Gaia" + "Zeri buruzko gela da?" + "Ezin da desenkriptatu" + "Egiaztatu gabeko gailu batetik bidalia" + "Igorlearen egiaztatutako identitatea berrezarri da" + "Ezin izan dira gonbidapenak erabiltzaile bati edo gehiagori bidali." + "Ezin d(ir)a gonbidapena(k) bidali" + "Desblokeatu" + "Aktibatu audioa" + "Deia ez da bateragarria" + "Gertaera ez da bateragarria" + "Erabiltzaile-izena" + "Egiaztaketa ezeztatuta" + "Egiaztaketa burututa" + "Egiaztapenak huts egin du" + "Egiaztatuta" + "Egiaztatu gailua" + "Egiaztatu identitatea" + "Egiaztatu erabiltzailea" + "Bideoa" + "Ahots-mezua" + "Zain…" + "Mezu honen zain" + "Zeu" + "%1$s(r)en identitatea berrezarri da. %2$s" + "%1$s(r)en %2$s identitatea berrezarri da. %3$s" + "%1$s(r)en identitatea berrezarri da." + "%1$s(r)en %2$s identitatea berrezarri da. %3$s" + "Egiaztatu honako esteka" + "Gela salatu da" + "Gela salatu eta utzi da" + "Baieztapena" + "Errorea" + "Arrakasta" + "Abisua" + "Zure aldaketak ez dira gorde. Ziur itzuli nahi duzula?" + "Aldaketak gorde?" + "Huts egin du esteka iraunkorra sortzeak" + "%1$s ezin izan da mapa kargatu. Saiatu berriro geroago." + "Huts egin du mezuak kargatzeak" + "%1$s ezin izan da zure kokapena atzitu. Saiatu berriro geroago." + "Ahots-mezua igotzeak huts egin du" + "Ez da mezua aurkitu" + "%1$sek ez du zure kokapena atzitzeko baimenik. Sarbidea Ezarpenetan gaitu dezakezu." + "%1$sek ez du zure kokapena atzitzeko baimenik. Gaitu sarbidea behean." + "%1$s(e)k ez du mikrofonoa atzitzeko baimenik. Gaitu sarbidea ahots-mezua grabatzeko." + "Sarearen edo zerbitzariaren arazoren bategatik izan daiteke." + "Gela badago lehendik ere. Saiatu gelaren helbidearen eremua editatzen edo aldatu gelaren izena" + "Karaktere batzuk ez daude baimenduta. Hizkiak, zifrak, eta ondorengo ikurrak onartzen dira bakarrik: ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" + "Mezu batzuk ez dira bidali" + "Barka, errore bat gertatu da" + "Ez dago zifratuta." + "🔐️ Zatoz nirekin %1$s(e)ra" + "%1$s Android" + "Astindu erroreen berri emateko" + "Huts egin du multimedia aukeratzeak, saiatu berriro." + "Huts egin du multimedia igotzeak, saiatu berriro." + "Finkatutako mezuak" + "Bidali mezua hala ere" + "%2$s(e)tik %1$s" + "Mezua kargatzen…" + "Ikusi guztia" + "Txata" + "Partekatu kokapena" + "Partekatu nire kokapena" + "Ireki Apple Maps-en" + "Ireki Google Maps-en" + "Ireki OpenStreetMap-en" + "Partekatu kokapen hau" + "Kokapena" + "Bertsioa: %1$s (%2$s)" + "eu" + "Iraganeko mezuak ez daude gailu honetan eskuragarri" + "Ezin da mezua deszifratu" + 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 b41246e3b77..9ea728cbfaf 100644 --- a/libraries/ui-strings/src/main/res/values-fa/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fa/translations.xml @@ -1,7 +1,13 @@ + "چهرک" "حذف" + + "%1$d رقم وارد شده" + "%1$d رقم وارد شده" + "نهفتن گذرواژه" + "پیوستن به تماس" "به پایین بروید" "فقط اشاره‌ها" "خموش" @@ -16,14 +22,16 @@ "خوانده به دست %1$s و %2$s" "خوانده به دست %1$s" "زدن برای نمایش همه" - "برداشتن واکنش با %1$s" + "برداشتن واکنش با %1$s" "ارسال پرونده‌ها" "نمایش گذرواژه" "آغاز یک تماس" "فهرست کاربر" + "دیدن جزییات" "ضبط پیام صوتی." "توقّف ضبط" "پذیرش" + "افزودن عنوان" "افزودن به خط زمانی" "بازگشت" "تماس" @@ -37,18 +45,23 @@ "تأیید گذرواژه" "ادامه" "رونوشت" + "رونوشت از عنوان7" "رونوشت از پیوند" "رونوشت از پیوند پیام" + "رونوشت از متن" "ایجاد" "ایجاد اتاق" "غیرفعّال" "غیرفعّال‌سازی حساب" "رد" + "رد و انسداد" "حذف نظرسنجی" "از کار انداختن" "دور ریختن" + "دورانداختن" "انجام شد" "ویرایش" + "ویرایش عنوان" "ویرایش نظرسنجی" "به کار انداختن" "پایان نظرسنجی" @@ -56,6 +69,7 @@ "گذرواژه را فراموش کردید؟" "پیشروی" "پس‌روی" + "نادیده‌گرفتن" "دعوت" "دعوت افراد" "دعوت به %1$s" @@ -82,10 +96,15 @@ "واکنش" "رد کردن" "برداشتن" + "برداشتن عنوان" + "برداشتن پیام‌ها" "پاسخ" "پاسخ در رشته" + "گزارش" "گزارش اشکال" "گزارش محتوا" + "گزارش گفت‌وگو" + "گزارش اتاق" "بازنشانی" "بازنشانی هویت" "تلاش دوباره" @@ -112,8 +131,11 @@ "دیدن در خط زمانی" "دیدن منبع" "بله" + "بله. تلاش دوباره" + "ارتقا موجود است" "درباره" "سیاست استفادهٔ پذیرفتنی" + "افزودن عنوان" "تنظیمات پیش‌رفته" "تجزیه و تحلیل" "ظاهر" @@ -125,15 +147,23 @@ "در تخته‌گیره رونوشت شد" "حق رونوشت" "ایجاد کردن اتاق…" + "درخواست لغو شد" "اتاق را ترک کرد" + "دعوت لغو شد" "تیره" "خطای رمزگشایی" "گزینه‌های توسعه دهنده" + "شناسه دستگاه" "گپ مستقیم" "این مورد را دوباره نشان نده" + "بارگیری شکست خورد" + "بار گرفتن" "(ویراسته)" "ویرایش" + "ویراستن عنوان" "* %1$s %2$s" + "پروندهٔ خالی" + "رمزنگاری" "رمزنگاری به کار افتاده" "پینتان را وارد کنید" "خطا" @@ -142,8 +172,11 @@ "برگزیده" "برگزیده" "پرونده" + "پرونده حذف شد" + "پرونده ذخیره شد" "پرونده در بارگیری‌ها ذخیره شد" "هدایت پیام" + "پراستفاده" "جیف" "تصویر" "در پاسخ به %1$s" @@ -151,8 +184,10 @@ "شناسهٔ ماتریکس نتوانست پیدا شود. ممکن است دعوت نرسیده باشد." "ترک کردن اتاق" "روشن" + "خط در تخته‌گیره رونوشت شد" "پیوند در تخته‌گیره رونوشت شد" "بار کردن…" + "بار کردن بیش‌تر…" "%1$d عضو" "%1$d عضو" @@ -163,8 +198,10 @@ "پیام برداشته شد" "نوین" "بی‌صدا" + "%1$s ‏(%2$s)" "بدون نتیجه" "بدون نام اتاق" + "رمزنگارش نشده" "برون‌خط" "پروانه‌های نرم‌افزاری آزاد" "یا" @@ -173,14 +210,20 @@ "پایاپیوند" "اجازه" "سنجاق شده" + "لطفاً اتّصال اینترنتیتان را بررسی کنید" "لطفا صبر کنید…" "مطئنید که می‌خواهید این نظرسنجی را پایان دهید؟" "نظرسنجی: %1$s" "مجموع آرا: %1$s" "نتیجه‌ها پس از پایان نظرسنجی نشان داده خواهند شد" + + "%d رأی" + "%d رأی" + "سیاست محرمانگی" "واکنش" "واکنش‌ها" + "دلیل" "کلید بازیابی" "تازه سازی…" "پاسخ دادن به %1$s" @@ -225,17 +268,27 @@ "ناتوان در فرستادن دعوت(ها)" "قفل‌گشایی" "باصدا" + "تماس پشتیبانی نشده" "رویداد پشتیبانی نشده" "نام کاربری" "تأیید لغو شد" "تأیید کامل شد" + "صحت‌سنجی شکست خورد" + "تأیید‌شده" "تأیید افزاره" + "تأیید هویت" + "تأیید کاربر" "ویدیو" "پیام صوتی" "در انتظار…" "در انتظار این پیام" "شما" - "به نظر می‌رسد هویت %1$s تغییر کرده. %2$s" + "هویت %1$s بازنشانی شد. %2$s" + "(%1$s)" + "انصراف از تأیید" + "این پیوند را دوباره بررسی کنید" + "اتاق گزارش شد" + "گزارش و از اتاق خارج شد" "تایید" "خطا" "موفّقیت" @@ -243,9 +296,14 @@ "تغییراتتان ذخیره نشده‌اند. مطمئنید که می‌خواهید برگردید؟" "ذخیرهٔ تغییرات؟" "شکست در ایجاد پایاپیوند" + "%1$s نتوانست نقشه را بارگیری کند. لطفا بعدا دوباره امتحان کنید." "شکست در بار کردن پیام‌ها" + "%1$s نمی تواند به موقعیت مکانی شما دسترسی داشته باشد. لطفا بعدا دوباره امتحان کنید." "شکست در بارگذاری پیام صوتیتان." "پیام پیدا نشد" + "%1$s اجازه دسترسی به موقعیت مکانی شما را ندارد. می توانید دسترسی را در تنظیمات فعال کنید." + "%1$s اجازه دسترسی به موقعیت مکانی شما را ندارد. دسترسی را در زیر فعال کنید." + "%1$s اجازه دسترسی به میکروفون شما را ندارد. دسترسی را برای ضبط پیام صوتی فعال کنید." "برخی پیام‌ها ارسال نشده‌اند" "متأسفیم ، خطایی رخ داد" "اعتبار این پیام رمز شده نمی‌تواند روی این افزاره تأیید شود." @@ -257,6 +315,7 @@ "🔐️ پییوستن به من روی %1$s" "درود. با من روی %1$s صحبت کن: %2$s" "%1$s اندروید" + "Rageshake برای گزارش اشکال" "گزینش رسانه شکست خورد. لطفاً دوباره تلاش کنید." "پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید." "بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید." @@ -264,6 +323,8 @@ "داردید برای بازنشانی هویتتان به حساب %1$s می‌روید. پس از آن به کاره برگردانده خواهید شد." "فرستادن پیام به هر روی" "پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید." + "نمی توان جزئیات کاربر را بازیابی کرد" + "پیام در %1$s" "%1$s از %2$s" "%1$s پیام‌های سنجاق شده" "بار کردن پشام‌ها…" @@ -279,4 +340,5 @@ "نگارش : %1$s (%2$s)" "fa" "en" + "به این پیام دسترسی ندارید" 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 e43d2a583f0..8b0d3e0cf76 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -1,16 +1,19 @@ + "Avatar" "Poista" "%1$d numero syötetty" "%1$d numeroa syötetty" "Piilota salasana" + "Liity puheluun" "Siirry loppuun" "Vain maininnat" "Mykistetty" "Sivu %1$d" "Keskeytä" + "Ääniviesti, kesto: %1$s, nykyinen sijainti: %2$s" "PIN-kenttä" "Toista" "Kysely" @@ -24,12 +27,13 @@ "%1$s on lukenut viestin" "Näytä kaikki napauttamalla" - "Poista reaktio emojilla %1$s" + "Poista reaktio emojilla %1$s" "Lähetä tiedostoja" "Näytä salasana" "Aloita puhelu" "Käyttäjävalikko" "Näytä tiedot" + "Ääniviesti, kesto: %1$s" "Nauhoita ääniviesti." "Lopeta nauhoittaminen" "Hyväksy" @@ -56,9 +60,11 @@ "Deaktivoi" "Deaktivoi tili" "Hylkää" + "Hylkää ja estä" "Poista kysely" "Poista käytöstä" "Hylkää" + "Sulje" "Valmis" "Muokkaa" "Muokkaa kuvatekstiä" @@ -100,8 +106,11 @@ "Poista viesti" "Vastaa" "Vastaa ketjuun" + "Ilmoita" "Ilmoita virheestä" "Ilmoita sisällöstä" + "Ilmoita keskustelusta" + "Ilmoita huoneesta" "Nollaa" "Nollaa identiteetti" "Yritä uudelleen" @@ -129,6 +138,8 @@ "Näytä lähde" "Kyllä" "Kyllä, yritä uudelleen" + "Palvelimesi tukee nyt uutta, nopeampaa protokollaa. Kirjaudu ulos ja takaisin sisään päivittääksesi nyt. Jos teet tämän nyt, voit välttää pakotetun uloskirjautumisen, kun vanha protokolla poistetaan myöhemmin." + "Päivitys saatavilla" "Tietoa" "Hyväksyttävän käytön käytäntö" "Lisätään kuvatekstiä" @@ -143,7 +154,9 @@ "Kopioitu leikepöydälle" "Tekijänoikeudet" "Luodaan huonetta…" + "Pyyntö peruutettu" "Poistuit huoneesta" + "Kutsu hylätty" "Tumma" "Salauksen purkuvirhe" "Kehittäjän asetukset" @@ -156,6 +169,7 @@ "Muokataan viestiä" "Muokataan kuvatekstiä" "* %1$s %2$s" + "Tyhjä tiedosto" "Salaus" "Salaus käytössä" "Syötä PIN-koodisi" @@ -180,9 +194,14 @@ Syy: %1$s." "Tätä Matrix-tunnusta ei löytynyt, joten kutsu ei välttämättä mene perille." "Poistutaan huoneesta" "Vaalea" + "Rivi kopioitu leikepöydälle" "Linkki kopioitu leikepöydälle" "Ladataan…" "Ladataan lisää…" + + "%d muu" + "%d muuta" + "%1$d jäsen" "%1$d jäsentä" @@ -193,8 +212,10 @@ Syy: %1$s." "Viesti poistettu" "Moderni" "Mykistä" + "%1$s (%2$s)" "Ei tuloksia" "Nimetön huone" + "Ei salattu" "Ei yhteyttä" "Avoimen lähdekoodin lisenssit" "tai" @@ -203,6 +224,7 @@ Syy: %1$s." "Pysyvä linkki" "Lupa" "Kiinnitetty" + "Tarkista internet-yhteytesi" "Odota hetki…" "Haluatko varmasti lopettaa tämän kyselyn?" "Kysely: %1$s" @@ -215,6 +237,7 @@ Syy: %1$s." "Tietosuojakäytäntö" "Reaktio" "Reaktiot" + "Syy" "Palautusavain" "Päivitetään…" "Vastataan käyttäjälle %1$s" @@ -242,6 +265,7 @@ Syy: %1$s." "Jaettu sijainti" "Kirjaudutaan ulos" "Jokin meni pieleen" + "Kohtasimme ongelman. Yritä uudelleen." "Aloitetaan keskustelua…" "Tarra" "Onnistui" @@ -256,7 +280,7 @@ Syy: %1$s." "Salauksen purkaminen ei onnistunut" "Lähetetty suojaamattomasta laitteesta" "Sinulla ei ole oikeutta lukea tätä viestiä" - "Lähettäjän vahvistettu identiteetti on vaihtunut" + "Lähettäjän vahvistettu identiteetti nollattiin" "Kutsujen ei voitu lähettää yhdelle tai useammalle käyttäjälle." "Kutsujen lähettäminen ei onnistunut" "Avaa" @@ -270,16 +294,24 @@ Syy: %1$s." "Vahvistettu" "Vahvista laite" "Vahvista identiteetti" + "Vahvista käyttäjä" "Video" "Ääniviesti" "Odotetaan…" "Odotetaan viestiä" "Sinä" - "Käyttäjän %1$s identiteetti näyttää muuttuneen. %2$s" - "Käyttäjän %1$s %2$s identiteetti näyttää muuttuneen. %3$s" + "Käyttäjän %1$s identiteetti nollattiin. %2$s" + "Käyttäjän %1$s %2$s identiteetti nollattiin. %3$s" "(%1$s)" - "Käyttäjän %1$s %2$s vahvistettu identiteetti on muuttunut. %3$s" + "Käyttäjän %1$s identiteetti nollattiin." + "Käyttäjän %1$s %2$s identiteetti nollattiin. %3$s" "Peruuta vahvistus" + "Linkki %1$s on viemässä sinua toiselle sivustolle %2$s + +Haluatko varmasti jatkaa?" + "Tarkista tämä linkki" + "Huone ilmoitettu" + "Ilmoitettu ja poistuttu huoneesta" "Vahvistus" "Virhe" "Onnistui" @@ -296,6 +328,7 @@ Syy: %1$s." "%1$s -sovelluksella ei ole lupaa sijaintiisi. Voit sallia sen asetuksista." "%1$s -sovelluksella ei ole lupaa sijaintiisi. Voit sallia sen painamalla alla olevaa nappia." "%1$s -sovelluksella ei ole lupaa käyttää mikrofoniasi. Anna lupa, jotta voit nauhoittaa ääniviestejä." + "Tämä voi johtua verkko- tai palvelinongelmista." "Tämä huoneen osoite on jo käytössä, yritä muokata huoneen osoitekenttää tai muuta huoneen nimeä" "Jotkin merkit eivät ole sallittuja. Vain kirjaimet, numerot ja seuraavat symbolit ovat tuettuja ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Joitakin viestejä ei ole lähetetty" @@ -325,7 +358,7 @@ Syy: %1$s." "Etkö voi vahvistaa? Siirry tilillesi ja nollaa identiteettisi." "Peruuta vahvistus ja lähetä" "Voit peruuttaa vahvistuksen ja lähettää tämän viestin silti, tai voit peruuttaa viestin lähettämisen toistaiseksi ja yrittää uudelleen myöhemmin, kun olet vahvistanut käyttäjän %1$s uudelleen." - "Viestiäsi ei lähetetty, koska käyttäjän %1$s vahvistettu identiteetti on muuttunut" + "Viestiäsi ei lähetetty, koska käyttäjän %1$s vahvistettu identiteetti nollattiin" "Lähetä viesti silti" "%1$s käyttää yhtä tai useampaa vahvistamatonta laitetta. Voit lähettää viestin silti tai voit peruuttaa sen toistaiseksi ja yrittää myöhemmin uudelleen, kun %2$s on vahvistanut kaikki laitteensa." "Viestiäsi ei lähetetty, koska %1$s ei ole vahvistanut kaikkia laitteitaan." @@ -333,19 +366,19 @@ Syy: %1$s." "Viestiäsi ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi." "Median käsittely epäonnistui, yritä uudelleen." "Käyttäjän tietojen hakeminen epäonnistui" + "Viesti huoneessa %1$s" "%1$s / %2$s" "Kiinnitetty viesti %1$s" "Viestiä ladataan…" "Näytä kaikki" "Keskustelu" - "Liittymispyyntö lähetetty" "Jaa sijainti" "Jaa sijaintini" "Avaa Apple Mapsissa" "Avaa Google Mapsissa" "Avaa OpenStreetMapissa" "Jaa tämä sijainti" - "Viestiä ei lähetetty, koska käyttäjän %1$s vahvistettu identiteetti on muuttunut." + "Viestiä ei lähetetty, koska käyttäjän %1$s vahvistettu identiteetti nollattiin." "Viestiä ei lähetetty, koska %1$s ei ole vahvistanut kaikkia laitteitaan." "Viestiä ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi." "Sijainti" 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 6d3c873dce1..d1d11775340 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -1,22 +1,27 @@ + "Ajouter une réaction: %1$s" + "Avatar" "Supprimer" "%1$d chiffre saisi" "%1$d chiffres saisis" "Masquer le mot de passe" + "Rejoindre l’appel" "Retourner à la fin de la conversation" "Mentions uniquement" "En sourdine" + "Avatar de l’autre utilisateur" "Page %1$d" "Pause" + "Message vocal, durée: %1$s, position actuelle: %2$s" "Code PIN" "Lecture" "Sondage" "Sondage terminé" "Réagir avec %1$s" - "Réagir avec d’autres emojis" + "Réagir avec d’autres émojis" "Lu par %1$s et %2$s" "Lu par %1$s et %2$d autre" @@ -24,14 +29,20 @@ "Lu par %1$s" "Taper pour voir toute la liste" + "Supprimer la réaction avec %1$s" "Supprimer la réaction avec %1$s" + "Avatar du salon" "Envoyer des fichiers" "Afficher le mot de passe" "Démarrer un appel" + "Avatar de l’utilisateur" "Menu utilisateur" + "Voir l’avatar" "Afficher les détails" + "Message vocal, durée: %1$s" "Enregistrer un message vocal." "Arrêter l’enregistrement" + "Votre avatar" "Accepter" "Ajouter une légende" "Ajouter à la discussion" @@ -56,9 +67,11 @@ "Désactiver" "Désactiver le compte" "Refuser" + "Refuser et bloquer" "Supprimer le sondage" "Désactiver" "Annuler" + "Ignorer" "Terminé" "Modifier" "Modifier la légende" @@ -88,6 +101,7 @@ "Non" "Pas maintenant" "OK" + "Ouvrir le menu contextuel" "Ouvrir les paramètres" "Ouvrir avec" "Épingler" @@ -100,8 +114,11 @@ "Supprimer le message" "Répondre" "Répondre dans le fil de discussion" + "Signaler" "Signaler un problème" "Signaler le contenu" + "Signaler la discussion" + "Signaler le salon" "Réinitialiser" "Réinitialiser l’identité" "Réessayer" @@ -109,7 +126,9 @@ "Enregistrer" "Rechercher" "Envoyer" + "Envoyer un message modifié" "Envoyer un message" + "Envoyer un message vocal" "Partager" "Partager le lien" "Afficher" @@ -125,14 +144,18 @@ "Appuyez pour afficher les options" "Essayer à nouveau" "Désépingler" + "Voir" "Voir dans la discussion" "Afficher la source" "Oui" "Oui, réessayez" + "Votre serveur prend désormais en charge un nouveau protocole plus rapide. Déconnectez-vous, puis reconnectez-vous pour effectuer la mise à niveau dès maintenant. En le faisant tout de suite, vous éviterez une déconnexion forcée lorsque l’ancien protocole sera supprimé." + "Mise à niveau disponible" "À propos" "Politique d’utilisation acceptable" "Ajout d’une légende" "Paramètres avancés" + "une image" "Statistiques d’utilisation" "Apparence" "Audio" @@ -143,7 +166,9 @@ "Copié dans le presse-papiers" "Droits d’auteur" "Création du salon…" + "Demande annulée" "Quitter le salon" + "Invitation refusée" "Sombre" "Erreur de déchiffrement" "Options pour les développeurs" @@ -156,6 +181,7 @@ "Édition" "Modification de la légende" "* %1$s %2$s" + "Fichier vide" "Chiffrement" "Chiffrement activé" "Saisissez votre code PIN" @@ -180,9 +206,14 @@ Raison : %1$s." "Cet identifiant Matrix est introuvable, il est donc possible que l’invitation ne soit pas reçue." "Quitter le salon…" "Clair" + "Ligne copiée dans le presse-papiers" "Lien copié dans le presse-papiers" "Chargement…" "Chargement…" + + "%d autre" + "%d autres" + "%1$d membre" "%1$d membres" @@ -193,8 +224,10 @@ Raison : %1$s." "Message supprimé" "Moderne" "Mettre en sourdine" + "%1$s (%2$s)" "Aucun résultat" "Salon sans nom" + "Non chiffré" "Hors ligne" "Licences open source" "ou" @@ -203,6 +236,7 @@ Raison : %1$s." "Permalien" "Autorisation" "Épinglé" + "Veuillez vérifier votre connexion internet" "Veuillez patienter…" "Êtes-vous sûr de vouloir mettre fin à ce sondage ?" "Sondage : %1$s" @@ -215,8 +249,12 @@ Raison : %1$s." "Politique de confidentialité" "Réaction" "Réactions" + "Raison" "Clé de récupération" "Actualisation…" + + "%1$d réponses" + "En réponse à %1$s" "Signaler un problème" "Remonter un problème" @@ -236,12 +274,14 @@ Raison : %1$s." "Envoi en cours…" "Échec de l’envoi" "Envoyé" + ". " "Serveur non pris en charge" "URL du serveur" "Paramètres" "Position partagée" "Déconnexion" "Une erreur s’est produite" + "Nous avons rencontré un problème. Veuillez réessayer." "Création de la discussion…" "Autocollant" "Succès" @@ -256,7 +296,7 @@ Raison : %1$s." "Échec de déchiffrement" "Envoyé depuis un appareil non sécurisé" "Vous ne pouvez pas voir ce message" - "L’identité vérifiée de l’expéditeur a changé" + "L’identité vérifiée de l’expéditeur a été réinitialisée" "Les invitations n’ont pas pu être envoyées à un ou plusieurs utilisateurs." "Impossible d’envoyer une ou plusieurs invitations" "Déverrouillage" @@ -270,16 +310,24 @@ Raison : %1$s." "Vérifié(e)" "Vérifier la session" "Vérifier l’identité" + "Vérifier l’utilisateur" "Vidéo" "Message vocal" "En attente…" "En attente de la clé de déchiffrement" "Vous" - "L’identité de %1$s semble avoir changé. %2$s" - "L’identité de %1$s %2$s semble avoir changé. %3$s" + "L’identité de %1$s a été réinitialisée. %2$s" + "L’identité de %1$s %2$s a été réinitialisée. %3$s" "(%1$s)" - "L’identité vérifiée de %1$s %2$s a changé. %3$s" + "L’identité de %1$s a été réinitialisée." + "L’identité de %1$s %2$s a été réinitialisée. %3$s" "Révoquer la vérification" + "Le lien \"%1$s\" vous redirige vers un autre site \"%2$s\". + +Êtes-vous sûr de vouloir continuer ?" + "Veuillez vérifier ce lien" + "Salon signalé" + "Salon quitté et signalé" "Confirmation" "Erreur" "Succès" @@ -292,14 +340,17 @@ Raison : %1$s." "Échec du chargement des messages" "%1$s n’a pas pu accéder à votre position. Veuillez réessayer ultérieurement." "Échec lors de l’envoi du message vocal." + "Ce salon n’existe plus ou l’invitation n’est plus valable." "Message introuvable" "%1$s n’est pas autorisé à accéder à votre position. Vous pouvez activer l’accès dans les Paramètres." "%1$s n’est pas autorisé à accéder à votre position. Activez l’accès ci-dessous." "%1$s n’a pas l’autorisation d’utiliser le microphone. Autorisez l’utilisation pour enregistrer un message vocal." + "Cela peut être dû à des problèmes de réseau ou de serveur." "Cette adresse de salon existe déjà, veuillez essayer de modifier le champ d’adresse de salon ou de modifier le nom du salon" "Certains caractères ne sont pas autorisés. Seuls les lettres, les chiffres et les symboles suivants sont utilisables ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Certains messages n’ont pas été envoyés" "Désolé, une erreur s’est produite" + "L’expéditeur de ce message ne correspond pas au propriétaire de l’appareil qui l’a envoyé." "L’authenticité de ce message chiffré ne peut être garantie sur cet appareil." "Chiffré par un utilisateur précédemment vérifié." "Non chiffré." @@ -310,6 +361,10 @@ Raison : %1$s." "Salut, parle-moi sur %1$s : %2$s" "%1$s Android" "Rageshake pour signaler un problème" + "%1$s: %2$s" + "Options" + "Supprimer %1$s" + "Paramètres" "Échec de la sélection du média, veuillez réessayer." "Les légendes peuvent ne pas être visibles pour les utilisateurs d’anciennes applications." "Échec du traitement des médias à télécharger, veuillez réessayer." @@ -325,7 +380,7 @@ Raison : %1$s." "Vous ne pouvez pas confirmer ? Accédez à votre compte pour réinitialiser votre identité." "Révoquer la verification et envoyer" "Vous pouvez révoquer la verification et envoyer ce message, ou vous pouvez annuler pour l’instant et réessayer plus tard après avoir vérifié à nouveau %1$s." - "Votre message n’a pas été envoyé car l’identité vérifiée de %1$s a changé" + "Votre message n’a pas été envoyé car l’identité vérifiée de %1$s a été réinitialisée" "Envoyer le message quand même" "%1$s utilise un ou plusieurs appareils non vérifiés. Vous pouvez quand même envoyer le message, ou vous pouvez annuler pour l’instant et réessayer plus tard après que %2$s vérifie tous ses appareils." "Votre message n’a pas été envoyé car %1$s n’a pas vérifié tous ses appareils" @@ -333,19 +388,22 @@ Raison : %1$s." "Votre message n’a pas été envoyé car vous n’avez pas vérifié tous vos appareils" "Échec du traitement des médias à télécharger, veuillez réessayer." "Impossible de récupérer les détails de l’utilisateur" + "Message dans %1$s" + "Développer" + "Réduire" + "Vous êtes déjà dans ce salon!" "%1$s sur %2$s" "%1$s Messages épinglés" "Chargement du message…" "Voir tout" "Discussion" - "Demande d’adhésion envoyée" "Partage de position" "Partager ma position" "Ouvrir dans Apple Maps" "Ouvrir dans Google Maps" "Ouvrir dans OpenStreetMap" "Partager cette position" - "Le message n’a pas été envoyé car l’identité vérifiée de %1$s a changé." + "Le message n’a pas été envoyé car l’identité vérifiée de %1$s a été réinitialisée." "Le message n’a pas été envoyé car %1$s n’a pas vérifié tous ses appareils." "Message non envoyé car vous n’avez pas vérifié tous vos appareils." "Position" 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 a74d0a2ed08..b0d8a37de94 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -1,16 +1,20 @@ + "Reakció hozzáadása: %1$s" + "Profilkép" "Törlés" "%1$d megadott számjegy" "%1$d megadott számjegy" "Jelszó elrejtése" + "Csatlakozás a híváshoz" "Ugrás az aljára" "Csak megemlítések" "Némítva" "%1$d. oldal" "Szüneteltetés" + "Hangüzenet, időtartam:%1$s, jelenlegi pozíció:%2$s" "PIN-mező" "Lejátszás" "Szavazás" @@ -24,12 +28,15 @@ "Olvasta: %1$s" "Koppintson az összes megjelenítéséhez" + "Reakció eltávolítása: %1$s" "Reakció eltávolítása: %1$s" "Fájlküldés" "Jelszó megjelenítése" "Hanghívás indítása" "Felhasználói menü" + "Profilkép megtekintése" "Részletek megtekintése" + "Hangüzenet, időtartam: %1$s" "Hangüzenet felvétele." "Rögzítés leállítása" "Elfogadás" @@ -56,9 +63,11 @@ "Deaktiválás" "Fiók deaktiválása" "Elutasítás" + "Elutasítás és letiltás" "Szavazás törlése" "Letiltás" "Elvetés" + "Eltüntetés" "Kész" "Szerkesztés" "Felirat szerkesztése" @@ -100,8 +109,11 @@ "Üzenet eltávolítása" "Válasz" "Válasz az üzenetszálban" + "Jelentés" "Hiba jelentése" "Tartalom jelentése" + "Beszélgetés jelentése" + "Szoba jelentése" "Visszaállítás" "Személyazonosság visszaállítása" "Újra" @@ -129,10 +141,13 @@ "Forrás megtekintése" "Igen" "Igen, újrapróbálkozás" + "A kiszolgálója mostantól egy új, gyorsabb protokollt támogat. A frissítéshez jelentkezzen ki, majd jelentkezzen be újra. Ha ezt most megteszi, elkerülheti a kényszerített kijelentkeztetést a régi protokollt eltávolításakor." + "Frissítés érhető el" "Névjegy" "Elfogadható használatra vonatkozó szabályzat" "Felirat hozzáadása" "Speciális beállítások" + "egy kép" "Elemzések" "Megjelenítés" "Hang" @@ -143,7 +158,9 @@ "A vágólapra másolva" "Szerzői jogok" "Szoba létrehozása…" + "Kérés megszakítva" "Elhagyta a szobát" + "Meghívás elutasítva" "Sötét" "Visszafejtési hiba" "Fejlesztői beállítások" @@ -156,6 +173,7 @@ "Szerkesztés" "Felirat szerkesztése" "* %1$s %2$s" + "Üres fájl" "Titkosítás" "Titkosítás engedélyezve" "Adja meg a PIN-kódját" @@ -180,9 +198,14 @@ 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" "Világos" + "A sor a vágólapra másolva" "Hivatkozás a vágólapra másolva" "Betöltés…" "Továbbiak betöltése…" + + "%d további felhasználó" + "%d további felhasználó" + "%1$d tag" "%1$d tag" @@ -193,8 +216,10 @@ Ok: %1$s." "Üzenet eltávolítva" "Modern" "Némítás" + "%1$s (%2$s)" "Nincs találat" "Nincs szobanév" + "Nincs titkosítva" "Kapcsolat nélkül" "Nyílt forráskódú licencek" "vagy" @@ -203,6 +228,7 @@ Ok: %1$s." "Állandó hivatkozás" "Engedély" "Kitűzve" + "Ellenőrizze az internetkapcsolatát" "Kis türelmet…" "Biztos, hogy befejezi ezt a szavazást?" "Szavazás: %1$s" @@ -215,8 +241,12 @@ Ok: %1$s." "Adatvédelmi nyilatkozat" "Reakció" "Reakciók" + "Ok" "Helyreállítási kulcs" "Frissítés…" + + "%1$d válasz" + "Válasz %1$s számára" "Hiba jelentése" "Probléma jelentése" @@ -236,12 +266,14 @@ Ok: %1$s." "Küldés…" "A küldés sikertelen" "Elküldve" + ". " "A kiszolgáló nem támogatott" "Kiszolgáló webcíme" "Beállítások" "Megosztott tartózkodási hely" "Kijelentkezés" "Valamilyen hiba történt" + "Problémába ütköztünk. Próbálja újra." "Csevegés megkezdése…" "Matrica" "Sikeres" @@ -270,16 +302,24 @@ Ok: %1$s." "Ellenőrizve" "Eszköz ellenőrzése" "Személyazonosság ellenőrzése" + "Felhasználó ellenőrzése" "Videó" "Hangüzenet" "Várakozás…" "Várakozás a visszafejtési kulcsra" "Ön" - "Úgy tűnik, hogy %1$s személyazonossága megváltozott. %2$s" - "Úgy tűnik, hogy %1$s %2$s személyazonossága megváltozott. %3$s" + "%1$s személyazonossága megváltozott. %2$s" + "%1$s (%2$s) személyazonossága megváltozott. %3$s" "(%1$s)" - "%1$s, %2$s ellenőrzött személyazonossága megváltozott. %3$s" + "%1$s személyazonossága megváltozott." + "%1$s (%2$s) ellenőrzött személyazonossága megváltozott. %3$s" "Ellenőrzés visszavonása" + "A(z) %1$s hivatkozás átviszi egy másik webhelyre: %2$s + +Biztos, hogy folytatja?" + "Ellenőrizze újra ezt a hivatkozást" + "Szoba jelentve" + "Jelentve, és a szoba elhagyva" "Megerősítés" "Hiba" "Sikeres" @@ -292,14 +332,17 @@ Ok: %1$s." "Nem sikerült betölteni az üzeneteket" "Az %1$s nem tudta elérni a tartózkodási helyét. Próbálja újra később." "Nem sikerült feltölteni a hangüzenetét." + "A szoba már nem létezik, vagy a meghívó már nem érvényes." "Az üzenet nem található" "Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyéhez. Ezt a beállításokban engedélyezheti." "Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyéhez. Engedélyezze alább az elérését." "Az %1$snek nincs engedélye, hogy hozzáférjen a mikrofonhoz. Engedélyezze, hogy tudjon hangüzenetet felvenni." + "Ennek oka hálózati vagy kiszolgálóprobléma lehet." "Ez a szobacím már létezik. Próbálja meg szerkeszteni a szobacím mezőt, vagy módosítsa a szoba nevét." "Egyes karakterek nem engedélyezettek. Csak a betűk, a számjegyek és a következő szimbólumok támogatottak: $ & \'() * +/; =? @ [] - . _" "Néhány üzenet nem került elküldésre" "Elnézést, hiba történt" + "Az esemény feladója nem egyezik az eseményt küldő eszköz tulajdonosával." "A titkosított üzenetek valódiságát ezen az eszközön nem lehet garantálni." "Egy korábban ellenőrzött felhasználó által titkosítva." "Nincs titkosítva." @@ -310,6 +353,10 @@ Ok: %1$s." "Beszélgessünk itt: %1$s, %2$s" "%1$s Android" "Az eszköz rázása a hibajelentéshez" + "%1$s: %2$s" + "Lehetőségek" + "Eltávolítás: %1$s" + "Beállítások" "Nem sikerült kiválasztani a médiát, próbálja újra." "Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára." "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra." @@ -325,7 +372,7 @@ Ok: %1$s." "Nem tudja megerősíteni? Ugorjon a fiókjához, és állítsa vissza a személyazonosságát." "Ellenőrzés visszavonása és elküldés" "Visszavonhatja az ellenőrzést, és ennek ellenére elküldheti ezt az üzenetet, vagy egyelőre törölheti, és %1$s újbóli ellenőrzése után újra megpróbálhatja." - "Az üzenete nem lett elküldve, mert %1$s ellenőrzött személyazonossága megváltozott." + "Az üzenete nem lett elküldve, mert %1$s személyazonossága megváltozott." "Üzenet elküldése mindenképp" "%1$s egy vagy több ellenőrizetlen eszközt használ. Így is elküldheti az üzenetet, vagy megszakíthatja most, és megpróbálhatja újra, miután %2$s ellenőrizte az összes eszközét." "Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét" @@ -333,12 +380,15 @@ Ok: %1$s." "Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte" "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra." "Nem sikerült letölteni a felhasználói adatokat" + "Üzenet a következőben: %1$s" + "Kibontás" + "Csökkentés" + "Már ezt a szobát nézi!" "%1$s / %2$s" "%1$s kitűzött üzenet" "Üzenet betöltése…" "Összes megtekintése" "Csevegés" - "Csatlakozási kérés elküldve" "Hely megosztása" "Saját hely megosztása" "Megnyitás az Apple Mapsben" 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 5ac935b1c56..b77370f81bd 100644 --- a/libraries/ui-strings/src/main/res/values-in/translations.xml +++ b/libraries/ui-strings/src/main/res/values-in/translations.xml @@ -1,15 +1,20 @@ + "Tambahkan reaksi: %1$s" + "Avatar" "Hapus" "%1$d digit dimasukkan" "Sembunyikan kata sandi" + "Bergabung dalam panggilan" "Lompat ke bawah" "Hanya sebutan" "Dibisukan" + "Avatar pengguna lain" "Halaman %1$d" "Jeda" + "Pesan suara, durasi: %1$s, posisi saat ini: %2$s" "Kolom PIN" "Putar" "Pemungutan suara" @@ -22,13 +27,20 @@ "Dibaca oleh %1$s" "Ketuk untuk melihat semua" + "Hapus reaksi dengan %1$s" "Hapus reaksi dengan %1$s" + "Avatar ruangan" "Kirim berkas" "Tampilkan kata sandi" "Mulai panggilan" + "Avatar pengguna" "Menu pengguna" + "Lihat avatar" + "Lihat detail" + "Pesan suara, durasi: %1$s" "Rekam pesan suara." "Berhenti merekam" + "Avatar Anda" "Terima" "Tambahkan keterangan" "Tambahkan ke lini masa" @@ -53,9 +65,11 @@ "Nonaktifkan" "Nonaktifkan akun" "Tolak" + "Tolak dan blokir" "Hapus pemungutan suara" "Nonaktifkan" "Abaikan" + "Abaikan" "Selesai" "Sunting" "Sunting keterangan" @@ -97,8 +111,11 @@ "Hapus pesan" "Balas" "Balas dalam utas" + "Laporkan" "Laporkan kutu" "Laporkan Konten" + "Laporkan percakapan" + "Laporkan ruangan" "Atur ulang" "Atur ulang identitas" "Coba lagi" @@ -122,13 +139,18 @@ "Ketuk untuk opsi" "Coba lagi" "Lepaskan sematan" + "Lihat" "Lihat di lini masa" "Tampilkan sumber" "Ya" + "Ya, coba lagi" + "Server Anda kini mendukung protokol baru yang lebih cepat. Keluar dan masuk lagi untuk memperbarui sekarang. Melakukan hal ini sekarang akan membantu Anda menghindari keluar paksa saat protokol lama dihapus nantinya." + "Peningkatan tersedia" "Tentang" "Kebijakan penggunaan wajar" "Menambahkan keterangan" "Pengaturan tingkat lanjut" + "sebuah gambar" "Analitik" "Penampilan" "Audio" @@ -139,18 +161,22 @@ "Disalin ke papan klip" "Hak cipta" "Membuat ruangan…" + "Permintaan dibatalkan" "Keluar dari ruangan" + "Undangan ditolak" "Gelap" "Kesalahan dekripsi" "Opsi pengembang" "ID Perangkat" "Obrolan langsung" "Jangan tampilkan ini lagi" + "Pengunduhan gagal" "Mengunduh" "(disunting)" "Penyuntingan" "Menyunting keterangan" "* %1$s %2$s" + "Berkas kosong" "Enkripsi" "Enkripsi diaktifkan" "Masukkan PIN Anda" @@ -163,6 +189,8 @@ Alasan: %1$s." "Favorit" "Difavoritkan" "Berkas" + "Berkas dihapus" + "Berkas disimpan" "Berkas disimpan ke Unduhan" "Teruskan pesan" "Sering digunakan" @@ -173,8 +201,13 @@ Alasan: %1$s." "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…" + "Memuat lebih banyak…" + + "%d lainnya" + "%1$d anggota" @@ -184,8 +217,10 @@ Alasan: %1$s." "Pesan dihapus" "Modern" "Bisukan" + "%1$s (%2$s)" "Tidak ada hasil" "Tidak ada nama ruangan" + "Tidak terenkripsi" "Luring" "Lisensi sumber terbuka" "atau" @@ -194,6 +229,7 @@ Alasan: %1$s." "Tautan Permanen" "Perizinan" "Disematkan" + "Silakan periksa koneksi internet Anda" "Mohon tunggu…" "Apakah Anda yakin ingin mengakhiri pemungutan suara ini?" "Pemungutan suara: %1$s" @@ -205,8 +241,12 @@ Alasan: %1$s." "Kebijakan privasi" "Reaksi" "Reaksi" + "Alasan" "Kunci pemulihan" "Menyegarkan…" + + "%1$d balasan" + "Membalas %1$s" "Laporkan kutu" "Laporkan masalah" @@ -226,12 +266,14 @@ Alasan: %1$s." "Mengirim…" "Pengiriman gagal" "Terkirim" + ". " "Server tidak didukung" "URL Server" "Pengaturan" "Lokasi terbagi" "Mengeluarkan dari akun" "Ada yang salah" + "Kami mengalami masalah. Silakan coba lagi." "Memulai obrolan…" "Stiker" "Berhasil" @@ -246,7 +288,7 @@ Alasan: %1$s." "Tidak dapat mendekripsi" "Dikirim dari perangkat yang tidak aman" "Anda tidak memiliki akses ke pesan ini" - "Identitas terverifikasi pengirim telah berubah" + "Identitas terverifikasi pengirim diatur ulang" "Undangan tidak dapat dikirim ke satu atau beberapa pengguna." "Tidak dapat mengirim undangan" "Buka kunci" @@ -260,14 +302,24 @@ Alasan: %1$s." "Terverifikasi" "Verifikasi perangkat" "Verifikasi identitas" + "Verifikasi pengguna" "Video" "Pesan suara" "Menunggu…" "Menunggu pesan ini" "Anda" - "Identitas %1$s tampaknya telah berubah. %2$s" - "Identitas %1$s yang %2$s tampaknya telah berubah. %3$s" + "Identitas %1$s telah diatur ulang. %2$s" + "Identitas %2$s %1$s telah diatur ulang. %3$s" "(%1$s)" + "Identitasnya %1$s sudah diatur ulang." + "Identitas %2$s %1$s telah diatur ulang. %3$s" + "Tolak verifikasi" + "Tautan %1$s membawa Anda ke situs lain %2$s + +Apakah Anda yakin ingin melanjutkan?" + "Periksa kembali tautan ini" + "Ruangan dilaporkan" + "Dilaporkan dan ruangan ditinggalkan" "Konfirmasi" "Eror" "Berhasil" @@ -280,14 +332,17 @@ Alasan: %1$s." "Gagal memuat pesan" "%1$s tidak dapat mengakses lokasi Anda. Silakan coba lagi nanti." "Gagal mengunggah pesan suara Anda." + "Ruangan tidak ada lagi atau undangan tidak lagi valid." "Pesan tidak ditemukan" "%1$s tidak memiliki izin untuk mengakses lokasi Anda. Anda dapat mengaktifkan akses di Pengaturan." "%1$s tidak memiliki izin untuk mengakses lokasi Anda. Aktifkan akses di bawah ini." "%1$s tidak memiliki izin untuk mengakses mikrofon. Aktifkan akses untuk merekam pesan suara." + "Hal ini mungkin disebabkan oleh masalah jaringan atau server." "Alamat ruangan sudah ada, silakan coba sunting kolom alamat ruangan atau ubah nama ruangan" "Beberapa karakter tidak diperbolehkan. Hanya huruf, angka, dan simbol berikut didukung ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Beberapa pesan belum terkirim" "Maaf, terjadi kesalahan" + "Pengirim peristiwa tidak cocok dengan pemilik perangkat yang mengirimnya." "Keaslian pesan terenkripsi ini tidak dapat dijamin pada perangkat ini." "Dienkripsi oleh pengguna yang telah diverifikasi sebelumnya." "Tidak dienkripsi." @@ -298,6 +353,10 @@ Alasan: %1$s." "Hai, bicaralah dengan saya di %1$s: %2$s" "%1$s Android" "Rageshake untuk melaporkan kutu" + "%1$s: %2$s" + "Opsi" + "Hapus %1$s" + "Pengaturan" "Gagal memilih media, silakan coba lagi." "Keterangan mungkin tidak terlihat oleh orang yang menggunakan aplikasi lama." "Gagal memproses media untuk diunggah, silakan coba lagi." @@ -312,7 +371,7 @@ Alasan: %1$s." "Tidak dapat mengonfirmasi? Buka akun Anda untuk mengatur ulang identitas Anda." "Tarik verifikasi dan kirim" "Anda dapat menarik verifikasi dan tetap mengirim pesan ini, atau Anda dapat membatalkan untuk saat ini dan mencoba lagi nanti setelah memverifikasi ulang %1$s." - "Pesan Anda tidak terkirim karena identitas terverifikasi %1$s telah berubah" + "Pesan Anda tidak terkirim karena identitas terverifikasi %1$s telah diatur ulang" "Kirim pesan saja" "%1$s menggunakan satu atau beberapa perangkat yang belum diverifikasi. Anda tetap dapat mengirim pesan, atau Anda dapat membatalkan untuk saat ini dan mencoba lagi nanti setelah %2$s telah memverifikasi semua perangkat mereka." "Pesan Anda tidak terkirim karena %1$s belum memverifikasi semua perangkat" @@ -320,25 +379,29 @@ Alasan: %1$s." "Pesan Anda tidak terkirim karena Anda belum memverifikasi satu atau beberapa perangkat Anda" "Gagal memproses media untuk diunggah, silakan coba lagi." "Tidak dapat mengambil detail pengguna" + "Pesan dalam %1$s" + "Buka" + "Kurangi" + "Sudah melihat ruangan ini!" "%1$s dari %2$s" "%1$s Pesan yang disematkan" "Memuat pesan…" "Lihat Semua" "Obrolan" - "Permintaan untuk bergabung dikirim" "Bagikan lokasi" "Bagikan lokasi saya" "Buka di Apple Maps" "Buka di Google Maps" "Buka di OpenStreetMap" "Bagikan lokasi ini" - "Pesan tidak terkirim karena identitas terverifikasi %1$s telah berubah." + "Pesan tidak terkirim karena identitas terverifikasi %1$s telah diatur ulang." "Pesan tidak terkirim karena %1$s belum memverifikasi semua perangkat." "Pesan tidak terkirim karena Anda belum memverifikasi satu atau beberapa perangkat Anda." "Lokasi" "Versi: %1$s (%2$s)" "id" "Riwayat pesan tidak tersedia di perangkat ini" + "Anda harus memverifikasi perangkat ini untuk mengakses riwayat pesan" "Anda tidak memiliki akses ke pesan ini" "Tidak dapat mendekripsi pesan" "Pesan ini diblokir karena Anda tidak memverifikasi perangkat Anda atau karena pengirim perlu memverifikasi identitas Anda." 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 b6803448ca1..d0f28e13191 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -1,16 +1,20 @@ + "Aggiungi reazione: %1$s" + "Avatar" "Elimina" "%1$d cifra inserita" "%1$d cifre inserite" "Nascondi password" + "Entra in chiamata" "Vai alla fine" "Solo menzioni" "Silenziato" "Pagina %1$d" "Pausa" + "Messaggio vocale, durata: %1$s, posizione attuale: %2$s" "Campo del PIN" "Riproduci" "Sondaggio" @@ -24,12 +28,15 @@ "Visualizzato da %1$s" "Tocca per mostrare tutti" - "Rimuovi la reazione con %1$s" + "Rimuovi la reazione con %1$s" + "Rimuovere la reazione con %1$s" "Invia file" "Mostra password" "Avvia una chiamata" "Menu utente" + "Visualizza avatar" "Visualizza dettagli" + "Messaggio vocale, durata: %1$s" "Registra un messaggio vocale." "Ferma la registrazione" "Accetta" @@ -56,9 +63,11 @@ "Disattiva" "Disattiva account" "Rifiuta" + "Rifiuta e blocca" "Elimina sondaggio" "Disabilita" "Annulla" + "Chiudi" "Fine" "Modifica" "Modifica didascalia" @@ -100,8 +109,11 @@ "Rimuovi messaggio" "Rispondi" "Rispondi nella discussione" + "Segnala" "Segnala un problema" "Segnala contenuto" + "Segnala una conversazione" + "Segnala stanza" "Reimposta" "Reimposta identità" "Riprova" @@ -129,10 +141,13 @@ "Vedi codice sorgente" "Sì" "Sì, riprova" + "Il tuo server ora supporta un nuovo protocollo più veloce. Esci e rientra per effettuare l\'aggiornamento. Se lo fai ora, eviterai una disconnessione forzata quando il vecchio protocollo verrà rimosso in seguito." + "Aggiornamento disponibile" "Informazioni" "Regole sull\'utilizzo consentito" "Aggiunta didascalia" "Impostazioni avanzate" + "un\'immagine" "Statistiche di utilizzo" "Aspetto" "Audio" @@ -143,7 +158,9 @@ "Copiato negli appunti" "Copyright" "Creazione stanza…" + "Richiesta annullata" "Hai lasciato la stanza" + "Invito rifiutato" "Scuro" "Errore di decrittazione" "Opzioni sviluppatore" @@ -156,6 +173,7 @@ "Modifica in corso" "Modifica didascalia" "* %1$s %2$s" + "File vuoto" "Crittografia" "Crittografia abilitata" "Inserisci il PIN" @@ -180,9 +198,14 @@ Motivo:. %1$s" "Questo ID Matrix non può essere trovato, quindi l\'invito potrebbe non essere ricevuto." "Lascio la stanza" "Chiaro" + "Riga copiata negli appunti" "Collegamento copiato negli appunti" "Caricamento…" "Caricamento in corso…" + + "%d altro" + "altri %d" + "%1$d membro" "%1$d membri" @@ -193,8 +216,10 @@ Motivo:. %1$s" "Messaggio rimosso" "Moderno" "Silenzia" + "%1$s (%2$s)" "Nessun risultato" "Nessun nome della stanza" + "Non cifrata" "Non in linea" "Licenze open source" "o" @@ -203,6 +228,7 @@ Motivo:. %1$s" "Collegamento permanente" "Autorizzazione" "Fissato" + "Per favore controlla la tua connessione Internet" "Attendere prego…" "Vuoi davvero terminare questo sondaggio?" "Sondaggio: %1$s" @@ -215,8 +241,13 @@ Motivo:. %1$s" "Informativa sulla privacy" "Reazione" "Reazioni" + "Motivo" "Chiave di recupero" "Aggiornamento…" + + "%1$d risposta" + "%1$d risposte" + "Risposta a %1$s" "Segnala un problema" "Segnala un problema" @@ -236,12 +267,14 @@ Motivo:. %1$s" "Invio in corso…" "Invio fallito" "Inviato" + "." "Server non supportato" "URL del server" "Impostazioni" "Posizione condivisa" "Disconnessione" "Qualcosa è andato storto" + "Abbiamo riscontrato un problema. Per favore riprova." "Avvio della conversazione…" "Adesivo" "Operazione riuscita" @@ -256,7 +289,7 @@ Motivo:. %1$s" "Impossibile decrittografare" "Inviato da un dispositivo non sicuro" "Non hai accesso a questo messaggio" - "L\'identità verificata del mittente è cambiata" + "L\'identità verificata del mittente è stata reimpostata" "Non è stato possibile spedire inviti a uno o più utenti." "Impossibile inviare inviti" "Sblocca" @@ -270,16 +303,24 @@ Motivo:. %1$s" "Verificato" "Verifica dispositivo" "Verifica l\'identità" + "Verifica utente" "Video" "Messaggio vocale" "In attesa…" "In attesa del messaggio" "Tu" - "L\'identità di %1$s sembra essere cambiata. %2$s" - "L\'identità di %1$s %2$s sembra essere cambiata. %3$s" + "L\'identità di %1$s è stata reimpostata. %2$s" + "L\'identità %2$s di %1$s sembra essere cambiata. %3$s" "(%1$s)" - "L\'identità %2$s verificata di %1$s è cambiata. %3$s" + "L\'identità di %1$s è stata reimpostata." + "L\'identità %2$s di %1$s è stata reimpostata. %3$s" "Ritira verifica" + "Il link %1$s ti porta ad un altro sito %2$s + +Sei sicuro di voler continuare?" + "Ricontrolla questo link" + "Stanza segnalata" + "Stanza segnalata ed abbandonata" "Conferma" "Errore" "Operazione riuscita" @@ -292,14 +333,17 @@ Motivo:. %1$s" "Caricamento dei messaggi non riuscito" "%1$s non è riuscito ad accedere alla tua posizione. Riprova più tardi." "Invio del messaggio vocale fallito." + "La stanza non esiste più o l\'invito non è più valido." "Messaggio non trovato" "%1$s non ha l\'autorizzazione di accedere alla tua posizione. Puoi attivare l\'accesso nelle impostazioni." "%1$s non ha l\'autorizzazione per accedere alla tua posizione. Attiva l\'accesso di seguito." "%1$s non ha l\'autorizzazione di accedere al microfono. Attiva l\'accesso per registrare un messaggio vocale." + "Ciò può essere dovuto a problemi di rete o server." "L\'indirizzo di questa stanza esiste già. Prova a modificare il campo dell\'indirizzo o a cambiare il nome della stanza" "Alcuni caratteri non sono consentiti. Sono supportate solo lettere, cifre e i seguenti simboli ! $ & \'() * +/; =? @ [] - . _" "Alcuni messaggi non sono stati inviati" "Siamo spiacenti, si è verificato un errore" + "Il mittente dell\'evento non corrisponde al proprietario del dispositivo che lo ha inviato." "L\'autenticità di questo messaggio cifrato non può essere garantita su questo dispositivo." "Cifrato da un utente precedentemente verificato." "Non cifrato." @@ -310,6 +354,10 @@ Motivo:. %1$s" "Ehi, parliamo su %1$s: %2$s" "%1$s Android" "Scuoti per segnalare un problema" + "%1$s: %2$s" + "Risposte" + "Rimuovi %1$s" + "Impostazioni" "Selezione del file multimediale fallita, riprova." "Le didascalie potrebbero non essere visibili agli utenti di app meno recenti." "Elaborazione del file multimediale da caricare fallita, riprova." @@ -325,7 +373,7 @@ Motivo:. %1$s" "Non riesci a confermare? Vai al tuo account per ripristinare la tua identità." "Ritira la verifica e invia" "Puoi ritirare la tua verifica e inviare comunque questo messaggio, oppure annullarlo per ora e riprovare più tardi dopo aver riverificato %1$s." - "Il tuo messaggio non è stato inviato perché l\'identità verificata di %1$s è cambiata." + "Il tuo messaggio non è stato inviato perché l\'identità verificata di %1$s è stata reimpostata." "Invia comunque il messaggio" "%1$s sta usando uno o più dispositivi non verificati. Puoi inviare il messaggio in ogni caso, oppure annullarlo e riprovare più tardi quando %2$s avrà verificato tutti i suoi dispositivi." "Il tuo messaggio non è stato inviato perché %1$s non ha verificato tutti i dispositivi." @@ -333,19 +381,22 @@ Motivo:. %1$s" "Il tuo messaggio non è stato inviato perché non hai verificato uno o più dispositivi." "Elaborazione del file multimediale da caricare fallita, riprova." "Impossibile recuperare i dettagli dell\'utente" + "Messaggio in %1$s" + "Espandi" + "Riduci" + "Stai già visualizzando questa stanza!" "%1$s di %2$s" "%1$s Messaggi fissati" "Caricamento messaggio…" "Mostra tutti" "Conversazione" - "Richiesta di accesso inviata" "Condividi posizione" "Condividi la mia posizione" "Apri in Apple Maps" "Apri in Google Maps" "Apri in OpenStreetMap" "Condividi questa posizione" - "Messaggio non inviato perché l\'identità verificata di %1$s è cambiata." + "Messaggio non inviato perché l\'identità verificata di %1$s è stata reimpostata." "Messaggio non inviato perché %1$s non ha verificato tutti i dispositivi." "Messaggio non inviato perché non hai verificato uno o più dispositivi." "Posizione" 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 0df6e83beff..7e3fe1912ac 100644 --- a/libraries/ui-strings/src/main/res/values-ka/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ka/translations.xml @@ -24,11 +24,12 @@ "წაიკითხეს: %s" "შეეხეთ ყველაფრის საჩვენებლად" - "%1$s რეაქციის წაშლა" + "%1$s რეაქციის წაშლა" "ფაილების გაგზავნა" "პაროლის ჩვენება" "დარეკვა" "მომხმარებლის მენიუ" + "დეტალების ნახვა" "ხმოვანი შეტყობინების ჩაწერა." "ჩაწერის შეწყვეტა" "მიღება" @@ -49,6 +50,7 @@ "უარყოფა" "გამოკითხვის წაშლა" "გამორთვა" + "გაუქმება" "მზადაა" "რედაქტირება" "გამოკითხვის რედაქტირება" @@ -57,6 +59,7 @@ "შეიყვანეთ PIN" "დაგავიწყდათ პაროლი?" "გადაგზავნა" + "დაბრუნება" "მოწვევა" "ხალხის მოწვევა" "ადამიანების დამატება %1$s" @@ -65,6 +68,7 @@ "გაწევრიანება" "შეიტყვეთ მეტი" "დატოვება" + "საუბრის დატოვება" "ოთახის დატოვება" "მეტის ჩატვირთვა" "ანგარიშის მართვა" @@ -83,6 +87,7 @@ "პასუხი თემაში" "ხარვეზის შეტყობინება" "კონტენტის რეპორტი" + "განულება" "ხელახლა ცდა" "გაშიფვრის ხელახლა ცდა" "შენახვა" @@ -110,6 +115,7 @@ "ანალიტიკა" "გარეგნობა" "აუდიო" + "დაბლოკილი მომხმარებლები" "ბუშტები" "ჩატის სარეზერვო ასლი" "საავტორო უფლება" @@ -126,6 +132,9 @@ "შეიყვანეთ თქვენი PIN" "შეცდომა" "ყველა" + "ვერ შესრულდა" + "რჩეული" + "რჩეულები" "ფაილი" "ფაილი შენახულია ჩამოტვირთვებში" "შეტყობინების გადაგზავნა" @@ -138,6 +147,10 @@ "ღია" "ბმული კოპირებულია გაცვლის ბუფერში" "იტვირთება…" + + "%d სხვა" + "%d სხვა" + "%1$d წევრი" "%1$d წევრები" @@ -150,6 +163,7 @@ "დადუმება" "შედეგი არ არის" "ხაზგარეშე" + "ან" "პაროლი" "ხალხი" "მუდმივი ბმული" @@ -177,6 +191,8 @@ "ოთახი" "ოთახის სახელი" "მაგ. თქვენი პროექტის სახელი" + "შენახული ცვლილებები" + "შენახვა" "ეკრანის დაბლოკვა" "ვიღაცის ძებნა" "ძიების შედეგები" @@ -219,6 +235,8 @@ "შეცდომა" "წარმატება" "გაფრთხილება" + "თქვენი ცვლილებები არაა შენახული. დარწმუნებული ხართ დაბრუნებაში?" + "შენახვა?" "მუდმივი ბმულის შექმნა ვერ მოხერხდა" "ვერ გამოვიდა რუკის %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 new file mode 100644 index 00000000000..7760fec670a --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-lt/translations.xml @@ -0,0 +1,140 @@ + + + "Slėpti slaptažodį" + "Siųsti failus" + "Rodyti slaptažodį" + "Vartotojo meniu" + "Priimti" + "Atgal" + "Atšaukti" + "Pasirinkti nuotrauką" + "Išvalyti" + "Uždaryti" + "Užbaigti patvirtinimą" + "Patvirtinti" + "Tęsti" + "Kopijuoti" + "Kopijuoti nuorodą" + "Sukurti" + "Kurti kambarį" + "Atmesti" + "Išjungti" + "Atlikta" + "Taisyti" + "Įjungti" + "Pamiršote slaptažodį?" + "Kviesti" + "Pakviesti žmonių" + "Kviesti žmones į %1$s" + "Kvietimai" + "Sužinoti daugiau" + "Palikti" + "Palikti kambarį" + "Toliau" + "Ne" + "Ne dabar" + "Gerai" + "Sparčiai atsakyti" + "Cituoti" + "Šalinti" + "Atsakyti" + "Pranešti apie riktą" + "Pranešti apie turinį" + "Bandyti dar kartą" + "Pakartoti iššifravimą" + "Išsaugoti" + "Ieškoti" + "Siųsti" + "Siųsti žinutę" + "Dalintis" + "Bendrinti nuorodą" + "Praleisti" + "Pradėti" + "Pradėti pokalbį" + "Pradėti patvirtinimą" + "Fotografuoti" + "Peržiūrėti šaltinį" + "Taip" + "Apie" + "Analitika" + "Garsas" + "Burbulai" + "Kuriamas kambarys…" + "Išėjo iš kambario" + "Iššifravimo klaida" + "Kūrėjo nustatymai" + "(taisyta)" + "Taisymas" + "* %1$s %2$s" + "Šifravimas įjungtas" + "Klaida" + "Failas" + "Failas išsaugotas aplanke Atsisiuntimai" + "GIF" + "Paveikslėlis" + "Šio Matrix ID nepavyksta rasti, todėl kvietimas gali būti negautas." + "Paliekamas kambarys" + "Nuoroda nukopijuota į iškarpinę" + "Įkeliama…" + + "%1$d narys" + "%1$d nariai" + "%1$d narių" + + "Žinutė" + "Žinutės išdėstymas" + "Žinutė pašalinta" + "Modernus" + "Nėra rezultatų" + "Neprisijungta" + "Slaptažodis" + "Žmonės" + "Nuolatinė nuoroda" + "Reakcijos" + "Atsakant %1$s" + "Pranešti apie klaidą" + "Skundas pateiktas" + "Kambario pavadinimas" + "pvz., Jūsų projekto pavadinimas" + "Ieškoti ko nors" + "Paieškos rezultatai" + "Saugumas" + "Siunčiama…" + "Serveris nepalaikomas" + "Serverio URL" + "Nustatymai" + "Pradedamas pokalbis…" + "Lipdukas" + "Pavyko" + "Pasiūlymai" + "Tema" + "Apie ką šis kambarys?" + "Nepavyko iššifruoti" + "Kvietimų nepavyko išsiųsti vienam ar keliems vartotojams." + "Nepavyko išsiųsti kvietimo (-ų)" + "Nepalaikomas įvykis" + "Vartotojo vardas" + "Patvirtinimas atšauktas" + "Patvirtinimas baigtas" + "Vaizdo įrašas" + "Laukiama…" + "Patvirtinimas" + "Klaida" + "Pavyko" + "Įspėjimas" + "Nepavyko sukurti nuolatinės nuorodos" + "Nepavyko įkelti žinučių" + "Kai kurios žinutės nebuvo išsiųstos" + "Atsiprašome, įvyko klaida" + "🔐️ Prisijunkite prie manęs %1$s" + "Ei, pasikalbėkime naudojant %1$s: %2$s" + "%1$s Android" + "Papurtykite, kad praneštumėte apie klaidą" + "Nepavyko pasirinkti laikmenos, pabandykite dar kartą." + "Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą." + "Nepavyko įkelti laikmenos, pabandykite dar kartą." + "Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą." + "Nepavyko gauti naudotojo išsamios informacijos." + "Versija: %1$s (%2$s)" + "lt" + 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 94542dfb2e2..6c2ec13d81c 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -1,25 +1,41 @@ + "Legg til reaksjon: %1$s" + "Profilbilde" "Slett" + + "%1$d siffer angitt" + "%1$d sifre angitt" + "Skjul passord" + "Bli med i samtale" "Hopp til bunnen" "Bare omtaler" "Dempet" "Side %1$d" "Setter på pause" + "Talemelding, lengde: %1$s, nåværende posisjon: %2$s" "PIN-felt" "Spill av" + "Avstemning" "Avsluttet avstemning" "Reager med %1$s" "Reager med andre emojier" "Lest av %1$s og %2$s" + + "Lest av %1$s og %2$d andre" + "Lest av %1$s og %2$d andre" + "Lest av %1$s" "Trykk for å vise alle" + "Fjern reaksjonen med %1$s" "Fjern reaksjonen med %1$s" "Sende filer" "Vis passord" "Start en samtale" "Brukermeny" + "Vis detaljer" + "Talemelding, lengde: %1$s" "Ta opp talemelding." "Stopp opptaket" "Godta" @@ -46,9 +62,11 @@ "Deaktiver" "Deaktiver kontoen" "Avslå" + "Avslå og blokker" "Slett avstemning" "Deaktiver" "Forkast" + "Avvis" "Ferdig" "Rediger" "Rediger bildetekst" @@ -57,7 +75,7 @@ "Avslutt avstemning" "Skriv inn PIN-koden" "Glemt passordet?" - "Fremover" + "Videresend" "Gå tilbake" "Ignorer" "Inviter" @@ -87,10 +105,14 @@ "Avvis" "Fjern" "Fjern bildetekst" + "Fjern melding" "Svar" "Svar i tråden" + "Rapporter" "Rapporter feil" "Rapporter innhold" + "Rapporter samtale" + "Rapporter rommet" "Tilbakestill" "Tilbakestill identitet" "Prøv på nytt" @@ -111,12 +133,20 @@ "Start verifisering" "Trykk for å laste inn kart" "Ta bilde" + "Trykk for alternativer" + "Prøv igjen" + "Løsne" + "Vis i tidslinjen" "Vis kilde" "Ja" + "Ja, prøv igjen" + "Serveren din støtter nå en ny, raskere protokoll. Logg ut og logg inn igjen for å oppgradere nå. Ved å gjøre dette nå, unngår du å bli tvunget til å logge ut når den gamle protokollen fjernes senere." + "Oppgradering tilgjengelig" "Om" "Retningslinjer for akseptabel bruk" "Legger til bildetekst" "Avanserte innstillinger" + "et bilde" "Analyse" "Utseende" "Lyd" @@ -127,43 +157,68 @@ "Kopiert til utklippstavlen" "Opphavsrett" "Oppretter rom …" + "Forespørsel kansellert" "Forlot rommet" + "Invitasjon avslått" "Mørk" "Dekrypteringsfeil" "Alternativer for utviklere" "Enhets-ID" "Direkte chat" "Ikke vis dette igjen" + "Nedlastingen mislyktes" + "Laster ned" "(redigert)" "Redigering" "Redigerer bildetekst" "* %1$s %2$s" + "Tom fil" "Kryptering" "Kryptering aktivert" "Skriv inn PIN-koden din" "Feil" + "Det har oppstått en feil, og vil kanskje ikke motta varsler om nye meldinger. Vennligst feilsøk varslinger fra innstillingene. + +Årsak: %1$s." + "Alle" "Mislyktes" "Favoritt" + "Favoritt" "Fil" + "Fil slettet" + "Fil lagret" + "Fil lagret i Nedlastinger" + "Videresend melding" + "Ofte brukt" "GIF" "Bilde" "Som svar på %1$s" "Installer APK" + "Finner ikke denne Matrix-IDen, så invitasjonen blir kanskje ikke mottatt." "Forlater rommet" "Lys" + "Linje kopiert til utklippstavlen" "Lenke kopiert til utklippstavlen" "Laster inn…" + "Laster inn mer…" + + "%d andre" + "%d andre" + "%1$d medlem" "%1$d medlemmer" "Melding" + "Meldingshandlinger" "Meldingsoppsett" "Melding fjernet" "Moderne" "Demp" + "%1$s (%2$s)" "Ingen resultater" "Ingen romnavn" + "Ikke kryptert" "Frakoblet" "Åpen kildekode-lisenser" "eller" @@ -172,6 +227,7 @@ "Permalenke" "Tillatelse" "Festet" + "Vennligst sjekk internettforbindelsen din" "Vennligst vent…" "Er du sikker på at du vil avslutte denne avstemningen?" "Avstemning: %1$s" @@ -184,8 +240,12 @@ "Retningslinjer for personvern" "Reaksjon" "Reaksjoner" + "Årsak" "Gjenopprettingsnøkkel" "Oppdaterer…" + + "%1$d svar" + "Svar til %1$s" "Rapporter en feil" "Rapporter et problem" @@ -211,6 +271,7 @@ "Delt posisjon" "Logger av" "Noe gikk galt" + "Vi har støtt på et problem. Vennligst prøv igjen." "Starter chat…" "Klistremerke" "Suksess" @@ -218,46 +279,129 @@ "Synkroniserer" "System" "Tekst" + "Varsler fra tredjeparter" "Tråd" "Emne" + "Hva er dette rommet for?" "Kan ikke dekryptere" + "Sendt fra en usikker enhet" "Du har ikke tilgang til denne meldingen" - "Avsenderens verifiserte identitet er endret" + "Avsenderens verifiserte identitet ble tilbakestilt" "Invitasjoner kunne ikke sendes til en eller flere brukere." "Kunne ikke sende invitasjon(er)" "Lås opp" "Slå på lyden" + "Ikke støttet anrop" "Hendelsen er ikke støttet" "Brukernavn" "Verifisering kansellert" "Verifisering fullført" + "Verifisering mislyktes" "Verifisert" "Verifiser enhet" "Verifiser identiteten" + "Verifiser bruker" "Video" "Talemelding" "Venter…" "Venter på denne meldingen" "Du" + "%1$s\'s identitet ble tilbakestilt. %2$s" + "%1$ss %2$s-identitet ble tilbakestilt. %3$s" "(%1$s)" + "%1$ss identitet ble tilbakestilt." + "%1$ss %2$s identitet ble tilbakestilt. %3$s" + "Trekk tilbake verifisering" + "Lenken %1$s tar deg til et annet nettsted %2$s + +Er du sikker på at du vil fortsette?" + "Dobbeltsjekk denne lenken" + "Rommet er rapportert" + "Rapportert og forlatt rommet" "Bekreftelse" "Feil" "Suksess" "Advarsel" "Endringene dine er ikke lagret. Er du sikker på at du vil gå tilbake?" "Lagre endringer?" + "Hjemmeserveren din må oppgraderes for å støtte Matrix Authentication Service og kontooppretting." "Opprettelse av permalenken mislyktes" + "%1$s kunne ikke laste inn kartet. Prøv igjen senere." "Kunne ikke laste inn meldinger" + "%1$s fikk ikke tilgang til lokasjonen din. Vennligst prøv igjen senere." + "Kunne ikke laste opp talemeldingen din." + "Rommet eksisterer ikke lenger, eller invitasjonen er ikke lenger gyldig." + "Melding ikke funnet" + "%1$s har ikke tilgang til lokasjonen din. Du kan aktivere tilgang i Innstillinger." + "%1$s har ikke tilgang til lokasjonen din. Aktiver tilgang nedenfor." + "%1$s har ikke tilgang til mikrofonen din. Aktiver tilgang til å ta opp en talemelding." + "Dette kan skyldes nettverks- eller serverproblemer." + "Denne romadressen finnes allerede. Prøv å redigere romadressefeltet eller endre romnavnet" + "Noen tegn er ikke tillatt. Bare bokstaver, sifre og følgende symboler støttes! $ & \'() * +/; =? @ [] - . _" "Noen meldinger er ikke sendt" "Beklager, det oppstod en feil" + "Avsenderen av hendelsen samsvarer ikke med eieren av enheten som sendte den." + "Ektheten til denne krypterte meldingen kan ikke garanteres på denne enheten." + "Kryptert av en tidligere verifisert bruker." "Ikke kryptert." + "Kryptert av en ukjent eller slettet enhet." + "Kryptert med en enhet som ikke er verifisert av eieren." + "Kryptert av en ubekreftet bruker." "🔐️ Bli med meg på %1$s" "Hei, snakk med meg på %1$s: %2$s" "%1$s Android" + "Rageshake for å rapportere feil" + "%1$s: %2$s" + "Alternativer" + "Fjern %1$s" + "Innstillinger" + "Kunne ikke velge medium, prøv igjen." + "Teksting er kanskje ikke synlig for personer som bruker eldre apper." + "Kunne ikke behandle medier for opplasting, vennligst prøv igjen." + "Opplasting av medier mislyktes, vennligst prøv igjen." + "Trykk på en melding og velg “%1$s” for å inkludere her." + "Fest viktige meldinger slik at de lett kan ses" + + "%1$d Festet melding" + "%1$d Festede meldinger" + + "Festede meldinger" + "Du er i ferd med å gå til %1$s kontoen din for å tilbakestille identiteten din. Etterpå blir du tatt tilbake til appen." + "Kan du ikke bekrefte? Gå til kontoen din for å tilbakestille identiteten din." + "Trekk tilbake verifikasjon og send" + "Du kan trekke tilbake verifiseringen og sende denne meldingen likevel, eller du kan avbryte for nå og prøve igjen senere etter at du har reverifisert %1$s." + "Meldingen din ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt" + "Send melding uansett" + "%1$s bruker én eller flere uverifiserte enheter. Du kan sende meldingen uansett, eller du kan avbryte foreløpig og prøve igjen senere etter at %2$s har verifisert alle enhetene sine." + "Meldingen din ble ikke sendt fordi %1$s ikke har bekreftet alle enhetene" + "En eller flere av enhetene dine er ikke verifisert. Du kan sende meldingen likevel, eller du kan avbryte og prøve igjen senere etter at du har verifisert alle enhetene dine." + "Meldingen din ble ikke sendt fordi du ikke har verifisert en eller flere av enhetene dine" + "Kunne ikke behandle medier for opplasting, vennligst prøv igjen." "Kunne ikke hente brukerdetaljer" + "Melding i %1$s" + "Utvid" + "Reduser" + "Du ser allerede på dette rommet!" + "%1$s av %2$s" + "%1$s Festede meldinger" + "Laster inn melding…" + "Vis alle" + "Chat" + "Del lokasjon" + "Del min lokasjon" "Åpne i Apple Maps" "Åpne i Google Maps" + "Åpne i OpenStreetMap" + "Del denne lokasjonen" + "Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt." "Meldingen ble ikke sendt fordi %1$s ikke har verifisert alle enheter." + "Meldingen ble ikke sendt fordi du ikke har verifisert en eller flere av enhetene dine." + "Lokasjon" "Versjon: %1$s (%2$s)" "en" + "Historiske meldinger er ikke tilgjengelige på denne enheten" + "Du må verifisere denne enheten for å få tilgang til historiske meldinger" + "Du har ikke tilgang til denne meldingen" + "Kan ikke dekryptere meldingen" + "Denne meldingen ble blokkert enten fordi du ikke har bekreftet enheten din, eller fordi avsenderen må bekrefte identiteten din." 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 b9a672e706b..c6a8608fd4d 100644 --- a/libraries/ui-strings/src/main/res/values-nl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nl/translations.xml @@ -1,16 +1,19 @@ + "Afbeelding" "Verwijderen" "%1$d cijfer ingevoerd" "%1$d cijfers ingevoerd" "Wachtwoord verbergen" + "Deelnemen aan gesprek" "Spring naar einde" "Alleen vermeldingen" "Gedempt" "Pagina %1$d" "Pauzeren" + "Spraakbericht, duur: %1$s, huidige positie: %2$s" "PIN-veld" "Afspelen" "Peiling" @@ -24,14 +27,17 @@ "Gelezen door %1$s" "Tik om alles weer te geven" - "Verwijder reactie met %1$s" + "Verwijder reactie met %1$s" "Bestanden verzenden" "Wachtwoord weergeven" "Begin een oproep" "Gebruikersmenu" + "Details bekijken" + "Spraakbericht, duur: %1$s" "Spraakbericht opnemen." "Opnemen stoppen" "Accepteren" + "Beschrijving toevoegen" "Toevoegen aan tijdlijn" "Terug" "Bellen" @@ -64,6 +70,7 @@ "Wachtwoord vergeten?" "Doorsturen" "Terug" + "Negeren" "Uitnodigen" "Mensen uitnodigen" "Nodig mensen uit voor %1$s" @@ -73,7 +80,7 @@ "Meer informatie" "Verlaten" "Gesprek verlaten" - "Ruimte verlaten" + "Kamer verlaten" "Meer laden" "Account beheren" "Apparaten beheren" @@ -120,6 +127,8 @@ "Bekijk in tijdlijn" "Bron weergeven" "Ja" + "Je server ondersteunt nu een nieuw, sneller protocol. Log uit en log opnieuw in om nu te upgraden. Als je dit nu doet, voorkom je dat je geforceerd uitlogt wordt wanneer het oude protocol later wordt verwijderd." + "Upgrade beschikbaar" "Over" "Beleid inzake redelijk gebruik" "Geavanceerde instellingen" @@ -137,6 +146,7 @@ "Donker" "Decryptie fout" "Ontwikkelaarsopties" + "Apparaat-ID" "Directe chat" "Dit niet meer weergeven" "(bewerkt)" @@ -164,6 +174,10 @@ Reden: %1$s." "Licht" "Link gekopieerd naar klembord" "Laden…" + + "%d andere" + "%d anderen" + "%1$d lid" "%1$d leden" @@ -251,8 +265,8 @@ Reden: %1$s." "Wachten…" "Wachten op dit bericht" "Jij" - "%1$s\'s identiteit lijkt te zijn veranderd. %2$s" - "%1$s\'s %2$s identiteit lijkt te zijn veranderd. %3$s" + "%1$s\'s identiteit lijkt te zijn gewijzigd. %2$s" + "%1$s\'s %2$s identiteit lijkt te zijn gewijzigd. %3$s" "(%1$s)" "Bevestiging" "Fout" 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 e6a643b21fb..01bb2cadaa4 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -1,5 +1,6 @@ + "Awatar" "Usuń" "Wprowadzono %1$d cyfrę" @@ -7,11 +8,13 @@ "Wprowadzono %1$d cyfr" "Ukryj hasło" + "Dołącz do połączenia" "Przejdź na dół" "Tylko wzmianki" "Wyciszone" "Strona %1$d" "Wstrzymaj" + "Wiadomość głosowa, czas trwania: %1$s, aktualna pozycja: %2$s" "Pole PIN" "Odtwórz" "Ankieta" @@ -26,11 +29,13 @@ "Odczytane przez %1$s" "Stuknij, aby pokazać wszystkich" - "Usuń reakcję %1$s" + "Usuń reakcję %1$s" "Wyślij pliki" "Pokaż hasło" "Rozpocznij rozmowę" "Menu użytkownika" + "Wyświetl szczegóły" + "Wiadomość głosowa, czas trwania: %1$s" "Nagraj wiadomość głosową." "Zatrzymaj nagrywanie" "Akceptuj" @@ -57,9 +62,11 @@ "Dezaktywuj" "Dezaktywuj konto" "Odrzuć" + "Odrzuć i zablokuj" "Usuń ankietę" "Wyłącz" "Odrzuć" + "Zamknij" "Gotowe" "Edytuj" "Edytuj opis" @@ -101,8 +108,11 @@ "Usuń wiadomość" "Odpowiedz" "Odpowiedz w wątku" + "Zgłoś" "Zgłoś błąd" "Zgłoś treść" + "Zgłoś rozmowę" + "Zgłoś pokój" "Resetuj" "Zresetuj tożsamość" "Spróbuj ponownie" @@ -130,6 +140,8 @@ "Wyświetl źródło" "Tak" "Tak, spróbuj ponownie" + "Twój serwer obsługuje teraz nowy, szybszy protokół. Wyloguj się i zaloguj ponownie, aby uaktualnić teraz. Zrobienie tego teraz pomoże uniknąć wymuszonego wylogowania, gdy stary protokół zostanie później usunięty." + "Dostępna aktualizacja" "O programie" "Polityka użytkowania" "Dodawanie opisu" @@ -144,18 +156,22 @@ "Skopiowano do schowka" "Prawa autorskie" "Tworzenie pokoju…" + "Anulowano żądanie" "Opuszczono pokój" + "Odrzucono zaproszenie" "Ciemny" "Błąd deszyfrowania" "Opcje programisty" "ID urządzenia" "Czat prywatny" "Nie pokazuj ponownie" + "Błąd pobierania" "Pobieram" "(edytowane)" "Edytowanie" "Edytowanie opisu" "* %1$s %2$s" + "Pusty plik" "Szyfrowanie" "Szyfrowanie włączone" "Wprowadź kod PIN" @@ -168,6 +184,8 @@ Powód: %1$s." "Ulubione" "Ulubione" "Plik" + "Plik usunięty" + "Plik zapisany" "Plik zapisany do folderu Pobrane" "Przekaż wiadomość" "Często używane" @@ -178,8 +196,15 @@ Powód: %1$s." "Nie można znaleźć identyfikatora Matrix ID, zaproszenie mogło nie dotrzeć." "Opuszczanie pokoju" "Jasny" + "Wiersz skopiowany do schowka" "Link został skopiowany do schowka" "Ładowanie…" + "Ładuję więcej…" + + "%d inny" + "%d innych" + "%d innych" + "%1$d członek" "%1$d członki" @@ -191,8 +216,10 @@ Powód: %1$s." "Wiadomość usunięta" "Nowoczesny" "Wycisz" + "%1$s (%2$s)" "Brak wyników" "Brak nazwy pokoju" + "Nieszyfrowane" "Offline" "Licencje open-source" "lub" @@ -201,6 +228,7 @@ Powód: %1$s." "Link bezpośredni" "Uprawnienie" "Przypięte" + "Sprawdź swoje połączenie internetowe" "Proszę czekać…" "Jesteś pewien, że chcesz zakończyć tę ankietę?" "Ankieta: %1$s" @@ -214,6 +242,7 @@ Powód: %1$s." "Polityka prywatności" "Reakcja" "Reakcje" + "Powód" "Klucz przywracania" "Odświeżanie…" "Odpowiadanie do %1$s" @@ -227,7 +256,7 @@ Powód: %1$s." "Zapisano zmiany" "Zapisywanie" "Blokada ekranu" - "Wyszukaj kogoś" + "Szukaj osób" "Wyniki wyszukiwania" "Bezpieczeństwo" "Wyświetlone przez" @@ -241,6 +270,7 @@ Powód: %1$s." "Udostępniona lokalizacja" "Wylogowywanie" "Coś poszło nie tak" + "Napotkaliśmy problem. Spróbuj ponownie." "Rozpoczynanie czatu…" "Naklejka" "Sukces" @@ -255,7 +285,7 @@ Powód: %1$s." "Nie można odszyfrować" "Wysłane z niebezpiecznego urządzenia" "Nie masz uprawnień do tej wiadomości" - "Zweryfikowana tożsamość nadawcy uległa zmianie" + "Zweryfikowana tożsamość nadawcy została zresetowana" "Nie udało się wysłać zaproszenia do jednego lub więcej użytkowników." "Nie można wysłać zaproszeń" "Odblokuj" @@ -269,14 +299,24 @@ Powód: %1$s." "Zweryfikowano" "Weryfikuj urządzenie" "Zweryfikuj tożsamość" + "Zweryfikuj użytkownika" "Film" "Wiadomość głosowa" "Oczekiwanie…" "Oczekiwanie na tę wiadomość" "Ty" - "Tożsamość %1$s mogła ulec zmianie. %2$s" - "Wygląda na to, że tożsamość %1$s %2$s uległa zmianie. %3$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" + "Wycofaj weryfikację" + "Link %1$s prowadzi Cię do innej witryny %2$s + +Czy na pewno chcesz kontynuować?" + "Sprawdź dwukrotnie ten link" + "Pokój zgłoszony" + "Opuszczono i zgłoszono pokój" "Potwierdzenie" "Błąd" "Sukces" @@ -293,6 +333,7 @@ Powód: %1$s." "%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." "%1$s nie ma uprawnień dostępu do Twojego mikrofonu. Włącz dostęp, aby nagrać wiadomość głosową." + "Może to być spowodowane problemami z siecią lub serwerem." "Ten adres pokoju już istnieje. Spróbuj zmienić adres lub nazwę pokoju" "Niektóre znaki są niedozwolone. Obsługiwane są tylko litery, cyfry i następujące symbole ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Niektóre wiadomości nie zostały wysłane" @@ -323,7 +364,7 @@ Powód: %1$s." "Nie możesz potwierdzić? Przejdź do swojego konta i zresetuj swoją tożsamość." "Wycofaj weryfikację i wyślij" "Możesz wycofać weryfikację i wysłać wiadomość mimo wszystko lub anulować i spróbować ponownie po ponownej weryfikacji %1$s." - "Twoja wiadomość nie została wysłana, ponieważ tożsamość %1$s uległa zmianie." + "Twoja wiadomość nie została wysłana, ponieważ tożsamość %1$s została zresetowana." "Wyślij wiadomość mimo to" "%1$s korzysta z jednego lub więcej niezweryfikowanych urządzeń. Wyślij wiadomość mimo to lub anuluj i spróbuj ponownie, gdy %2$s zweryfikuje wszystkie swoje urządzenia." "Twoja wiadomość nie została wysłana, ponieważ %1$s nie zweryfikował swoich wszystkich urządzeń" @@ -331,19 +372,19 @@ Powód: %1$s." "Twoja wiadomość nie została wysłana, ponieważ nie zweryfikowałeś jednego lub więcej swoich urządzeń." "Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie." "Nie można pobrać danych użytkownika" + "Wiadomość w %1$s" "%1$s z %2$s" "%1$s przypiętych wiadomości" "Wczytywanie wiadomości…" "Wyświetl wszystkie" "Czat" - "Wysłano prośbę o dołączenie" "Udostępnij lokalizację" "Udostępnij moją lokalizację" "Otwórz w Apple Maps" "Otwórz w Google Maps" "Otwórz w OpenStreetMap" "Udostępnij tę lokalizację" - "Wiadomość nie została wysłana, ponieważ tożsamość %1$s uległa zmianie." + "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ń." "Lokalizacja" 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 a45d7bd37bd..40cd0391583 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 @@ -1,22 +1,26 @@ + "Adicionar reação: %1$s" + "Avatar" "Excluir" "%1$d dígito inserido" "%1$d dígitos inseridos" "Ocultar senha" + "Juntar-se à chamada" "Ir para o final" "Apenas menções" "Silenciado" "Página %1$d" "Pausar" + "Mensagem de voz, duração: %1$s, posição atual: %2$s" "Campo de PIN" "Reproduzir" "Enquete" "Enquete encerrada" "Reagir com %1$s" - "Reaja com outros emojis" + "Reagir com outros emojis" "Lido por %1$s e %2$s" "Lido por %1$s e %2$d outro" @@ -24,35 +28,49 @@ "Lido por %1$s" "Toque para mostrar tudo" - "Remova a reação com %1$s" + "Remover reação com %1$s" + "Remover reação com %1$s" "Enviar arquivos" "Mostrar senha" "Iniciar uma chamada" "Menu do usuário" + "Ver avatar" + "Visualizar detalhes" + "Mensagem de voz, duração: %1$s" "Gravar mensagem de voz." "Parar gravação" "Aceitar" + "Adicionar legenda" "Adicionar à linha do tempo" "Voltar" "Chamar" "Cancelar" + "Cancelar por agora" "Escolher foto" "Limpar" "Fechar" "Verificação completa" "Confirmar" + "Confirmar senha" "Continuar" "Copiar" + "Copiar legenda" "Copiar link" "Copiar link para a mensagem" + "Copiar texto" "Criar" "Criar uma sala" + "Desativar" + "Desativar conta" "Recusar" + "Recusar e bloquear" "Excluir Enquete" "Desabilitar" "Descartar" + "Dispensar" "Concluído" "Editar" + "Editar legenda" "Editar enquete" "Habilitar" "Encerrar enquete" @@ -60,6 +78,7 @@ "Esqueceu a senha?" "Encaminhar" "Voltar" + "Ignorar" "Convidar" "Convidar pessoas" "Convidar pessoas para %1$s" @@ -86,35 +105,49 @@ "Reagir" "Recusar" "Remover" + "Remover legenda" + "Remover mensagem" "Responder" "Responder no tópico" + "Reportar" "Reportar erro" - "Denunciar conteúdo" + "Reportar conteúdo" + "Reportar conversa" + "Reportar sala" "Redefinir" + "Redefinir identidade" "Tentar novamente" - "Tente descriptografar novamente" + "Tentar descriptografar novamente" "Salvar" "Pesquisar" "Enviar" "Enviar mensagem" "Compartilhar" "Compartilhar link" + "Mostrar" "Iniciar sessão novamente" "Sair" "Sair mesmo assim" "Pular" "Começar" - "Iniciar bate-papo" + "Iniciar conversa" "Iniciar verificação" "Toque para carregar o mapa" "Tirar foto" "Toque para opções" "Tente novamente" + "Desafixar" + "Visualizar na linha do tempo" "Ver fonte" "Sim" + "Sim, tentar novamente" + "Seu servidor agora é compatível com um protocolo novo e mais rápido. Faça logout e login novamente para atualizar agora. Fazendo isso agora, você evitará um logout forçado quando o protocolo antigo for removido mais tarde." + "Atualização disponível" "Sobre" "Política de uso aceitável" + "Adicionando legenda" "Configurações avançadas" + "uma imagem" "Telemetria" "Aparência" "Áudio" @@ -122,27 +155,42 @@ "Bolhas" "Chamada iniciada" "Backup de conversas" + "Copiado para a área de transferência" "Direitos autorais" "Criando sala…" + "Solicitação cancelada" "Saiu da sala" + "Convite recusado" "Escuro" "Erro de descriptografia" "Opções do desenvolvedor" + "ID do dispositivo" "Conversa privada" "Não mostrar isto novamente" + "O download falhou" + "Baixando" "(editado)" "Editando" + "Editando legenda" "* %1$s %2$s" + "Arquivo vazio" + "Criptografia" "Criptografia ativada" "Insira seu PIN" "Erro" + "Ocorreu​ um ​erro, você ​pode ​não ​receber ​notificações ​de ​novas ​mensagens. ​Você ​pode ​solucionar ​o ​problema ​das ​notificações ​nas ​configurações.↵ +↵ +Motivo:​ %1$s." "Todos" "Falhou" "Favorito" "Favoritado" "Arquivo" + "Arquivo excluído" + "Arquivo salvo" "Arquivo salvo em Downloads" "Encaminhar mensagem" + "Frequentemente usado" "GIF" "Imagem" "Em resposta a %1$s" @@ -150,8 +198,14 @@ "Este ID Matrix não foi encontrado, então o convite pode não ser recebido" "Saindo da sala" "Claro" + "Linha copiada para a área de transferência" "Link copiado para área de transferência" "Carregando…" + "Carregando mais…" + + "%d outro" + "%d outros" + "%1$d membro" "%1$d membros" @@ -162,8 +216,10 @@ "Mensagem removida" "Moderno" "Silenciar" + "%1$s (%2$s)" "Sem resultados" "Sem nome de sala" + "Não criptografado" "Offline" "Licenças de código aberto" "ou" @@ -171,6 +227,8 @@ "Pessoas" "Link permanente" "Permissão" + "Fixo" + "Verifique sua conexão com a Internet" "Por favor, aguarde…" "Tem certeza de que deseja encerrar esta enquete?" "Enquete: %1$s" @@ -183,8 +241,13 @@ "Política de Privacidade" "Reação" "Reações" + "Motivo" "Chave de recuperação" "Atualizando…" + + "%1$d resposta" + "%1$d respostas" + "Respondendo a %1$s" "Reportar um erro" "Reportar um problema" @@ -204,12 +267,14 @@ "Enviando…" "Envio falhou" "Enviado" + ". " "Servidor não suportado" "URL do Servidor" "Configurações" "Localização compartilhada" "Saindo" "Algo deu errado" + "Encontramos um problema. Tente novamente." "Iniciando o chat…" "Adesivo" "Sucesso" @@ -222,38 +287,66 @@ "Tópico" "Sobre o que é essa sala?" "Não é possível descriptografar" + "Enviado de um dispositivo inseguro" "Você não tem acesso a esta mensagem" + "A identidade verificada do remetente foi redefinida" "Não foi possível enviar convites para um ou mais usuários." "Não foi possível enviar o(s) convite(s)" "Desbloquear" "Desmutar" + "Chamada sem compatibilidade" "Evento não suportado" "Nome do usuário" "Verificação cancelada" "Verificação concluída" + "A verificação falhou" + "Verificado" "Verificar dispositivo" + "Verificar identidade" + "Verificar usuário" "Vídeo" "Mensagem de voz" "Esperando…" "Aguardando esta mensagem" "Você" + "A identidade de %1$s foi redefinida. %2$s" + "A identidade de %1$s em %2$s foi redefinida. %3$s" + "(%1$s)" + "A identidade de %1$s foi redefinida." + "Identidade de %1$s no %2$s foi redefinida. %3$s" + "Retirar verificação" + "O link %1$s está levando você para outro site %2$s + +Você tem certeza de que deseja continuar?" + "Verifique novamente este link" + "Sala denunciada" + "Denunciou e deixou a sala" "Confirmação" "Erro" "Sucesso" "Aviso" "Suas alterações não foram salvas. Tem certeza de que você quer voltar?" "Salvar alterações?" + "Seu servidor doméstico precisa ser atualizado para oferecer suporte ao Matrix Authentication Service e à criação de contas." "Falha ao criar o link permanente" "%1$s não conseguiu carregar o mapa. Por favor, tente novamente mais tarde." "Falha ao carregar mensagens" "%1$s não conseguiu acessar sua localização. Por favor, tente novamente mais tarde." "Falha ao enviar sua mensagem de voz." + "A sala não existe mais ou o convite não é mais válido." "Mensagem não encontrada" "%1$s não tem permissão para acessar sua localização. Você pode ativar o acesso nas Configurações." "%1$s não tem permissão para acessar sua localização. Habilite o acesso abaixo." "%1$s não tem permissão para acessar seu microfone. Permita o acesso para gravar uma mensagem de voz." + "Isso pode ocorrer devido a problemas na rede ou no servidor." + "Este endereço de sala já existe. Tente editar o campo de endereço da sala ou alterar o nome da sala" + "Alguns caracteres não são permitidos. Somente letras, dígitos e os seguintes símbolos são aceitos ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Algumas mensagens não foram enviadas" "Desculpe, ocorreu um erro" + "O remetente do evento não corresponde com o proprietário do dispositivo que o enviou." + "A autenticidade desta mensagem criptografada não pode ser garantida neste aparelho." + "Criptografado por um usuário previamente verificado." + "Não criptografado." "Criptografada por um dispositivo desconhecido ou apagado." "Criptografado por um dispositivo que não foi verificado pelo seu dono." "Criptografado por um usuário não verificado." @@ -261,18 +354,57 @@ "Ei, fale comigo em %1$s: %2$s" "%1$s Android" "Rageshake para relatar um bug" + "%1$s: %2$s" + "Opções" + "Remover %1$s" + "Configurações" "Falha ao selecionar a mídia, tente novamente." + "As legendas podem não ser visíveis para pessoas que usam aplicativos mais antigos." "Falha ao processar mídia para upload. Tente novamente." "Falha ao enviar mídia. Tente novamente." + "Pressione em uma mensagem e escolha \"%1$s\" para incluir aqui." + "Fixe mensagens importantes para que elas possam ser facilmente descobertas" + + "%1$d Mensagem fixada" + "%1$d Mensagens fixadas" + + "Mensagens fixadas" + "Você está prestes a acessar sua conta %1$s para redefinir sua identidade. Depois disso, você será levado de volta ao aplicativo." + "Não consegue confirmar? Acesse sua conta para redefinir sua identidade." + "Retire a verificação e envie" + "Você pode retirar sua verificação e enviar esta mensagem mesmo assim, ou pode cancelar por enquanto e tentar novamente mais tarde depois de reverificar %1$s." + "Sua mensagem não foi enviada porque a identidade verificada de %1$s foi redefinida" + "Enviar mensagem mesmo assim" + "%1$s está usando um ou mais dispositivos não verificados. Você pode enviar a mensagem de qualquer maneira, ou você pode cancelar por enquanto e tentar novamente mais tarde depois que %2$s tiver verificado todos os seus dispositivos." + "Sua mensagem não foi enviada porque %1$s não verificou todos os dispositivos" + "Um ou mais de seus dispositivos não foram verificados. Você pode enviar a mensagem mesmo assim ou pode cancelar por enquanto e tentar novamente mais tarde, depois de ter verificado todos os seus dispositivos." + "Sua mensagem não foi enviada porque você não verificou um ou mais de seus dispositivos" "Falha ao processar mídia para upload. Tente novamente." "Não foi possível recuperar os detalhes do usuário" + "Mensagem em %1$s" + "Expandir" + "Reduzir" + "Já está visualizando esta sala!" + "%1$s de %2$s" + "%1$s Mensagens fixadas" + "Carregando mensagem…" + "Ver tudo" + "Bate-papo" "Compartilhar localização" "Compartilhar minha localização" "Abrir no Apple Maps" "Abrir no Google Maps" "Abrir no OpenStreetMap" "Compartilhe esta localização" + "Mensagem não enviada porque a identidade verificada de %1$s foi redefinida." + "A mensagem não foi enviada porque %1$s não verificou todos os dispositivos." + "Mensagem não enviada porque você não verificou um ou mais dos seus dispositivos." "Localização" "Versão: %1$s (%2$s)" "pt-br" + "As mensagens históricas não estão disponíveis neste dispositivo" + "Você precisa verificar este dispositivo para ter acesso a mensagens históricas" + "Você não tem acesso a esta mensagem" + "Não foi possível descriptografar a mensagem" + "Esta mensagem foi bloqueada porque você não verificou seu dispositivo ou porque o remetente precisa verificar sua identidade." 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 516e7cdf0fc..9027b515479 100644 --- a/libraries/ui-strings/src/main/res/values-pt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt/translations.xml @@ -1,16 +1,20 @@ + "Adicionar reação: %1$s" + "Avatar" "Eliminar" "%1$d dígito inserido" "%1$d dígitos inseridos" - "Ocultar senha" + "Ocultar palavra-passe" + "Juntar-se à chamada" "Saltar para o fundo" "Apenas menções" "Silenciado" "Página %1$d" "Pausar" + "Mensagem de voz, duração: %1$s, posição atual: %2$s" "Campo para PIN" "Reproduzir" "Sondagem" @@ -24,11 +28,15 @@ "Lida por %1$s" "Toca para mostrar tudo" + "Remover reação com %1$s" "Remover reação com %1$s" "Enviar ficheiros" - "Mostrar senha" + "Mostrar palavra-passe" "Iniciar chamada" "Menu de utilizador" + "Ver avatar" + "Ver detalhes" + "Mensagem de voz, duração: %1$s" "Gravar mensagem de voz." "Parar gravação" "Aceitar" @@ -54,10 +62,12 @@ "Criar uma sala" "Desativar" "Desativar conta" - "Rejeitar" + "Recusar" + "Recusar e bloquear" "Eliminar sondagem" "Desativar" "Descartar" + "Dispensar" "Feito" "Editar" "Editar legenda" @@ -65,7 +75,7 @@ "Ativar" "Fim da sondagem" "Inserir PIN" - "Esqueceu-se da senha?" + "Esqueceu-se da palavra-passe?" "Reencaminhar" "Voltar" "Ignorar" @@ -79,7 +89,7 @@ "Sair" "Sair da conversa" "Sair da sala" - "Carrega mais" + "Carregar mais" "Gerir conta" "Gerir dispositivos" "Enviar mensagem" @@ -87,7 +97,7 @@ "Não" "Agora não" "OK" - "Configurações" + "Definições" "Abrir com" "Afixar" "Resposta rápida" @@ -99,8 +109,11 @@ "Remover mensagem" "Responder" "Responder ao tópico" + "Reportar" "Comunicar problema" "Denunciar conteúdo" + "Denunciar conversa" + "Denunciar sala" "Repor" "Repor identidade" "Tentar novamente" @@ -127,10 +140,14 @@ "Ver na cronologia" "Ver fonte" "Sim" + "Sim, tentar novamente" + "O teu servidor suporta agora um protocolo novo e mais rápido. Termina a sessão e volta a iniciar sessão para atualizar agora. Se o fizeres agora, evitarás um fim de sessão forçado quando o protocolo antigo for removido mais tarde." + "Atualização disponível" "Sobre" "Política de utilização aceitável" "A adicionar legenda" "Configurações avançadas" + "uma imagem" "Recolha e análise de dados" "Aparência" "Áudio" @@ -141,19 +158,23 @@ "Copiado para a área de transferência" "Direitos de autor" "A criar sala…" + "Pedido cancelado" "Saíste da sala" + "Convite rejeitado" "Escuro" "Erro de decifragem" "Opções de programador" "ID do dispositivo" "Conversa direta" "Não mostrar novamente" + "Descarga falhada" "A descarregar" "(editada)" "A editar" "A editar legenda" "* %1$s %2$s" - "Encriptação" + "Ficheiro vazio" + "Cifragem" "Cifragem ativada" "Introduz o teu PIN" "Erro" @@ -165,6 +186,8 @@ Razão: %1$s." "Marcar como favorita" "Favoritas" "Ficheiro" + "Ficheiro eliminado" + "Ficheiro guardado" "Ficheiro guardado nas Transferências" "Reencaminhar mensagem" "Frequentemente utilizado" @@ -175,8 +198,14 @@ 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" "Claro" + "Linha copiada para a área de transferência" "Ligação copiada para a área de transferência" "A carregar…" + "A carregar mais…" + + "%d outro" + "%d outros" + "%1$d membro" "%1$d membros" @@ -187,8 +216,10 @@ Razão: %1$s." "Mensagem removida" "Moderno" "Silenciar" + "%1$s (%2$s)" "Sem resultados" "Sala sem nome" + "Não encriptado" "Desligado" "Licenças de código aberto" "ou" @@ -197,6 +228,7 @@ Razão: %1$s." "Ligação permanente" "Permissão" "Afixado" + "Por favor, verifica a tua ligação à Internet" "Por favor, aguarde…" "Tens a certeza que queres concluir esta sondagem?" "Sondagem: %1$s" @@ -209,8 +241,13 @@ Razão: %1$s." "Política de privacidade" "Reação" "Reações" + "Motivo" "Chave de recuperação" "A atualizar…" + + "%1$d resposta" + "%1$d respostas" + "Em resposta a %1$s" "Comunicar falha" "Comunicar um problema" @@ -230,12 +267,14 @@ Razão: %1$s." "A enviar…" "Falha no envio" "Enviada" + ". " "Servidor não suportado" "URL do servidor" "Configurações" "Localização partilhada" "A terminar sessão" "Algo correu mal" + "Encontrámos um erro. Por favor, tenta novamente." "A iniciar conversa…" "Autocolante" "Sucesso" @@ -250,7 +289,7 @@ Razão: %1$s." "Impossível decifrar" "Enviado de um dispositivo inseguro" "Não tens acesso a esta mensagem" - "A identidade verificada do remetente foi alterada" + "A identidade verificada do remetente foi reposta" "Não foi possível enviar convites a um ou mais utilizadores." "Não foi possível enviar convite(s)" "Desbloquear" @@ -264,14 +303,24 @@ Razão: %1$s." "Verificado" "Verificar o dispositivo" "Verifica a identidade" + "Verificar utilizador" "Vídeo" "Mensagem de voz" "A aguardar…" "À espera desta mensagem" "Tu" - "A identidade de %1$s parece ter mudado. %2$s" - "A identidade de %1$s (username: %2$s ) aparenta ter mudado. %3$s" + "A identidade de %1$s foi reposta. %2$s" + "A identidade de %1$s (%2$s) foi reposta. %3$s" "(%1$s)" + "A identidade de %1$s foi reposta." + "A identidade de %1$s (%2$s) foi reposta. %3$s" + "Retirar verificação" + "O link %1$s está a levar-te para outro site %2$s + +Tens a certeza de que queres continuar?" + "Verifica novamente esta ligação" + "Sala denunciada" + "Sala denunciada e abandonada" "Confirmação" "Erro" "Sucesso" @@ -284,14 +333,17 @@ Razão: %1$s." "Falha ao carregar mensagens" "A %1$s não conseguiu aceder à tua localização. Por favor, tenta novamente mais tarde." "Falha ao carregar mensagem de voz." + "A sala já não existe ou o convite já não é válido." "Mensagem não encontrada" "A %1$s não tem permissão para aceder à tua localização. Podes ativar o acesso nas Definições." "A %1$s não tem permissão para aceder à tua localização. Continua para ativares o acesso." "A %1$s não tem permissão para aceder ao teu microfone. Permite o acesso para gravar uma mensagem de voz." + "Isto pode dever-se a problemas na rede ou no servidor." "Este endereço de sala já existe, tente editar o campo de endereço da sala ou altere o nome da sala" "Alguns caracteres não são permitidos. Apenas letras, dígitos e os seguintes símbolos são suportados! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Algumas mensagens não foram enviadas" "Desculpe, ocorreu um erro" + "O remetente deste evento não é o dono do dispositivo que o enviou." "A autenticidade desta mensagem cifrada não pode ser garantida neste dispositivo." "Criptografado por um usuário verificado anteriormente." "Não cifrado." @@ -302,6 +354,10 @@ Razão: %1$s." "Alô! Fala comigo na %1$s: %2$s" "%1$s Android" "Agita o dispositivo em fúria para comunicar um problema" + "%1$s: %2$s" + "Opções" + "Remover %1$s" + "Configurações" "Falha ao selecionar multimédia, por favor tente novamente." "As legendas poderão não ser visíveis em versões mais antigas da aplicação." "Falha ao processar multimédia para carregamento, por favor tente novamente." @@ -317,7 +373,7 @@ Razão: %1$s." "Não consegue confirmar? Aceda à sua conta para repor a sua identidade." "Retirar verificação e enviar" "Podes retirar a tua verificação e enviar esta mensagem na mesma, ou podes cancelar por agora e tentar novamente mais tarde depois de reverificares %1$s." - "A sua mensagem não foi enviada porque a identidade verificada de %1$s foi alterada" + "A tua mensagem não foi enviada porque a identidade verificada de %1$s foi reposta" "Enviar mensagem mesmo assim" "%1$s está a utilizar um ou mais dispositivos não verificados. Podes enviar a mensagem na mesma, ou podes cancelar por agora e tentar novamente mais tarde, depois de %2$s ter verificado todos os seus dispositivos." "A sua mensagem não foi enviada porque %1$s não verificou todos os dispositivos" @@ -325,25 +381,30 @@ Razão: %1$s." "A sua mensagem não foi enviada porque não verificou um ou mais dos seus dispositivos" "Falha ao processar multimédia para carregamento, por favor tente novamente." "Não foi possível obter os detalhes de utilizador." + "Mensagem em %1$s" + "Expandir" + "Reduzir" + "Já estás a ver esta sala!" "%1$s de %2$s" "%1$s mensagens afixadas" "A carregar mensagem…" "Ver todas" "Conversa" - "Pedido de adesão enviado" "Partilhar localização" "Partilhar a minha localização" "Abrir no Apple Maps" "Abrir no Google Maps" "Abrir no OpenStreetMap" "Partilhar este local" - "Mensagem não enviada porque a identidade verificada de %1$s foi alterada." + "Mensagem não enviada porque a identidade verificada de %1$s foi reposta." "Mensagem não enviada porque %1$s não verificou todos os dispositivos." "Mensagem não enviada porque não verificou um ou mais dos seus dispositivos." "Localização" "Versão: %1$s (%2$s)" "pt" "O histórico de mensagens não está disponível neste dispositivo" + "É necessário verificares este dispositivos para acederes a mensagens antigas" + "Não tens acesso a esta mensagem" "Impossível decifrar mensagem" "Esta mensagem foi bloqueada ou porque ainda não verificaste este dispositivo ou porque o remetente necessita de verificar a tua identidade." 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 8a77f7d567d..4f370e6924d 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -26,7 +26,7 @@ "Citit de%1$s" "Atingeți pentru a le afișa pe toate" - "Îndepărtați reacția cu %1$s" + "Îndepărtați reacția cu %1$s" "Trimiteți fișiere" "Afișați parola" "Începeți un apel" @@ -123,6 +123,8 @@ "Vedeți în cameră" "Vedeți sursă" "Da" + "Serverul dvs. acceptă acum un protocol nou, mai rapid. Deconectați-vă și conectați-vă din nou pentru a face upgrade acum. Dacă faceți acest lucru acum, vă va ajuta să evitați o deconectare forțată atunci când vechiul protocol este eliminat ulterior." + "Upgrade disponibil" "Despre" "Politică de utilizare rezonabilă" "Setări avansate" @@ -169,6 +171,11 @@ Motiv:%1$s." "Deschis" "Linkul a fost copiat în clipboard" "Se încarcă…" + + "%d altul" + "%d alții" + "%d alții" + "%1$d membru" "%1$d membri" @@ -320,7 +327,6 @@ Motiv:%1$s." "Se încarcă mesajul…" "Vedeți toate" "Chat" - "Cererea de alăturare a fost trimisă" "Partajați locația" "Distribuiți locația mea" "Deschideți în Apple Maps" 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 db85cb90f87..93fb5cb1701 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -1,5 +1,6 @@ + "Аватар" "Удалить" "Введена %1$d цифра" @@ -7,11 +8,13 @@ "Введено много цифр" "Скрыть пароль" + "Присоединиться к звонку" "Перейти вниз" "Только упоминания" "Звук отключен" "Страница %1$d" "Приостановить" + "Голосовое сообщение, длительность: %1$s, текущая позиция: %2$s" "Поле PIN-кода" "Воспроизвести" "Опрос" @@ -26,12 +29,13 @@ "Прочитано %1$s" "Нажмите, чтобы показать все" - "Удалить реакцию с %1$s" + "Удалить реакцию с %1$s" "Отправить файлы" "Показать пароль" "Начать звонок" "Меню пользователя" "Подробнее" + "Голосовое сообщение, продолжительность: %1$s" "Записать голосовое сообщение." "Остановить запись" "Разрешить" @@ -58,9 +62,11 @@ "Отключить" "Отключить учётную запись" "Отклонить" + "Отклонить и заблокировать" "Удалить опрос" "Отключить" "Отменить" + "Отклонить" "Готово" "Редактировать" "Изменить подпись" @@ -102,8 +108,11 @@ "Удалить сообщение" "Ответить" "Ответить в теме" + "Отчет" "Сообщить об ошибке" "Пожаловаться на содержание" + "Пожаловаться на беседу" + "Комната отчетов" "Сбросить" "Сбросить идентификацию" "Повторить" @@ -131,6 +140,8 @@ "Показать источник" "Да" "Да, попробуйте еще раз" + "Теперь ваш сервер поддерживает новый, более быстрый протокол. Чтобы обновить его прямо сейчас, выйдите и войдите в свою учётную запись снова. Сделав это сейчас, вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола." + "Доступно обновление" "О приложении" "Политика допустимого использования" "Добавление подписи" @@ -145,7 +156,9 @@ "Скопировано в буфер обмена" "Авторское право" "Создание комнаты…" + "Запрос отменен" "Покинул комнату" + "Приглашение отклонено" "Тёмное" "Ошибка расшифровки" "Для разработчика" @@ -158,6 +171,7 @@ "Редактирование" "Редактирование подписи" "%1$s%2$s" + "Пустой файл" "Шифрование" "Шифрование включено" "Введите свой PIN-код" @@ -182,9 +196,15 @@ "Идентификатор Matrix ID не найден, приглашение может быть не получено." "Покидание комнаты" "Светлое" + "Строка скопирована в буфер обмена" "Ссылка скопирована в буфер обмена" "Загрузка…" "Загрузить больше…" + + "%d" + "%d другие" + "%d другие" + "%1$d участник" "%1$d участников" @@ -196,8 +216,10 @@ "Сообщение удалено" "Современный" "Выкл. звук" + "%1$s (%2$s)" "Ничего не найдено" "Название комнаты отсутствует" + "Не зашифровано" "Не в сети" "Лицензии с открытым исходным кодом" "или" @@ -206,6 +228,7 @@ "Постоянная ссылка" "Разрешение" "Закрепленный" + "Пожалуйста, проверьте подключение к Интернету" "Подождите…" "Вы действительно хотите завершить данный опрос?" "Опрос: %1$s" @@ -219,6 +242,7 @@ "Политика конфиденциальности" "Реакция" "Реакции" + "Причина" "Ключ восстановления" "Обновление…" "Отвечает на %1$s" @@ -246,6 +270,7 @@ "Поделился местоположением" "Выход…" "Что-то пошло не так" + "Мы столкнулись с проблемой. Пожалуйста, попробуйте еще раз." "Чат запускается…" "Стикер" "Успешно" @@ -260,7 +285,7 @@ "Невозможно расшифровать" "Отправлено с незащищенного устройства" "Вы не имеете доступа к этому сообщению" - "Подтвержденная личность отправителя изменилась" + "Подтвержденная личность отправителя была сброшена" "Не удалось отправить приглашения одному или нескольким пользователям." "Не удалось отправить приглашение(я)" "Разблокировать" @@ -274,16 +299,24 @@ "Проверено" "Подтверждение устройства" "Подтвердить личность" + "Подтвердить пользователя" "Видео" "Голосовое сообщение" "Ожидание…" "Ожидание ключа расшифровки" "Вы" - "Судя по всему, идентификатор %1$s изменился. %2$s" + "Идентификатор %1$s изменился. %2$s" "Пользователь %1$s сменил имя пользователя на %2$s. %3$s" "(%1$s)" + "%1$s была сброшена." "%1$s’s %2$s подтвержденная личность изменилась. %3$s" "Вывод верификации" + "Ссылка %1$s ведет вас на другой сайт %2$s + +Вы действительно хотите продолжить?" + "Перепроверьте эту ссылку" + "Сообщение о комнате" + "Пожаловался и покинул комнату" "Подтверждение" "Ошибка" "Успешно" @@ -300,6 +333,7 @@ "У %1$s нет разрешения на доступ к вашему местоположению. Вы можете разрешить доступ в Настройках." "У %1$s нет разрешения на доступ к вашему местоположению. Разрешите доступ ниже." "%1$s не имеет разрешения на доступ к вашему микрофону. Разрешите доступ к записи голосового сообщения." + "Это может быть связано с проблемами сети или сервера." "Такой адрес комнаты уже существует, попробуйте отредактировать поле адреса комнаты или изменить название комнаты" "Некоторые символы не допускаются. Поддерживаются только буквы, цифры и следующие символы! $ & \'() * +/; =? @ [] - . _" "Некоторые сообщения не были отправлены" @@ -330,7 +364,7 @@ "Не можете подтвердить? Перейдите в свою учетную запись, чтобы сбросить свою идентификацию." "Отозвать статус и отправить" "Вы можете либо отозвать свой статус подтверждения и всё равно отправить это сообщение, либо отменить его сейчас и повторить попытку после повторного подтверждения %1$s." - "Ваше сообщение не было отправлено, потому что изменилась подтвержденная личность %1$s" + "Ваше сообщение не было отправлено, потому что подтвержденная личность %1$s была сброшена" "Отправь сообщение в любом случае" "%1$s использует одно или несколько непроверенных устройств. Вы все равно можете отправить сообщение или отменить его пока и повторить попытку позже %2$s, проверив все устройства пользователя." "Ваше сообщение не было отправлено, потому что %1$s не проверил одно или несколько устройств" @@ -338,19 +372,19 @@ "Ваше сообщение не было отправлено, поскольку вы не подтвердили одно или несколько своих устройств." "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Не удалось получить данные о пользователе" + "Сообщение в %1$s" "%1$s из %2$s" "%1$s Закрепленные сообщения" "Загрузка сообщения…" "Посмотреть все" "Чат" - "Запрос на присоединение отправлен" "Поделиться местоположением" "Поделиться моим местоположением" "Открыть в Apple Maps" "Открыть в Google Maps" "Открыть в OpenStreetMap" "Поделиться этим местоположением" - "Сообщение не отправлено, потому что верифицированная личность %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 ef239dad16b..250691e7037 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -1,5 +1,7 @@ + "Pridať reakciu: %1$s" + "Obrázok" "Vymazať" "%1$d zadaná číslica" @@ -7,11 +9,13 @@ "%1$d zadaných číslic" "Skryť heslo" + "Pripojiť sa k hovoru" "Prejsť na spodok" "Iba zmienky" "Stlmené" "Strana %1$d" "Pozastaviť" + "Hlasová správa, dĺžka:%1$s, aktuálna pozícia: %2$s" "Pole PIN" "Prehrať" "Anketa" @@ -26,12 +30,15 @@ "Prečítal/a %1$s" "Ťuknutím zobrazíte všetko" + "Odstrániť reakciu s %1$s" "Odstrániť reakciu s %1$s" "Odoslať súbory" "Zobraziť heslo" "Začať hovor" "Používateľské menu" + "Zobraziť profilový obrázok" "Zobraziť podrobnosti" + "Hlasová správa, dĺžka: %1$s" "Nahrať hlasovú správu." "Zastaviť nahrávanie" "Prijať" @@ -58,9 +65,11 @@ "Deaktivovať" "Deaktivovať účet" "Odmietnuť" + "Odmietnuť a zablokovať" "Odstrániť anketu" "Vypnúť" "Zahodiť" + "Zamietnuť" "Hotovo" "Upraviť" "Upraviť titulok" @@ -102,8 +111,11 @@ "Odstrániť správu" "Odpovedať" "Odpovedať vo vlákne" + "Nahlásiť" "Nahlásiť chybu" "Nahlásiť obsah" + "Nahlásiť konverzáciu" + "Nahlásiť miestnosť" "Obnoviť" "Obnoviť identitu" "Skúsiť znova" @@ -131,10 +143,13 @@ "Zobraziť zdroj" "Áno" "Áno, skúsiť to znova" + "Váš server teraz podporuje nový, rýchlejší protokol. Odhláste sa a prihláste sa znova, aby ste mohli aktualizovať. Ak to urobíte teraz, pomôže vám vyhnúť sa nútenému odhláseniu, keď sa starý protokol neskôr odstráni." + "Aktualizácia je k dispozícii" "O aplikácii" "Zásady prijateľného používania" "Pridáva sa titulok" "Pokročilé nastavenia" + "obrázok" "Analytika" "Vzhľad" "Zvuk" @@ -145,7 +160,9 @@ "Skopírované do schránky" "Autorské práva" "Vytváranie miestnosti…" + "Žiadosť bola zrušená" "Opustil/a miestnosť" + "Pozvánka bola odmietnutá" "Tmavý" "Chyba dešifrovania" "Možnosti pre vývojárov" @@ -158,6 +175,7 @@ "Upravuje sa" "Úprava titulku" "* %1$s %2$s" + "Prázdny súbor" "Šifrovanie" "Šifrovanie zapnuté" "Zadajte svoj PIN" @@ -182,9 +200,15 @@ Dôvod: %1$s." "Toto Matrix ID sa nedá nájsť, takže pozvánka nemusí byť prijatá." "Opustenie miestnosti" "Svetlý" + "Riadok skopírovaný do schránky" "Odkaz bol skopírovaný do schránky" "Načítava sa…" "Načítava sa viac…" + + "%d ďalší" + "%d ďalší" + "%d ďalších" + "%1$d člen" "%1$d členovia" @@ -196,8 +220,10 @@ Dôvod: %1$s." "Správa odstránená" "Moderné" "Stlmiť" + "%1$s (%2$s)" "Žiadne výsledky" "Žiadny názov miestnosti" + "Nešifrované" "Offline" "Licencie s otvoreným zdrojom" "alebo" @@ -206,6 +232,7 @@ Dôvod: %1$s." "Trvalý odkaz" "Povolenie" "Pripnuté" + "Skontrolujte si prosím vaše pripojenie k internetu" "Prosím, počkajte…" "Ste si istí, že chcete ukončiť túto anketu?" "Anketa: %1$s" @@ -219,8 +246,14 @@ Dôvod: %1$s." "Zásady ochrany osobných údajov" "Reakcia" "Reakcie" + "Dôvod" "Kľúč na obnovenie" "Obnovuje sa…" + + "%1$d odpoveď" + "%1$d odpovede" + "%1$d odpovedí" + "Odpoveď na %1$s" "Nahlásiť chybu" "Nahlásiť problém" @@ -240,12 +273,14 @@ Dôvod: %1$s." "Odosiela sa…" "Odoslanie zlyhalo" "Odoslané" + ". " "Server nie je podporovaný" "URL adresa servera" "Nastavenia" "Zdieľaná poloha" "Odhlasovanie" "Niečo sa pokazilo" + "Vyskytol sa problém. Skúste to prosím znova." "Spustenie konverzácie…" "Nálepka" "Úspech" @@ -260,7 +295,7 @@ Dôvod: %1$s." "Nie je možné dešifrovať" "Odoslané z nezabezpečeného zariadenia" "Nemáte prístup k tejto správe" - "Overená totožnosť odosielateľa sa zmenila" + "Overená totožnosť odosielateľa bola obnovená" "Pozvánky nebolo možné odoslať jednému alebo viacerým používateľom." "Nie je možné odoslať pozvánku/ky" "Odomknúť" @@ -274,16 +309,24 @@ Dôvod: %1$s." "Overené" "Overiť zariadenie" "Overiť totožnosť" + "Overiť používateľa" "Video" "Hlasová správa" "Čaká sa…" "Čaká sa na dešifrovací kľúč" "Vy" - "Zdá sa, že totožnosť používateľa %1$s sa zmenila.%2$s" - "Zdá sa, že identita %2$s používateľa %1$s sa zmenila. %3$s" + "Totožnosť používateľa %1$s sa obnovila.%2$s" + "Totožnosť používateľa %1$s %2$s bola obnovená. %3$s" "(%1$s)" - "Overená identita používateľa %1$s %2$s sa zmenila. %3$s" + "Totožnosť používateľa %1$s bola obnovená." + "Totožnosť používateľa %1$s %2$s bola obnovená. %3$s" "Zrušiť overenie" + "Odkaz %1$s vás presmeruje na inú stránku %2$s + +Naozaj chcete pokračovať?" + "Dôkladne skontrolujte tento odkaz" + "Miestnosť nahlásená" + "Nahlásili ste a opustili ste miestnosť" "Potvrdenie" "Chyba" "Úspech" @@ -296,14 +339,17 @@ Dôvod: %1$s." "Načítanie správ zlyhalo" "%1$s nemohol získať prístup k vašej polohe. Skúste to prosím neskôr." "Nepodarilo sa nahrať hlasovú správu." + "Miestnosť už neexistuje alebo pozvánka už nie je platná." "Správa sa nenašla" "%1$s nemá povolenie na prístup k vašej polohe. Prístup môžete zapnúť v Nastaveniach." "%1$s nemá povolenie na prístup k vašej polohe. Povoľte prístup nižšie." "%1$s nemá povolenie na prístup k vášmu mikrofónu. Povoľte prístup na nahrávanie hlasovej správy." + "Môže to byť spôsobené problémami so sieťou alebo serverom." "Táto adresa miestnosti už existuje, skúste upraviť pole adresy miestnosti alebo zmeňte názov miestnosti" "Niektoré znaky nie sú povolené. Podporované sú iba písmená, číslice a nasledujúce symboly ! $ & \'() * +/; =? @ [] - . _" "Niektoré správy neboli odoslané" "Prepáčte, vyskytla sa chyba" + "Odosielateľ udalosti sa nezhoduje s vlastníkom zariadenia, ktoré ju odoslalo." "Pravosť tejto šifrovanej správy nie je možné zaručiť na tomto zariadení." "Šifrované predtým overeným používateľom." "Nie je šifrované." @@ -314,6 +360,10 @@ Dôvod: %1$s." "Ahoj, porozprávajte sa so mnou na %1$s: %2$s" "%1$s Android" "Zúrivo potriasť pre nahlásenie chyby" + "%1$s: %2$s" + "Možnosti" + "Odstrániť %1$s" + "Nastavenia" "Nepodarilo sa vybrať médium, skúste to prosím znova." "Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." @@ -329,7 +379,7 @@ Dôvod: %1$s." "Chystáte sa prejsť na svoj %1$s účet, aby ste obnovili svoju identitu. Potom budete vrátení späť do aplikácie." "Neviete potvrdiť? Prejdite do svojho účtu a obnovte svoju identitu." "Odvolať overenie a odoslať" - "Svoje overenie môžete odvolať a odoslať túto správu aj tak, alebo ju môžete zatiaľ zrušiť a po opätovnom overení to skúsiť znova %1$s ." + "Svoje overenie môžete odvolať a odoslať túto správu aj tak, alebo ju môžete zatiaľ zrušiť a po opätovnom overení to skúsiť znova %1$s." "Vaša správa nebola odoslaná, pretože sa zmenila overená totožnosť používateľa %1$s." "Odoslať správu aj tak" "%1$s používa jedno alebo viac neoverených zariadení. Správu môžete odoslať aj tak, alebo ju môžete zatiaľ zrušiť a skúsiť to znova neskôr po %2$s overení všetkých zariadení." @@ -338,12 +388,15 @@ Dôvod: %1$s." "Vaša správa nebola odoslaná, pretože ste neoverili jedno alebo viac svojich zariadení" "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa získať údaje o používateľovi" + "Správa v %1$s" + "Rozbaliť" + "Zmenšiť" + "Už si prezeráte túto miestnosť!" "%1$s z %2$s" "%1$s Pripnutých správ" "Načítava sa správa…" "Zobraziť všetko" "Konverzácia" - "Žiadosť o vstup odoslaná" "Zdieľať polohu" "Zdieľať moju polohu" "Otvoriť v Apple Maps" 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 44f84c1bd2b..046da66d2d9 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -1,16 +1,19 @@ + "Avatar" "Radera" "%1$d siffra angiven" "%1$d siffror angivna" "Dölj lösenord" + "Anslut till samtal" "Hoppa till botten" "Endast omnämningar" "Tystad" "Sida %1$d" "Pausa" + "Röstmeddelande, varaktighet:%1$s, nuvarande position: %2$s" "PIN-fält" "Spela upp" "Omröstning" @@ -24,14 +27,17 @@ "Läst av %1$s" "Tryck för att visa alla" - "Ta bort reaktionen med %1$s" + "Ta bort reaktionen med %1$s" "Skicka filer" "Visa lösenord" "Starta ett samtal" "Användarmeny" + "Visa detaljer" + "Röstmeddelande, varaktighet: %1$s" "Spela in röstmeddelande." "Stoppa inspelning" "Godkänn" + "Lägg till bildtext" "Lägg till i tidslinjen" "Tillbaka" "Ring" @@ -45,18 +51,23 @@ "Bekräfta lösenord" "Fortsätt" "Kopiera" + "Kopiera bildtext" "Kopiera länk" "Kopiera länk till meddelande" + "Kopiera text" "Skapa" "Skapa ett rum" "Inaktivera" "Inaktivera konto" "Neka" + "Avvisa och blockera" "Radera omröstning" "Inaktivera" "Kassera" + "Avfärda" "Klar" "Redigera" + "Redigera bildtext" "Redigera omröstning" "Aktivera" "Avsluta omröstning" @@ -91,10 +102,15 @@ "Reagera" "Avvisa" "Ta bort" + "Ta bort bildtext" + "Ta bort meddelande" "Svara" "Svara i tråd" + "Anmäl" "Rapportera bugg" "Rapportera innehåll" + "Anmäl konversation" + "Anmäl rum" "Återställ" "Återställ identitet" "Försök igen" @@ -121,8 +137,12 @@ "Visa i tidslinjen" "Visa källkod" "Ja" + "Ja, försök igen" + "Din server stöder nu ett nytt, snabbare protokoll. Logga ut och logga in igen för att uppgradera nu. Om du gör detta nu hjälper du dig att undvika en tvingad utloggning när det gamla protokollet tas bort senare." + "Uppgradering tillgänglig" "Om" "Policy för godtagbar användning" + "Lägga till bildtext" "Avancerade inställningar" "Analysdata" "Utseende" @@ -134,16 +154,23 @@ "Kopierad till klippbordet" "Upphovsrätt" "Skapar rum …" + "Begäran avbruten" "Lämnade rummet" + "Inbjudan avvisad" "Mörkt" "Avkrypteringsfel" "Utvecklaralternativ" "Enhets-ID" "Direktchatt" "Visa inte detta igen" + "Nedladdningen misslyckades" + "Laddar ner" "(redigerad)" "Redigerar" + "Redigera bildtext" "* %1$s %2$s" + "Tom fil" + "Kryptering" "Kryptering aktiverad" "Ange din PIN-kod" "Fel" @@ -155,8 +182,11 @@ Anledning:%1$s." "Favorit" "Favoriter" "Fil" + "Fil borttagen" + "Fil sparad" "Fil sparad i Download" "Vidarebefordra meddelande" + "Används ofta" "GIF" "Bild" "Som svar på %1$s" @@ -164,8 +194,14 @@ Anledning:%1$s." "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 …" + "Laddar mer …" + + "%d annan" + "%d andra" + "%1$d medlem" "%1$d medlemmar" @@ -176,8 +212,10 @@ Anledning:%1$s." "Meddelande borttaget" "Modernt" "Tysta" + "%1$s (%2$s)" "Inga resultat" "Inget rumsnamn" + "Inte krypterad" "Frånkopplad" "Licenser för öppen källkod" "eller" @@ -186,6 +224,7 @@ Anledning:%1$s." "Permalänk" "Behörighet" "Fäst" + "Vänligen kontrollera din internetanslutning" "Vänligen vänta …" "Är du säker på att du vill avsluta den här omröstningen?" "Omröstning: %1$s" @@ -198,6 +237,7 @@ Anledning:%1$s." "Integritetspolicy" "Reaktion" "Reaktioner" + "Orsak" "Återställningsnyckel" "Uppdaterar …" "Svarar till %1$s" @@ -225,6 +265,7 @@ Anledning:%1$s." "Delade plats" "Loggar ut" "Något gick fel" + "Vi stötte på ett problem. Vänligen försök igen." "Startar chatt …" "Dekal" "Lyckades" @@ -237,11 +278,14 @@ Anledning:%1$s." "Ämne" "Vad handlar det här rummet om?" "Kan inte avkryptera" + "Skickad från en osäker enhet" "Du har inte tillgång till det här meddelandet" + "Avsändarens verifierade identitet återställdes" "Inbjudan kunde inte skickas till en eller flera användare." "Kunde inte skicka inbjudningar" "Lås upp" "Avtysta" + "Samtal som inte stöds" "Händelse som inte stöds" "Användarnamn" "Verifiering avbruten" @@ -249,14 +293,25 @@ Anledning:%1$s." "Verifiering misslyckades" "Verifierad" "Verifiera enheten" + "Verifiera identitet" + "Verifiera användare" "Video" "Röstmeddelande" "Väntar …" "Väntar på detta meddelande" "Du" - "Identitet för %1$s verkar ha ändrats. %2$s" - "Identitet för %1$s %2$s verkar ha förändrats. %3$s" + "Identitet för %1$s återställdes. %2$s" + "Identitet för %1$s %2$s återställdes. %3$s" "(%1$s)" + "Verifierad identitet för %1$s återställdes." + "Verifierad identitet för %1$s %2$s återställdes.%3$s" + "Återkalla verifiering" + "Länken %1$s tar dig till en annan webbplats %2$s + +Är du säker på att du vill fortsätta?" + "Dubbelkolla den här länken" + "Rum anmält" + "Anmälde och lämnade rummet" "Bekräftelse" "Fel" "Lyckades" @@ -273,6 +328,9 @@ Anledning:%1$s." "%1$s är inte behörig att komma åt din plats. Du kan aktivera åtkomst i Inställningar." "%1$s är inte behörig att komma åt din plats. Aktivera åtkomst nedan." "%1$s är inte behörig att komma åt din mikrofon. Aktivera åtkomst för att spela in ett röstmeddelande." + "Detta kan bero på nätverks- eller serverproblem." + "Den här rumsadressen finns redan. Försök att redigera adressfältet för rummet eller ändra rummets namn" + "Vissa tecken är inte tillåtna. Endast bokstäver, siffror och följande symboler stöds ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Vissa meddelanden har inte skickats" "Tyvärr, ett fel uppstod" "Detta krypterade meddelandes äkthet kan inte garanteras på den här enheten." @@ -286,6 +344,7 @@ Anledning:%1$s." "%1$s Android" "Raseriskaka för att rapportera bugg" "Misslyckades att välja media, vänligen pröva igen." + "Bildtexter kanske inte är synliga för personer som använder äldre appar." "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen." "Misslyckades att ladda upp media, vänligen pröva igen." "Tryck på ett meddelande och välj ”%1$s” för att inkludera det här." @@ -299,7 +358,7 @@ Anledning:%1$s." "Kan du inte bekräfta? Gå till ditt konto för att återställa din identitet." "Dra tillbaka verifieringen och skicka" "Du kan dra tillbaka din verifiering och skicka det här meddelandet ändå, eller så kan du avbryta för tillfället och försöka igen senare efter att ha verifierat %1$s igen." - "Ditt meddelande skickades inte eftersom verifierad identitet för %1$s har ändrats" + "Ditt meddelande skickades inte eftersom verifierad identitet för %1$s återställdes" "Skicka meddelandet ändå" "%1$s använder en eller flera overifierade enheter. Du kan skicka meddelandet ändå, eller så kan du avbryta för tillfället och försöka igen senare efter att %2$s har verifierat alla sina enheter." "Ditt meddelande skickades inte eftersom %1$s inte har verifierat alla enheter" @@ -307,6 +366,7 @@ Anledning:%1$s." "Ditt meddelande skickades inte eftersom du inte har verifierat en eller flera av dina enheter" "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen." "Kunde inte hämta användarinformation" + "Meddelande i %1$s" "%1$s av %2$s" "%1$s Fästa meddelanden" "Laddar meddelande …" @@ -318,10 +378,15 @@ Anledning:%1$s." "Öppna i Google Maps" "Öppna i OpenStreetMap" "Dela den här platsen" - "Meddelandet skickades inte eftersom verifierad identitet för %1$s har ändrats." + "Meddelandet skickades inte eftersom verifierad identitet för %1$s återställdes." "Meddelandet skickades inte eftersom %1$s inte har verifierat alla enheter." "Meddelandet skickades inte eftersom du inte har verifierat en eller flera av dina enheter." "Plats" "Version: %1$s (%2$s)" "sv" + "Historiska meddelanden är inte tillgängliga på den här enheten" + "Du måste verifiera den här enheten för åtkomst till historiska meddelanden" + "Du har inte tillgång till det här meddelandet" + "Det gick inte att dekryptera meddelandet" + "Det här meddelandet blockerades antingen för att du inte verifierade din enhet eller för att avsändaren måste verifiera din identitet." diff --git a/libraries/ui-strings/src/main/res/values-tr/translations.xml b/libraries/ui-strings/src/main/res/values-tr/translations.xml new file mode 100644 index 00000000000..fdec006bb32 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-tr/translations.xml @@ -0,0 +1,368 @@ + + + "Sil" + + "%1$d basamak girildi" + "%1$d basamak girildi" + + "Şifreyi gizle" + "Aşağıya atla" + "Yalnızca bahsetmeler" + "Sessiz" + "Sayfa %1$d" + "Duraklat" + "PIN alanı" + "Oynat" + "Anket" + "Anket sona erdi" + "%1$s ile tepki verin" + "Diğer emojilerle tepki verin" + "Okuyan %1$s ve %2$s" + + "%1$s ve %2$d diğer kişi tarafından okundu" + "%1$s ve %2$d diğer kişi tarafından okundu" + + "Okuyan %1$s" + "Tümünü göstermek için dokunun" + "Tepkimeyi kaldır %1$s" + "Dosyaları gönder" + "Şifreyi göster" + "Bir arama başlatın" + "Kullanıcı menüsü" + "Ayrıntıları görüntüle" + "Sesli mesajı kaydedin." + "Kaydı durdur" + "Kabul et" + "Açıklama ekle" + "Zaman çizelgesine ekle" + "Geri" + "Çağrı" + "İptal" + "Şimdilik iptal et" + "Fotoğraf seç" + "Temizle" + "Kapat" + "Doğrulamayı tamamlayın" + "Onayla" + "Şifreyi onayla" + "Devam et" + "Kopyala" + "Açıklamayı kopyala" + "Bağlantıyı kopyala" + "Bağlantıyı mesaja kopyala" + "Metni kopyala" + "Oluştur" + "Bir oda oluştur" + "Devre dışı bırak" + "Hesabı devre dışı bırak" + "Reddet" + "Anketi Sil" + "Devre dışı" + "Vazgeç" + "Bitti" + "Düzenle" + "Açıklamayı düzenle" + "Anketi düzenle" + "Etkinleştir" + "Anketi sonlandır" + "PIN girin" + "Parolanızı mı unuttunuz?" + "İleri" + "Geri dön" + "Yoksay" + "Davet et" + "İnsanları davet et" + "İnsanları davet et %1$s" + "İnsanları davet et %1$s" + "Davetiyeler" + "Katıl" + "Daha fazla bilgi" + "Ayrıl" + "Sohbeti bırak" + "Odadan ayrıl" + "Daha fazla yükle" + "Hesabı yönet" + "Cihazları yönet" + "Mesaj" + "Sonraki" + "Hayır" + "Şimdi değil" + "TAMAM" + "Ayarlar" + "İle aç" + "Pin" + "Hızlı cevap" + "Alıntı" + "Tepki" + "Reddet" + "Kaldır" + "Açıklamayı kaldır" + "Mesajı kaldır" + "Yanıtla" + "Konuya cevap ver" + "Hata bildir" + "İçeriği bildir" + "Sıfırla" + "Kimliği sıfırla" + "Yeniden dene" + "Şifre çözmeyi tekrar deneyin" + "Kaydet" + "Ara" + "Gönder" + "Mesaj gönder" + "Paylaş" + "Linki paylaş" + "Göster" + "Tekrar oturum açın" + "Oturumu kapat" + "Yine de çıkış yap" + "Atla" + "Başlat" + "Sohbeti başlat" + "Doğrulamayı başlat" + "Haritayı yüklemek için dokunun" + "Fotoğraf çek" + "Seçenekler için dokunun" + "Tekrar deneyin" + "Sabitlemeyi kaldır" + "Zaman çizelgesinde görüntüle" + "Kaynağı görüntüle" + "Evet" + "Evet, tekrar dene" + "Sunucunuz artık yeni, daha hızlı bir protokolü destekliyor. Şimdi oturumu kapatıp tekrar oturum açarak yükseltme yapın. Bunu şimdi yapmak, eski protokol daha sonra kaldırıldığında zorunlu oturum kapatmayı önlemenize yardımcı olacaktır." + "Yükseltme mevcut" + "Hakkında" + "Kabul edilebilir kullanım politikası" + "Açıklama ekleme" + "Gelişmiş Ayarlar" + "Analizler" + "Görünüm" + "Ses" + "Engellenen kullanıcılar" + "Kabarcıklar" + "Çağrı başladı" + "Sohbet yedekleme" + "Panoya kopyalandı" + "Telif Hakkı" + "Oda yaratmak…" + "Sol oda" + "Koyu" + "Şifre çözme hatası" + "Geliştirici seçenekleri" + "Cihaz Kimliği" + "Doğrudan sohbet" + "Bunu bir daha gösterme" + "İndirme başarısız oldu" + "İndiriliyor" + "(düzenlendi)" + "Düzenleme" + "Açıklamayı düzenleme" + "* %1$s %2$s" + "Şifreleme" + "Şifreleme etkin" + "PIN\'inizi girin" + "Hata" + "Bir hata oluştu, yeni mesajlar için bildirim alamayabilirsiniz. Lütfen ayarlardan bildirimlerle ilgili sorunları giderin. + +Neden: %1$s." + "Herkes" + "Başarısız" + "Favori" + "Favorilere eklendi" + "Dosya" + "Dosya silindi" + "Dosya kaydedildi" + "Dosya İndirilenler\'e kaydedildi" + "Mesajı ilet" + "Sık kullanılanlar" + "GIF" + "Resim" + "Cevap olarak %1$s" + "APK\'yı yükleyin" + "Bu Matrix Kimliği bulunamıyor, bu nedenle davet alınmayabilir." + "Odadan ayrılma" + "Aydınlık" + "Bağlantı panoya kopyalandı" + "Yükleniyor…" + "Daha fazla yükleniyor…" + + "%d diğer" + "%d diğer" + + + "%1$d üye" + "%1$d üye" + + "Mesaj" + "Mesaj eylemleri" + "Mesaj düzeni" + "Mesaj kaldırıldı" + "Modern" + "Sessiz" + "%1$s (%2$s)" + "Sonuç yok" + "Oda adı yok" + "Çevrimdışı" + "Açık kaynak lisansları" + "veya" + "Şifre" + "Kişiler" + "Kalıcı bağlantı" + "İzin" + "Sabitlendi" + "Lütfen bekleyin…" + "Bu anketi sonlandırmak istediğinizden emin misiniz?" + "Anket: %1$s" + "Toplam oy: %1$s" + "Sonuçlar anket sona erdikten sonra gösterilecektir" + + "%d oy" + "%d oy" + + "Gizlilik Politikası" + "Tepki" + "Tepkiler" + "Kurtarma anahtarı" + "Yenileniyor…" + "Cevaplamak için %1$s" + "Hata bildir" + "Sorun bildir" + "Rapor gönderildi" + "Zengin metin editörü" + "Oda" + "Oda adı" + "örn. proje adınız" + "Kaydedilen değişiklikler" + "Kaydediliyor" + "Ekran kilidi" + "Birini arayın" + "Arama sonuçları" + "Güvenlik" + "Tarafından görüldü" + "Şuraya gönder" + "Gönderiliyor…" + "Gönderme başarısız oldu" + "Gönderildi" + "Sunucu desteklenmiyor" + "Sunucu URL\'si" + "Ayarlar" + "Paylaşılan konum" + "Oturumu kapatma" + "Bir şeyler ters gitti" + "Sohbet başlatılıyor…" + "Çıkartma" + "Başarılı" + "Öneriler" + "Senkronizasyon" + "Sistem" + "Metin" + "Üçüncü taraf bildirimleri" + "Konu" + "Konu" + "Bu oda ne hakkında?" + "Şifre çözülemiyor" + "Güvenli olmayan bir cihazdan gönderildi" + "Bu mesaja erişiminiz yok" + "Gönderenin doğrulanmış kimliği değişti" + "Davetler bir veya daha fazla kullanıcıya gönderilemedi." + "Davetiye(ler) gönderilemedi" + "Kilidi aç" + "Sesi aç" + "Desteklenmeyen çağrı" + "Desteklenmeyen etkinlik" + "Kullanıcı adı" + "Doğrulama iptal edildi" + "Doğrulama tamamlandı" + "Doğrulama başarısız" + "Doğrulandı" + "Cihazı doğrula" + "Kimliği doğrula" + "Kullanıcıyı Doğrula" + "Video" + "Sesli Mesaj" + "Bekleniyor…" + "Bu mesajı bekliyorum" + "Sen" + "%1$s kişinin kimliği değişmiş gibi görünüyor. %2$s" + "%1$s\'ın %2$s kimliği değişmiş gibi görünüyor. %3$s" + "(%1$s)" + "%1$s kullanıcısının doğrulanmış kimliği değişti." + "%1$s kullanıcısının %2$s doğrulanmış kimliği değişti. %3$s" + "Doğrulamayı iptal et" + "Onaylama" + "Hata" + "Başarılı" + "Uyarı" + "Değişiklikleriniz kaydedilmedi. Geri dönmek istediğinden emin misin?" + "Değişiklikleri Kaydet?" + "Ana sunucunuzun Matrix Authentication Service ve hesap oluşturmayı destekleyecek şekilde güncellenmesi gerekiyor." + "Kalıcı bağlantı oluşturulamadı" + "%1$s harita yüklenemedi. Lütfen daha sonra tekrar deneyin." + "Mesajlar yüklenemedi" + "%1$s konumunuza erişemedi. Lütfen daha sonra tekrar deneyin." + "Sesli mesajınız yüklenemedi." + "Mesaj bulunamadı" + "%1$s konumunuza erişim iznine sahip değil. Erişimi Ayarlar\'dan etkinleştirebilirsiniz." + "%1$s konumunuza erişim iznine sahip değil. Aşağıdan erişimi etkinleştirin." + "%1$s mikrofonunuza erişim izni yok. Sesli mesaj kaydetmek için erişimi etkinleştirin." + "Bunun nedeni ağ veya sunucu sorunları olabilir." + "Bu oda adresi zaten mevcut. Lütfen oda adres alanını düzenlemeyi deneyin veya oda adını değiştirin" + "Bazı karakterlere izin verilmez. Yalnızca harfler, rakamlar ve aşağıdaki semboller desteklenir ! $ &amp; \' ( ) * + / ; = ? @ [ ] - . _" + "Bazı mesajlar gönderilmedi" + "Üzgünüz, bir hata oluştu" + "Bu şifrelenmiş mesajın doğruluğu bu cihazda garanti edilemez." + "Daha önce doğrulanmış bir kullanıcı tarafından şifrelenmiştir." + "Şifrelenmemiş." + "Bilinmeyen veya silinmiş bir cihaz tarafından şifrelenmiştir." + "Sahibi tarafından doğrulanmamış bir cihaz tarafından şifrelenmiştir." + "Doğrulanmamış bir kullanıcı tarafından şifrelenmiştir." + "🔐️ Bana katılın %1$s" + "Hey, benimle konuş %1$s: %2$s" + "%1$s Android" + "Hata bildirmek için Rageshake" + "Medya seçilemedi, lütfen tekrar deneyin." + "Açıklamalar, eski uygulamaları kullanan kişiler tarafından görülemeyebilir." + "Medya yüklenemedi, lütfen tekrar deneyin." + "Medya yüklenemedi, lütfen tekrar deneyin." + "Bir mesaja basın ve buraya eklemek için “%1$s” yi seçin." + "Önemli mesajları kolayca keşfedilebilmeleri için sabitleyin" + + "%1$d Sabitlenmiş mesaj" + "%1$d Sabitlenmiş mesajlar" + + "Sabitlenmiş mesajlar" + "Kimliğinizi sıfırlamak için %1$s hesabınıza gitmek üzeresiniz. Daha sonra uygulamaya geri döneceksiniz." + "Onaylayamıyor musunuz? Kimliğinizi sıfırlamak için hesabınıza gidin." + "Doğrulamayı geri çek ve gönder" + "Doğrulamanızı geri çekebilir ve yine de bu mesajı gönderebilir veya şimdilik iptal edebilir ve %1$s yeniden doğruladıktan sonra daha sonra tekrar deneyebilirsiniz." + "%1$s kullanıcısının doğrulanmış kimliği değiştiği için mesajınız gönderilmedi" + "Yine de mesaj gönder" + "%1$s bir veya daha fazla doğrulanmamış cihaz kullanıyor. Mesajı yine de gönderebilir veya şimdilik iptal edebilir ve %2$s tüm cihazlarını doğruladıktan sonra tekrar deneyebilirsiniz." + "%1$s tüm cihazları doğrulamadığı için mesajınız gönderilmedi" + "Bir veya daha fazla cihazınız doğrulanmamış. Mesajı yine de gönderebilir veya şimdilik iptal edip tüm cihazlarınızı doğruladıktan sonra tekrar deneyebilirsiniz." + "Bir veya daha fazla cihazınızı doğrulamadığınız için mesajınız gönderilmedi" + "Medya yüklenemedi, lütfen tekrar deneyin." + "Kullanıcı ayrıntıları alınamadı" + "%1$s / %2$s" + "%1$s Sabitlenmiş mesajlar" + "Mesaj yükleniyor…" + "Tümünü görüntüle" + "Sohbet" + "Konum paylaş" + "Konumumu paylaş" + "Apple Maps\'de aç" + "Google Maps\'te aç" + "OpenStreetMap\'te aç" + "Bu konumu paylaş" + "%1$s kullanıcısının doğrulanmış kimliği değiştiği için ileti gönderilmedi." + "%1$s tüm cihazları doğrulamadığı için mesaj gönderilmedi." + "Bir veya daha fazla cihazınızı doğrulamadığınız için mesaj gönderilmedi." + "Konum" + "Sürüm: %1$s (%2$s)" + "tr" + "Geçmiş mesajlar bu cihazda kullanılamıyor" + "Geçmiş mesajlara erişim için bu cihazı doğrulamanız gerekir" + "Bu mesaja erişiminiz yok" + "Mesaj şifresi çözülemedi" + "Bu mesaj, cihazınızı doğrulamadığınız veya gönderenin kimliğinizi doğrulaması gerektiği için engellendi." + 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 12245254252..b57f560076f 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -1,5 +1,7 @@ + "Додати реакцію: %1$s" + "Аватар" "Видалити" "Введена %1$d цифра" @@ -7,11 +9,13 @@ "Введено %1$d цифр" "Cховати пароль" + "Приєднатися до виклику" "Перейти вниз" "Тільки згадки" "Звук вимкнено" "Сторінка %1$d" "Пауза" + "Голосове повідомлення, тривалість: %1$s, поточна позиція: %2$s" "Поле PIN-коду" "Відтворити" "Опитування" @@ -26,12 +30,14 @@ "Прочитано %1$s" "Натисніть, щоб показати все" - "Видалити реакцію з %1$s" + "Видалити реакцію з %1$s" + "Прибрати реакцію %1$s" "Надіслати файли" "Показати пароль" "Розпочати виклик" "Меню користувача" "Переглянути подробиці" + "Голосове повідомлення, тривалість: %1$s" "Записати голосове повідомлення." "Припинити запис" "Прийняти" @@ -58,9 +64,11 @@ "Деактивувати" "Деактивувати обліковий запис" "Відхилити" + "Відхилити та заблокувати" "Видалити опитування" "Вимкнути" "Відкинути" + "Відхилити" "Готово" "Редагувати" "Редагувати підпис" @@ -102,8 +110,11 @@ "Вилучити повідомлення" "Відповісти" "Відповісти в гілці" + "Поскаржитися" "Повідомити про помилку" "Повідомити про вміст" + "Поскаржитися на розмову" + "Поскаржитися на кімнату" "Скинути" "Скинути ідентичність" "Спробувати ще раз" @@ -131,10 +142,13 @@ "Переглянути джерело" "Так" "Так, повторити спробу" + "Ваш сервер тепер підтримує новий, швидший протокол. Вийдіть із системи та увійдіть знову, щоб оновити систему зараз. Якщо ви зробите це зараз, це допоможе вам уникнути примусового виходу з системи, коли старий протокол буде видалено пізніше." + "Доступне оновлення" "Відомості" "Політика прийнятного використання" "Додавання підпису" "Додаткові налаштування" + "зображення" "Аналітика" "Тема" "Аудіо" @@ -145,7 +159,9 @@ "Скопійовано до буферу обміну" "Авторське право" "Створення кімнати…" + "Запит скасовано" "Виходить з кімнати" + "Запрошення відхилено" "Темна" "Помилка розшифрування" "Налаштування розробника" @@ -158,6 +174,7 @@ "Редагування" "Редагування підпису" "* %1$s %2$s" + "Порожній файл" "Шифрування" "Шифрування ввімкнено" "Введіть свій PIN-код" @@ -182,8 +199,15 @@ "Цей Matrix-ID не знайдено, тому запрошення може не бути отримано." "Вихід з кімнати" "Світла" + "Рядок скопійовано до буфера обміну" "Посилання скопійовано в буфер обміну" "Завантаження" + "Завантаження наступних…" + + "%d інший" + "%d інші" + "%d інші" + "%1$d учасник" "%1$d учасники" @@ -195,8 +219,10 @@ "Повідомлення вилучено" "Модерн" "Вимкнути звук" + "%1$s (%2$s)" "Немає результатів" "Немає назви кімнати" + "Не зашифровано" "Не в мережі" "Ліцензії відкритого коду" "або" @@ -205,6 +231,7 @@ "Постійне посилання" "Дозвіл" "Закріплено" + "Перевірте з\'єднання з інтернетом" "Будь ласка, зачекайте…" "Ви впевнені, що хочете закінчити це опитування?" "Опитування: %1$s" @@ -218,8 +245,14 @@ "Політика конфіденційності" "Реакція" "Реакції" + "Причина" "Ключ відновлення" "Оновлення…" + + "%1$d відповідь" + "%1$d відповіді" + "%1$d відповідей" + "Відповідь %1$s" "Повідомити про ваду" "Повідомити про проблему" @@ -239,12 +272,14 @@ "Надсилання…" "Не вдалося надіслати" "Надіслано" + ". " "Сервер не підтримується" "URL-адреса сервера" "Налаштування" "Поширене розташування" "Вихід" "Щось пішло не так" + "Ми зіткнулися з проблемою. Будь ласка, повторіть спробу." "Початок бесіди…" "Наліпка" "Успіх" @@ -259,7 +294,7 @@ "Неможливо розшифрувати" "Надіслано з незахищеного пристрою" "Ви не маєте доступу до цього повідомлення" - "Підтверджена особа відправника змінилася" + "Ідентичність відправника скинуто" "Не вдалося надіслати запрошення одному чи кільком користувачам." "Не вдалося надіслати запрошення" "Розблокувати" @@ -273,16 +308,24 @@ "Перевірено" "Верифікувати пристрій" "Підтвердити особу" + "Верифікувати користувача" "Відео" "Голосове повідомлення" "Очікування…" "Чекаємо на це повідомлення" "Ви" - "Ідентичність %1$s, схоже, змінилася. %2$s" - "Ідентичність %1$s %2$s схоже, змінилася. %3$s" + "Ідентичність %1$s скинуто. %2$s" + "Ідентичність %1$s %2$s скинуто. %3$s" "(%1$s)" - "Верифіковано особистість %1$s %2$s змінилася. %3$s" + "Ідентичність %1$s скинуто." + "Ідентичність %1$s %2$s скинуто. %3$s" "Відкликати верифікацію" + "Посилання %1$s спрямовує вас на інший сайт %2$s + +Ви впевнені, що хочете продовжити?" + "Уважно перевірте це посилання" + "Скаргу на кімнату надіслано" + "Поскаржитися та вийти з кімнати" "Підтвердження" "Помилка" "Успіх" @@ -295,14 +338,17 @@ "Не вдалося завантажити повідомлення" "%1$s не вдалося отримати доступ до вашого розташування. Повторіть спробу пізніше." "Не вдалося завантажити голосове повідомлення." + "Кімната більше не існує або запрошення не чинне." "Повідомлення не знайдено" "%1$s не має дозволу на доступ до вашого розташування. Увімкнути доступ можна в Налаштуваннях." "%1$s не має дозволу на доступ до вашого розташування. Увімкніть доступ нижче." "%1$s не має доступу до вашого мікрофона. Надайте доступ, щоб записати голосове повідомлення." + "Це може бути пов\'язано з проблемами мережі або сервера." "Ця адреса кімнати вже існує, будь ласка, спробуйте відредагувати поле адреси кімнати або змінити назву кімнати" "Деякі символи не допускаються. Підтримуються тільки букви, цифри і наступні символи! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Деякі повідомлення не були надіслані" "Вибачте, сталася помилка" + "Відправник події не збігається з власником пристрою, який його надіслав." "Автентичність цього зашифрованого повідомлення не може бути гарантована на цьому пристрої." "Зашифровано попередньо перевіреним користувачем." "Не зашифровано." @@ -313,6 +359,10 @@ "Вітаю, поспілкуйтеся зі мною в %1$s: %2$s" "%1$s Android" "Повідомити про ваду за допомогою Rageshake" + "%1$s: %2$s" + "Варіанти" + "Вилучити %1$s" + "Налаштування" "Не вдалося вибрати медіафайл, спробуйте ще раз." "Користувачі старих застосунків можуть не бачити підписи." "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." @@ -329,7 +379,7 @@ "Не можете підтвердити? Перейдіть до свого облікового запису, щоб скинути облікові дані." "Відкликати верифікацію та відправити" "Ви все одно можете відкликати підтвердження та надіслати це повідомлення, або ви можете скасувати підписку на даний момент і спробувати пізніше після повторної перевірки %1$s." - "Ваше повідомлення не було надіслано, оскільки підтверджена особистість %1$s змінилася" + "Ваше повідомлення не надіслано, оскільки підтверджену особистість %1$s скинуто" "Надіслати повідомлення в будь-якому випадку" "%1$s використовує один або кілька неперевірених пристроїв. Ви можете відправити повідомлення в будь-якому випадку, або ж скасувати відправку і спробувати пізніше, коли %2$s перевірить всі пристрої." "Ваше повідомлення не було надіслано, тому що %1$s не перевірив усі пристрої" @@ -337,19 +387,22 @@ "Ваше повідомлення не було надіслано, оскільки ви не підтвердили один або декілька своїх пристроїв" "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." "Не вдалося отримати дані користувача" + "Повідомлення в %1$s" + "Розгорнути" + "Згорнути" + "Уже переглядаєте цю кімнату!" "%1$s із %2$s" "%1$s закріплених повідомлень" "Завантаження повідомлення…" "Переглянути всі" "Бесіда" - "Запит на приєднання надіслано" "Поділитися розташуванням" "Поділитися моїм розташуванням" "Відкрити в Apple Maps" "Відкрити в Картах Google" "Відкрити в OpenStreetMap" "Поділитися цим місцем перебування" - "Повідомлення не надіслано, оскільки підтверджена особистість %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 new file mode 100644 index 00000000000..aa813b829bb --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-ur/translations.xml @@ -0,0 +1,298 @@ + + + "حذف کریں" + + "%1$d ہندسہ درج" + "%1$d ہندسے درج" + + "لفظ عبور چھپائیں" + "نیچے چھلانگ لگائیں" + "صرف تذکرے" + "خاموش" + "صفحہ %1$d" + "متوقف" + "PIN میدان" + "چلائیں" + "رائے شماری" + "رائے شماری ختم کردی" + "%1$s کے ساتھ رد عمل کریں" + "دیگر رموز تعبیری سے رد عمل کریں" + "%1$s اور %2$s نے پڑھا" + + "%1$s اور %2$d دیگر نے پڑھا" + "%1$s اور %2$d دیگر نے لڑھا" + + "%1$s نے پڑھا" + "سب دکھانے کے لیے تھپتھپائیں" + "%1$s کے ساتھ رد عمل ہٹائیں" + "مسلیں بھیجیں" + "لفظ عبور دکھائیں" + "مکالمہ شروع کریں" + "لائحۂ صارف" + "صوتی پیغام ثبت کریں۔" + "ثبت کاری بند کریں" + "قبول کریں" + "جدول زمانی میں شامل کریں" + "واپس" + "مکالمہ کریں" + "منسوخ کریں" + "تصویر چنیں" + "صاف کریں" + "بند کریں" + "توثیق مکمل کریں" + "تصدیق کریں" + "جاری رکھیں" + "نقل" + "ربط نقل کریں" + "پیغام کا ربط نقل کریں" + "تخلیق کریں" + "ایک کمرہ بنائیں" + "مسترد کریں" + "رائے شماری حذف کریں" + "غیر فعال کریں" + "رد کریں" + "ہوگیا" + "تدوین کریں" + "رائے شماری میں ترمیم کریں" + "فعال کریں" + "رائے شماری ختم کریں" + "‏PIN درج کریں" + "لفظِ عبور بھول گئے؟" + "آگے کریں" + "واپس جائیں" + "مدعو کریں" + "لوگوں کو مدعو کریں" + "لوگوں کو %1$s پر مدعو کریں" + "لوگوں کو %1$s پر مدعو کریں" + "دعوت نامے" + "شامل ہوں" + "مزید جانئے" + "چھوڑ دیں" + "گفتگو چھوڑیں" + "کمرہ چھوڑ دیں" + "مزید لادیں" + "کھاتہ کا انتظام کریں" + "آلات کا نظم کریں" + "ارسال" + "اگلا" + "نہیں" + "ابھی نہیں" + "ٹھیک" + "ترتیبات" + "کھولیں مع" + "تثبیت کریں" + "فوری جواب" + "حوالہ دیں" + "رد عمل" + "مسترد کریں" + "ہٹائیں" + "جواب دیں" + "دھاگے میں جواب دیں" + "خطاء کی اطلاع دیں" + "مواد کی اطلاع دیں" + "بحال کریں" + "شناخت بحال کر دیں" + "پھر کوشش کریں" + "رمزکشائی کی پھر کوشش کریں" + "محفوظ کریں" + "تلاش کریں" + "بھیجیں" + "پیغام بھیجیں" + "اشتراک کریں" + "ربط کا اشتراک کریں" + "دوبارہ داخل ہوں" + "خارج ہوں" + "بہرحال خارج ہوں" + "چھوڑیں" + "شروع کریں" + "گفتگو شروع کریں" + "توثیق شروع کریں" + "نقشہ لادنے کیلئے تھپتھپائیں" + "تصویر لیں" + "اختیارات کے لیے تھپتھپائیں" + "دوبارہ کوشش کریں" + "غیر تثبیت کریں" + "ماخذ دیکھیں" + "ہاں" + "بمتعلق" + "قابل قبول استعمال کی سیاست" + "اعلیٰ ترتیبات" + "تجزیات" + "ظہور" + "صوت" + "مسدود صارفین" + "بلبلے" + "مکالمہ شروع" + "گفتگو کا پشتارہ" + "حقوقِ طبع و نشر" + "کمرہ تخلیق کررہاہے…" + "کمرہ چھوڑ لیا" + "اندھیرا" + "رمزکشائی کی خرابی" + "مطور اختیارات" + "براہ راست گفتگو" + "اسے دوبارہ نہ دکھائیں" + "(تدوین شدہ)" + "تدوین" + "* %1$s %2$s" + "مرموزکاری فعال ہے" + "اپنا PIN درج کریں" + "خرابی" + "کوئی نقص واقع ہوا ہے، ہوسکتا ہے کہ آپ کو نئے پیغامات کی اطلاعات موصول نہ ہوں۔ براہ کرم ترتیبات سے اطلاعات کا ازالہ کریں۔ + +وجہ: %1$s۔" + "ہر کوئی" + "ناکام" + "پسندیدہ" + "پسندیدہ" + "مسل" + "مسل تنزیلات میں محفوظ کر دی" + "پیغام آگے بھیجیں" + "GIF" + "تصویر" + "%1$s کے جواب میں" + "APK تنصیب کریں" + "یہ میٹرکس شناخت نہیں مل سکتی، تو ہو سکتا ہے کہ دعوت نامہ موصول نہ ہو۔" + "کمرہ چھوڑنا" + "روشنی" + "ربط تختہ تراشہ پر نقل کردا گیا" + "لاد رہا ہے…" + + "%d دیگر" + "%d دیگر" + + + "%1$d رکن" + "%1$d اراکین" + + "پیغام" + "پیغام کے اعمال" + "ترتیب پیغام" + "پیغام ہٹا دیا گیا" + "جدید" + "خاموش کریں" + "کوئی نتائج نہیں" + "کمرے کا کوئی نام نہیں" + "پرے خط" + "کھلا ماخذ رخصات" + "یا" + "لفظ عبور" + "لوگ" + "مستقل ربط" + "اجازت" + "براہ کرم انتظار کریں…" + "کیا آپ واقعی اس رائے شماری کو ختم کرنا چاہتے ہیں؟" + "رائے شماری: %1$s" + "ک آراء: %1$s" + "رائے شماری ختم ہونے کے بعد نتائج ظاہر ہوں گے" + + "%d رائے" + "%d آراء" + + "سیاستِ نجیت" + "ردعمل" + "ردود عمل" + "بازیابی کی کلید" + "تاکہ کر رہا ہے…" + "%1$s کا جواب دے رہے ہیں" + "ایک خطاء کی اطلاع دیں" + "کسی مسئلے کی اطلاع دیں" + "گزارش جمع ہوگئی" + "امیر مدیرِ متن" + "کمرہ" + "کمرے کا نام" + "e.g. your project name" + "محفوظ شدہ تبدیلیاں" + "محفوظ کر رہا ہے" + "صفحۂ نمائش قفل" + "کسی کیلئے تلاش کریں" + "تلاش کے نتائج" + "حفاظت" + "دیکھا گیا بہ" + "بھیجیں تا" + "بھیج رہا ہے…" + "بھیجنا ناکام" + "ارسال کردہ" + "خادم تعاون یافتہ نہیں" + "خادم عنوان" + "ترتیبات" + "مشترکہ مقام" + "خارج ہو رہا ہے" + "کچھ غلط ہو گیا" + "گفتگو شروع کر رہا ہے۔۔۔" + "برچسب" + "کامیابی" + "تجاویز" + "ہمسات سازی" + "نظام" + "متن" + "فریق ثالث کے اشعارات" + "دھاگہ" + "موضوع" + "یہ کمرہ کس کے بارے میں ہے؟" + "رمزکشائی کرنے سے قاصر" + "آپ کو اس پیغام تک رسائی حاصل نہیں" + "دعوت نامے ایک یا زیادہ صارفین کو نہیں بھیجے جا سکے۔" + "دعوت نامہ بھیجنے سے قاصر" + "غیر مقفل کریں" + "غیر خاموش کریں" + "غیر تعاون یافتہ واقعہ" + "صارف نام" + "توثیق منسوخ کردہ" + "توثیق مکمل" + "آلہ کی توثیق کریں" + "ویڈیو" + "صوتی پیغام" + "منتظر…" + "اس پیغام کا منتظر" + "تصدیق" + "خرابی" + "کامیابی" + "انتباہ" + "آپ کی تبدیلیاں محفوظ نہیں کی گئیں۔ کیا آپ کو یقین ہے کہ آپ واپس جانا چاہتے ہیں؟" + "تبدیلیاں محفوظ کریں؟" + "مستقل ربط تخلیق کرنا ناکام" + "%1$s نقشہ لاد نہیں سکا۔ برائے مہربانی بعد میں دوبارہ کوشش کریں۔" + "پیغامات لادنا ناکام" + "%1$s آپ کے مقام تک رسائی حاصل نہیں کر سکا۔ برائے مہربانی بعد میں دوبارہ کوشش کریں۔" + "آپکا صوتی پیغام رفع کرنے میں ناکام۔" + "پیغام نہیں ملا" + "%1$sآپ کے مقام تک رسائی کی اجازت نہیں ہے۔ آپ ترتیبات میں رسائی کو فعال کر سکتے ہیں۔" + "%1$s کو آپ کے مقام تک رسائی حاصل کرنے کی اجازت نہیں ہے۔ نیچے رسائی کو فعال کریں۔" + "%1$s کو آپ کے صوتگر تک رسائی حاصل کرنے کی اجازت نہیں ہے۔ صوتی پیغام ثبت کرنے کیلئے رسائی فعال کریں۔" + "کچھ پیغامات ارسال نہیں ہوئے" + "معذرت، ایک خرابی واقع ہوگئی" + "اس آلہ پر اس مرموز کردہ پیغام کی صداقت کی ضمانت نہیں دی جا سکتی۔" + "مرموز کردہ نہیں۔" + "کسی نامعلوم یا حذف شدہ آلے کے ذریعے مرموز کردہ۔" + "کسی ایسے آلے کے ذریعے مرموز کردہ جس کی توثیق اسکے مالک سے نہیں ہوئی۔" + "ایک غیر توثیق شدہ صارف کے ذریعے مرموز کردہ۔" + "🔐️ %1$s پر میرے ساتھ شامل ہوں" + "ارے، مجھ سے %1$s پر بات کریں: %2$s" + "%1$s Android" + "خطاء کی اطلاع دینے کیلئے غصے سے ہلائیں" + "وسائط منتخب کرنا ناکام، برائے مہربانی دوبارہ کوشش کریں۔" + "وسائط کا معالجہ برائے ترفیع ناکام، برائے مہربانی دوبارہ کوشش کریں۔" + "وسائط رفع کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" + + "%1$d مثبوتہ پیغام" + "%1$d مثبوتہ پیغامات" + + "مثبوتہ پیغامات" + "وسائط کا معالجہ برائے ترفیع ناکام، برائے مہربانی دوبارہ کوشش کریں۔" + "صارف کی تفصیلات بازیافت نہیں ہوسکیں" + "%1$s از %2$s" + "%1$s مثبتہ پیغامات" + "پیغام لاد رہا ہے…" + "تمام ملاحظہ کریں" + "گفتگو" + "مقام کا اشتراک کریں" + "میرے مقام کا اشتراک کریں" + "ایپل میپس میں کھولیں" + "گوگل میپس میں کھولیں۔ل" + "اوپن اسٹریٹ میپ میں کھولیں" + "اس مقام کا اشتراک کریں" + "مقام" + "نسخہ %1$s (%2$s)" + "ur" + 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 2f15a5ccd8e..f2498383d74 100644 --- a/libraries/ui-strings/src/main/res/values-uz/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uz/translations.xml @@ -1,5 +1,6 @@ + "Avatar" "Oʻchirish" "Parolni yashirish" "Faqat eslatmalar" 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 b35980f4794..86b6dfdaaf0 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,15 +1,18 @@ + "大頭貼" "刪除" "已輸入 %1$d 個位數" "隱藏密碼" + "加入通話" "跳至底部" "僅限提及" "已關閉通知" "第 %1$d 頁" "暫停" + "語音訊息,時長:%1$s,目前位置:%2$s" "PIN 碼欄位" "播放" "投票" @@ -22,11 +25,13 @@ "%1$s 已讀" "點擊以顯示全部" - "移除 %1$s 的反應" + "移除 %1$s 的反應" "傳送檔案" "顯示密碼" "開始通話" "使用者選單" + "檢視詳細資訊" + "語音訊息,時長:%1$s" "錄製語音訊息。" "停止錄音" "接受" @@ -53,9 +58,11 @@ "停用" "停用帳號" "拒絕" + "拒絕並封鎖" "刪除投票" "停用" "捨棄" + "關閉" "完成" "編輯" "編輯標題" @@ -97,8 +104,11 @@ "移除訊息" "回覆" "在討論串中回覆" + "回報" "回報程式錯誤" "檢舉內容" + "回報對話" + "回報聊天室" "重設" "重設身份" "再試一次" @@ -123,9 +133,11 @@ "再試一次" "取消釘選" "在時間軸中檢視" - "檢視原始碼" + "檢視來源" "是" "是的,再試一次" + "您的伺服器現在支援新的、更快的協定。立即登出並重新登入以進行升級。現在這樣做將協助您避免在稍後移除舊協定時被強制登出。" + "可升級" "關於" "可接受使用政策" "新增標題" @@ -140,7 +152,9 @@ "已複製到剪貼簿" "著作權" "正在建立聊天室…" + "請求已取消" "已離開聊天室" + "邀請被拒絕" "深色" "解密錯誤" "開發者選項" @@ -153,6 +167,7 @@ "編輯中" "編輯標題" "* %1$s %2$s" + "空檔案" "加密" "已啟用加密" "輸入您的 PIN 碼" @@ -177,8 +192,13 @@ "找不到此 Matrix ID,因此可能沒有人會收到邀請。" "正在離開聊天室" "淺色" + "行已複製到剪貼簿" "連結已複製到剪貼簿" "載入中…" + "載入更多……" + + "其他 %d 個人" + "%1$d 位成員" @@ -188,8 +208,10 @@ "訊息已移除" "現代" "關閉通知" + "%1$s (%2$s)" "查無結果" "無聊天室名稱" + "未加密" "離線" "開放原始碼授權條款" "或" @@ -198,6 +220,7 @@ "永久連結" "權限" "已釘選" + "請檢查您的網際網路連線" "請稍等…" "您確定要結束這項投票嗎?" "投票:%1$s" @@ -209,6 +232,7 @@ "隱私權政策" "回應" "回應" + "理由" "復原金鑰" "重新整理中…" "正在回覆%1$s" @@ -236,6 +260,7 @@ "位置分享" "正在登出" "有錯誤發生" + "我們了遇到了問題。請再試一次。" "開始聊天…" "貼圖" "成功" @@ -250,7 +275,7 @@ "無法解密" "從不安全的裝置傳送" "您無法存取此則訊息" - "傳送者的驗證身份已變更" + "傳送者的驗證身份已重設" "無法發送邀請給一或多個使用者。" "無法發送邀請" "解鎖" @@ -264,16 +289,24 @@ "已驗證" "驗證裝置" "驗證身份" + "驗證使用者" "影片" "語音訊息" "等待中…" "等待此則訊息" "您" - "%1$s 的身份似乎已變更。%2$s" - "%1$s 的 %2$s 身份似乎已變更。%3$s" + "%1$s 的身份似乎已重設。%2$s" + "%1$s 的 %2$s 身份似乎已重設。%3$s" "(%1$s)" - "%1$s 的 %2$s 驗證身份已變更。%3$s" + "%1$s 的已驗證身份被重設。" + "%1$s 的 %2$s 驗證身份已重設。 %3$s" "撤回驗證" + "連結 %1$s 會將您帶往其他網站 %2$s + +您確定您想要繼續嗎?" + "仔細檢查此連結" + "聊天室已回報" + "回報並離開聊天室" "確認" "錯誤" "成功" @@ -290,6 +323,7 @@ "%1$s 沒有權限存取您的位置。您可以到設定中開啟權限。" "%1$s 沒有權限存取您的位置。請在下方開啟權限。" "%1$s 沒有權限存取您的麥克風。您需要開啟權限才能錄製語音訊息。" + "這可能是因為網路或伺服器的問題所致。" "此聊天室地址已存在。請嘗試編輯聊天室地址欄位或變更聊天室名稱" "不允許使用部份字元。僅支援字母、數字與以下符號 ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "有些訊息尚未傳送" @@ -318,7 +352,7 @@ "無法確認?前往您的帳號以重設您的身份。" "撤回驗證並傳送" "您可以撤回您的驗證並仍傳送此訊息,或者您也可以立刻取消並在重新驗證 %1$s 後再試一次。" - "因為 %1$s 的驗證身份已變更,因此未傳送您的訊息。" + "因為 %1$s 的驗證身份已重設,因此未傳送您的訊息。" "仍要傳送訊息" "%1$s 正在使用一個或多個未經驗證的裝置。您仍然可以傳送訊息,也可以立刻取消並在 %2$s 驗證其所有裝置後再試一次。" "未傳送您的訊息,因為 %1$s 尚未驗證所有裝置。" @@ -326,19 +360,19 @@ "因為您尚未驗證一個或多個裝置,因為未傳送您的訊息" "無法處理要上傳的媒體,請再試一次。" "無法擷取使用者詳細資訊" + "%1$s 中的訊息" "第 %1$s 個,共 %2$s 個" "%1$s 個釘選訊息" "正在載入訊息……" "檢視全部" "聊天" - "已傳送加入請求" "分享位置" "分享我的位置" "在 Apple Maps 中開啟" "在 Google Maps 中開啟" "在開放街圖(OpenStreetMap) 中開啟" "分享這個位置" - "因為 %1$s 的驗證身份已變更,因此未傳送訊息。" + "因為 %1$s 的驗證身份已重設,因此未傳送訊息。" "訊息未傳送,因為 %1$s 尚未驗證所有裝置。" "因為您尚未驗證一個或多個裝置,因此未傳送訊息" "位置" 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 76ba789f3ca..b9dc5d882e4 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -1,15 +1,18 @@ + "头像" "删除" "已输入 %1$d 个数字" "隐藏密码" + "加入通话" "跳转到底部" "仅提及" "通知已关闭" "第 %1$d 页" "暂停" + "语音消息,时长:%1$s,当前位置:%2$s" "PIN 字段" "播放" "投票" @@ -22,13 +25,14 @@ "%1$s 已读" "点击以显示全部" - "撤回反应 %1$s" + "撤回反应 %1$s" "发送文件" "显示密码" "开始通话" "用户菜单" "查看详情" - "录制语音消息。" + "语音消息,时长:%1$s" + "录制语音消息" "停止录制" "接受" "添加标题" @@ -54,9 +58,11 @@ "停用" "停用账户" "拒绝" + "拒绝并屏蔽" "删除投票" "禁用" "丢弃" + "关闭" "完成" "编辑" "编辑标题" @@ -70,8 +76,8 @@ "忽略" "邀请" "邀请朋友" - "邀请朋友加入 %1$s" - "邀请人们加入 %1$s" + "邀请别人加入 %1$s" + "邀请别人加入 %1$s" "邀请" "加入" "了解更多" @@ -98,8 +104,11 @@ "删除消息" "回复" "在消息列中回复" + "举报" "报告错误" "举报内容" + "举报对话" + "举报房间" "重置" "重置身份" "重试" @@ -127,6 +136,8 @@ "查看源码" "是" "是的,再试一次" + "您的服务器现在支持更快的新协议。现在登出并重新登录以进行升级。现在这样做可以帮助您避免在以后删除旧协议时被强制登出。" + "有可用升级" "关于" "可接受的使用政策" "添加标题" @@ -141,7 +152,9 @@ "已复制到剪贴板" "版权" "正在创建聊天室…" + "请求已取消" "离开聊天室" + "邀请已拒绝" "深色" "解密错误" "开发者选项" @@ -154,6 +167,7 @@ "编辑中" "编辑标题" "* %1$s %2$s" + "空文件" "加密" "已启用加密" "输入 PIN 码" @@ -178,9 +192,13 @@ "找不到此 Matrix ID,因此可能无法收到邀请。" "正在离开聊天室" "浅色" + "链接已复制到剪贴板" "链接已复制到剪贴板" "正在加载…" "正在加载更多……" + + "其他 %d 人" + "%1$d个成员" @@ -190,8 +208,10 @@ "消息已移除" "现代" "静音" + "%1$s (%2$s)" "没有结果" "无聊天室名" + "未加密" "离线" "开源许可证" "或" @@ -200,6 +220,7 @@ "固定链接" "权限" "已置顶" + "请检查 Internet 连接" "请稍候……" "确定要结束这个投票吗?" "投票:%1$s" @@ -211,8 +232,12 @@ "隐私政策" "回应" "回应" + "理由" "恢复密钥" "正在刷新…" + + "%1$d 个回复" + "正在回复 %1$s" "报告错误" "报告问题" @@ -220,7 +245,7 @@ "富文本编辑器" "聊天室" "聊天室名称" - "例如:项目名称" + "例如:您的项目名称" "保存的更改" "正在保存" "屏幕锁定" @@ -238,6 +263,7 @@ "共享位置" "正在登出" "发生了一些错误" + "我们遇到了一个问题。请重试。" "开始聊天…" "贴纸" "成功" @@ -266,6 +292,7 @@ "已验证" "验证设备" "验证身份" + "验证用户" "视频" "语音消息" "等待…" @@ -274,8 +301,15 @@ "%1$s 的身份似乎已经改变。%2$s" "%1$s 的 %2$s 身份似乎已经改变。%3$s" "(%1$s)" + "%1$s 的身份已重置。" "%1$s 的 %2$s 已验证身份已发生改变。%3$s" "撤回验证" + "链接 %1$s 将跳转至外部网站 %2$s + +确定要继续吗?" + "请再次确认链接" + "已举报房间" + "举报并离开房间" "确认" "错误" "成功" @@ -292,6 +326,7 @@ "%1$s 没有权限访问您的位置。您可以在设置中启用位置权限。" "%1$s 没有权限访问您的位置。在下方启用位置权限。" "%1$s 没有权限访问您的麦克风。启用录制语音消息的权限。" + "这可能是由于网络或服务器问题导致" "此房间地址已存在。请尝试编辑房间地址字段或更改房间名称" "不允许使用某些字符。仅支持字母、数字和以下符号 $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "某些信息尚未发送" @@ -333,7 +368,6 @@ "正在加载消息…" "查看全部" "聊天" - "加入请求已发送" "分享位置" "分享我的位置" "在 Apple Maps 中打开" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 2d6c7eea53c..270e8623007 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -1,16 +1,21 @@ + "Add reaction: %1$s" + "Avatar" "Delete" "%1$d digit entered" "%1$d digits entered" "Hide password" + "Join call" "Jump to bottom" "Mentions only" "Muted" + "Other user\'s avatar" "Page %1$d" "Pause" + "Voice message, duration: %1$s, current position: %2$s" "PIN field" "Play" "Poll" @@ -24,14 +29,20 @@ "Read by %1$s" "Tap to show all" + "Remove reaction: %1$s" "Remove reaction with %1$s" + "Room avatar" "Send files" "Show password" "Start a call" + "User avatar" "User menu" + "View avatar" "View details" + "Voice message, duration: %1$s" "Record voice message." "Stop recording" + "Your avatar" "Accept" "Add caption" "Add to timeline" @@ -56,9 +67,11 @@ "Deactivate" "Deactivate account" "Decline" + "Decline and block" "Delete Poll" "Disable" "Discard" + "Dismiss" "Done" "Edit" "Edit caption" @@ -88,6 +101,7 @@ "No" "Not now" "OK" + "Open context menu" "Settings" "Open with" "Pin" @@ -100,8 +114,11 @@ "Remove message" "Reply" "Reply in thread" + "Report" "Report bug" "Report content" + "Report conversation" + "Report room" "Reset" "Reset identity" "Retry" @@ -109,7 +126,9 @@ "Save" "Search" "Send" + "Send edited message" "Send message" + "Send voice message" "Share" "Share link" "Show" @@ -125,14 +144,18 @@ "Tap for options" "Try again" "Unpin" + "View" "View in timeline" "View source" "Yes" "Yes, try again" + "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later." + "Upgrade available" "About" "Acceptable use policy" "Adding caption" "Advanced settings" + "an image" "Analytics" "Appearance" "Audio" @@ -143,7 +166,9 @@ "Copied to clipboard" "Copyright" "Creating room…" + "Request canceled" "Left room" + "Invite declined" "Dark" "Decryption error" "Developer options" @@ -156,6 +181,7 @@ "Editing" "Editing caption" "* %1$s %2$s" + "Empty file" "Encryption" "Encryption enabled" "Enter your PIN" @@ -180,9 +206,14 @@ Reason: %1$s." "This Matrix ID can\'t be found, so the invite might not be received." "Leaving room" "Light" + "Line copied to clipboard" "Link copied to clipboard" "Loading…" "Loading more…" + + "%d other" + "%d others" + "%1$d member" "%1$d members" @@ -193,8 +224,10 @@ Reason: %1$s." "Message removed" "Modern" "Mute" + "%1$s (%2$s)" "No results" "No room name" + "Not encrypted" "Offline" "Open source licenses" "or" @@ -203,6 +236,7 @@ Reason: %1$s." "Permalink" "Permission" "Pinned" + "Please check your internet connection" "Please wait…" "Are you sure you want to end this poll?" "Poll: %1$s" @@ -215,8 +249,13 @@ Reason: %1$s." "Privacy policy" "Reaction" "Reactions" + "Reason" "Recovery key" "Refreshing…" + + "%1$d reply" + "%1$d replies" + "Replying to %1$s" "Report a bug" "Report a problem" @@ -236,12 +275,14 @@ Reason: %1$s." "Sending…" "Sending failed" "Sent" + ". " "Server not supported" "Server URL" "Settings" "Shared location" "Signing out" "Something went wrong" + "We encountered an issue. Please try again." "Starting chat…" "Sticker" "Success" @@ -256,7 +297,7 @@ Reason: %1$s." "Unable to decrypt" "Sent from an insecure device" "You don\'t have access to this message" - "Sender\'s verified identity has changed" + "Sender\'s verified identity was reset" "Invites couldn\'t be sent to one or more users." "Unable to send invite(s)" "Unlock" @@ -270,16 +311,24 @@ Reason: %1$s." "Verified" "Verify device" "Verify identity" + "Verify user" "Video" "Voice message" "Waiting…" "Waiting for this message" "You" - "%1$s\'s identity appears to have changed. %2$s" - "%1$s’s %2$s identity appears to have changed. %3$s" + "%1$s\'s identity was reset. %2$s" + "%1$s’s %2$s identity was reset. %3$s" "(%1$s)" - "%1$s’s %2$s verified identity has changed. %3$s" + "%1$s’s identity was reset." + "%1$s’s %2$s identity was reset. %3$s" "Withdraw verification" + "The link %1$s is taking you to another site %2$s + +Are you sure you want to continue?" + "Double-check this link" + "Room reported" + "Reported and left room" "Confirmation" "Error" "Success" @@ -292,14 +341,17 @@ Reason: %1$s." "Failed loading messages" "%1$s could not access your location. Please try again later." "Failed to upload your voice message." + "The room no longer exists or the invite is no longer valid." "Message not found" "%1$s does not have permission to access your location. You can enable access in Settings." "%1$s does not have permission to access your location. Enable access below." "%1$s does not have permission to access your microphone. Enable access to record a voice message." + "This may be due to network or server issues." "This room address already exists. Please try editing the room address field or change the room name" "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" "Some messages have not been sent" "Sorry, an error occurred" + "The sender of the event does not match the owner of the device that sent it." "The authenticity of this encrypted message can\'t be guaranteed on this device." "Encrypted by a previously-verified user." "Not encrypted." @@ -310,6 +362,10 @@ Reason: %1$s." "Hey, talk to me on %1$s: %2$s" "%1$s Android" "Rageshake to report bug" + "%1$s: %2$s" + "Options" + "Remove %1$s" + "Settings" "Failed selecting media, please try again." "Captions might not be visible to people using older apps." "Failed processing media to upload, please try again." @@ -325,7 +381,7 @@ Reason: %1$s." "Can\'t confirm? Go to your account to reset your identity." "Withdraw verification and send" "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$s." - "Your message was not sent because %1$s’s verified identity has changed" + "Your message was not sent because %1$s’s verified identity was reset" "Send message anyway" "%1$s is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$s has verified all their devices." "Your message was not sent because %1$s has not verified all devices" @@ -333,19 +389,22 @@ Reason: %1$s." "Your message was not sent because you have not verified one or more of your devices" "Failed processing media to upload, please try again." "Could not retrieve user details" + "Message in %1$s" + "Expand" + "Reduce" + "Already viewing this room!" "%1$s of %2$s" "%1$s Pinned messages" "Loading message…" "View All" "Chat" - "Request to join sent" "Share location" "Share my location" "Open in Apple Maps" "Open in Google Maps" "Open in OpenStreetMap" "Share this location" - "Message not sent because %1$s’s verified identity has changed." + "Message not sent because %1$s’s verified identity was reset." "Message not sent because %1$s has not verified all devices." "Message not sent because you have not verified one or more of your devices." "Location" diff --git a/libraries/ui-utils/build.gradle.kts b/libraries/ui-utils/build.gradle.kts index ec75f6627bb..fc60dc277e8 100644 --- a/libraries/ui-utils/build.gradle.kts +++ b/libraries/ui-utils/build.gradle.kts @@ -6,7 +6,7 @@ */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { @@ -15,5 +15,7 @@ android { dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) + testImplementation(libs.coroutines.test) + testImplementation(libs.test.truth) } } diff --git a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/MultipleTapToUnlock.kt b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/MultipleTapToUnlock.kt new file mode 100644 index 00000000000..15a8b82eeee --- /dev/null +++ b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/MultipleTapToUnlock.kt @@ -0,0 +1,42 @@ +/* + * 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.ui.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +/** + * Returns true if the user has tapped [numberOfTapToUnlock] times in a short amount of time. + * The counter is reset after 2 seconds of inactivity. + * + * @param numberOfTapToUnlock The number of taps required to unlock. + */ +class MultipleTapToUnlock( + private val numberOfTapToUnlock: Int = 7, +) { + private var counter = numberOfTapToUnlock + private var currentJob: Job? = null + + fun unlock(scope: CoroutineScope): Boolean { + counter-- + currentJob?.cancel() + return if (counter > 0) { + currentJob = scope.launch { + delay(2.seconds) + // Reset counter if user is not fast enough + counter = numberOfTapToUnlock + } + false + } else { + true + } + } +} diff --git a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/IsTalkbackEnabled.kt b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/IsTalkbackEnabled.kt new file mode 100644 index 00000000000..40b0a3cc761 --- /dev/null +++ b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/IsTalkbackEnabled.kt @@ -0,0 +1,34 @@ +/* + * 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.ui.utils.time + +import android.view.accessibility.AccessibilityManager +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.platform.LocalContext + +@Composable +fun isTalkbackActive(): Boolean { + val context = LocalContext.current + val accessibilityManager = remember { context.getSystemService(AccessibilityManager::class.java) } + var isTouchExplorationEnabled by remember { mutableStateOf(accessibilityManager.isTouchExplorationEnabled) } + DisposableEffect(Unit) { + val listener = AccessibilityManager.TouchExplorationStateChangeListener { enabled -> + isTouchExplorationEnabled = enabled + } + accessibilityManager.addTouchExplorationStateChangeListener(listener) + onDispose { + accessibilityManager.removeTouchExplorationStateChangeListener(listener) + } + } + return isTouchExplorationEnabled +} diff --git a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/KeyEventExt.kt b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/KeyEventExt.kt new file mode 100644 index 00000000000..580807f987e --- /dev/null +++ b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/KeyEventExt.kt @@ -0,0 +1,34 @@ +/* + * 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.ui.utils.time + +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.key + +/** + * Extension property to get the digit character from a KeyEvent. + * This handles both regular digit keys and numpad keys. + */ +val KeyEvent.digit: Char? get() { + val char = nativeKeyEvent.unicodeChar.toChar() + return when { + Character.isDigit(char) -> char + key == Key.NumPad0 -> '0' + key == Key.NumPad1 -> '1' + key == Key.NumPad2 -> '2' + key == Key.NumPad3 -> '3' + key == Key.NumPad4 -> '4' + key == Key.NumPad5 -> '5' + key == Key.NumPad6 -> '6' + key == Key.NumPad7 -> '7' + key == Key.NumPad8 -> '8' + key == Key.NumPad9 -> '9' + else -> null + } +} diff --git a/libraries/ui-utils/src/test/kotlin/io/element/android/libraries/ui/utils/MultipleTapToUnlockTest.kt b/libraries/ui-utils/src/test/kotlin/io/element/android/libraries/ui/utils/MultipleTapToUnlockTest.kt new file mode 100644 index 00000000000..1745f617d84 --- /dev/null +++ b/libraries/ui-utils/src/test/kotlin/io/element/android/libraries/ui/utils/MultipleTapToUnlockTest.kt @@ -0,0 +1,41 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.ui.utils + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.time.Duration.Companion.seconds + +class MultipleTapToUnlockTest { + @Test + fun `test multiple tap should unlock`() = runTest { + val sut = MultipleTapToUnlock(3) + assertThat(sut.unlock(backgroundScope)).isFalse() + assertThat(sut.unlock(backgroundScope)).isFalse() + assertThat(sut.unlock(backgroundScope)).isTrue() + assertThat(sut.unlock(backgroundScope)).isTrue() + // All next call returns true + advanceTimeBy(3.seconds) + assertThat(sut.unlock(backgroundScope)).isTrue() + } + @Test + fun `test waiting should reset counter`() = runTest { + val sut = MultipleTapToUnlock(3) + assertThat(sut.unlock(backgroundScope)).isFalse() + assertThat(sut.unlock(backgroundScope)).isFalse() + advanceTimeBy(3.seconds) + assertThat(sut.unlock(backgroundScope)).isFalse() + assertThat(sut.unlock(backgroundScope)).isFalse() + assertThat(sut.unlock(backgroundScope)).isTrue() + } +} diff --git a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt index 515aab80722..43dcb308361 100644 --- a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt +++ b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt @@ -50,7 +50,7 @@ internal class MatrixUserListDataSourceTest { val matrixClient = FakeMatrixClient() matrixClient.givenSearchUsersResult( searchTerm = "test", - result = Result.failure(Throwable("Ruhroh")) + result = Result.failure(RuntimeException("Ruhroh")) ) val dataSource = MatrixUserListDataSource(matrixClient) @@ -76,7 +76,7 @@ internal class MatrixUserListDataSourceTest { val matrixClient = FakeMatrixClient() matrixClient.givenGetProfileResult( userId = A_USER_ID, - result = Result.failure(Throwable("Ruhroh")) + result = Result.failure(RuntimeException("Ruhroh")) ) val dataSource = MatrixUserListDataSource(matrixClient) diff --git a/libraries/voiceplayer/impl/build.gradle.kts b/libraries/voiceplayer/impl/build.gradle.kts index 6566f3557ef..f579586aa08 100644 --- a/libraries/voiceplayer/impl/build.gradle.kts +++ b/libraries/voiceplayer/impl/build.gradle.kts @@ -19,6 +19,7 @@ setupAnvil() dependencies { api(projects.libraries.voiceplayer.api) + implementation(projects.libraries.audio.api) implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt index 726fccdca0f..e6d596e90a1 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.voiceplayer.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.architecture.Presenter 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.media.MediaSource import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory @@ -22,7 +23,8 @@ import kotlin.time.Duration @ContributesBinding(RoomScope::class) class DefaultVoiceMessagePresenterFactory @Inject constructor( private val analyticsService: AnalyticsService, - private val scope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val voiceMessagePlayerFactory: VoiceMessagePlayer.Factory, ) : VoiceMessagePresenterFactory { override fun createVoiceMessagePresenter( @@ -41,7 +43,7 @@ class DefaultVoiceMessagePresenterFactory @Inject constructor( return VoiceMessagePresenter( analyticsService = analyticsService, - scope = scope, + sessionCoroutineScope = sessionCoroutineScope, player = player, eventId = eventId, duration = duration, diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt index 922751852fc..9f000b8c32a 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.media.MatrixMediaLoader @@ -80,7 +81,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor( source = mediaSource, mimeType = mimeType, filename = filename, - ).mapCatching { + ).mapCatchingExceptions { it.use { mediaFile -> val dest = cachedFile.apply { parentFile?.mkdirs() } if (mediaFile.persist(dest.path)) { diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt index 1e42f6d8dac..799a7ff8543 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.voiceplayer.impl import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId @@ -194,7 +195,7 @@ class DefaultVoiceMessagePlayer( }.distinctUntilChanged() override suspend fun prepare(): Result = if (eventId != null) { - repo.getMediaFile().mapCatching { mediaFile -> + repo.getMediaFile().mapCatchingExceptions { mediaFile -> val state = internalState.value mediaPlayer.setMedia( uri = mediaFile.path, diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt index 96b74087e61..ce865c61f8a 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.core.extensions.flatMap +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.utils.time.formatShort import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents @@ -30,7 +31,7 @@ import kotlin.time.Duration.Companion.milliseconds class VoiceMessagePresenter( private val analyticsService: AnalyticsService, - private val scope: CoroutineScope, + private val sessionCoroutineScope: CoroutineScope, private val player: VoiceMessagePlayer, private val eventId: EventId?, private val duration: Duration, @@ -97,7 +98,7 @@ class VoiceMessagePresenter( } else if (playerState.isReady) { player.play() } else { - scope.launch { + sessionCoroutineScope.launch { play.runUpdatingState( errorTransform = { analyticsService.trackError( @@ -107,7 +108,7 @@ class VoiceMessagePresenter( }, ) { player.prepare().flatMap { - runCatching { player.play() } + runCatchingExceptions { player.play() } } } } diff --git a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt index c10b263b759..9d16cd6bcc1 100644 --- a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt +++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt @@ -236,7 +236,7 @@ fun TestScope.createVoiceMessagePresenter( mediaSource: MediaSource = MediaSource(contentUri), ) = VoiceMessagePresenter( analyticsService = analyticsService, - scope = this, + sessionCoroutineScope = this, player = DefaultVoiceMessagePlayer( mediaPlayer = mediaPlayer, voiceMessageMediaRepoFactory = { _, _, _ -> voiceMessageMediaRepo }, diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorder.kt index 64f57b05740..5e690ab1f7b 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorder.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorder.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import io.element.android.libraries.voicerecorder.impl.audio.Audio @@ -51,10 +52,11 @@ class DefaultVoiceRecorder @Inject constructor( private val config: AudioConfig, private val fileConfig: VoiceFileConfig, private val audioLevelCalculator: AudioLevelCalculator, - appCoroutineScope: CoroutineScope, + @SessionCoroutineScope + sessionCoroutineScope: CoroutineScope, ) : VoiceRecorder { private val voiceCoroutineScope by lazy { - appCoroutineScope.childScope(dispatchers.io, "VoiceRecorder-${UUID.randomUUID()}") + sessionCoroutineScope.childScope(dispatchers.io, "VoiceRecorder-${UUID.randomUUID()}") } private var outputFile: File? = null 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 7e7fc51d71a..a64d2733791 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 @@ -48,7 +48,6 @@ class DefaultEncoder @Inject constructor( override fun release() { encoder?.release() - ?: Timber.w("Can't release encoder that is not initialized") encoder = null } } diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt index 12ad5f65b29..8dc6bf2dbea 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt @@ -12,7 +12,7 @@ import io.element.android.libraries.core.hash.md5 import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.BaseRoom import java.io.File import java.util.UUID import javax.inject.Inject @@ -21,7 +21,7 @@ import javax.inject.Inject class DefaultVoiceFileManager @Inject constructor( @CacheDirectory private val cacheDir: File, private val config: VoiceFileConfig, - room: MatrixRoom, + room: BaseRoom, ) : VoiceFileManager { private val roomId: RoomId = room.roomId diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt index 639686fbf8a..6ed540a2f40 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt @@ -141,7 +141,7 @@ class DefaultVoiceRecorderTest { fileConfig = fileConfig, fileManager = FakeVoiceFileManager(fakeFileSystem, fileConfig, FILE_ID), audioLevelCalculator = FakeAudioLevelCalculator(), - appCoroutineScope = backgroundScope, + sessionCoroutineScope = backgroundScope, ) } diff --git a/plugins/src/main/kotlin/ModulesConfig.kt b/plugins/src/main/kotlin/ModulesConfig.kt index 323fbcf9ab5..57379a7fd64 100644 --- a/plugins/src/main/kotlin/ModulesConfig.kt +++ b/plugins/src/main/kotlin/ModulesConfig.kt @@ -6,16 +6,36 @@ */ import config.AnalyticsConfig +import config.BuildTimeConfig import config.PushProvidersConfig object ModulesConfig { val pushProvidersConfig = PushProvidersConfig( - includeFirebase = true, - includeUnifiedPush = true, + includeFirebase = BuildTimeConfig.PUSH_CONFIG_INCLUDE_FIREBASE, + includeUnifiedPush = BuildTimeConfig.PUSH_CONFIG_INCLUDE_UNIFIED_PUSH, ) - val analyticsConfig: AnalyticsConfig = AnalyticsConfig.Enabled( - withPosthog = true, - withSentry = true, - ) + val analyticsConfig: AnalyticsConfig = if (isEnterpriseBuild) { + // Is Posthog configuration available? + val withPosthog = BuildTimeConfig.SERVICES_POSTHOG_APIKEY.isNullOrEmpty().not() && + BuildTimeConfig.SERVICES_POSTHOG_HOST.isNullOrEmpty().not() + // Is Sentry configuration available? + val withSentry = BuildTimeConfig.SERVICES_SENTRY_DSN.isNullOrEmpty().not() + if (withPosthog || withSentry) { + println("Analytics enabled with Posthog: $withPosthog, Sentry: $withSentry") + AnalyticsConfig.Enabled( + withPosthog = withPosthog, + withSentry = withSentry, + ) + } else { + println("Analytics disabled") + AnalyticsConfig.Disabled + } + } else { + println("Analytics enabled with Posthog and Sentry") + AnalyticsConfig.Enabled( + withPosthog = true, + withSentry = true, + ) + } } diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 2c4526339e5..d759c34a84e 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -29,7 +29,7 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion */ private const val versionYear = 25 -private const val versionMonth = 2 +private const val versionMonth = 7 // Note: must be in [0,99] private const val versionReleaseNumber = 2 @@ -42,7 +42,9 @@ object Versions { const val TARGET_SDK = 35 // When updating the `minSdk`, make sure to update the value of `minSdkVersion` in the file `tools/release/release.sh` - val minSdk = if (isEnterpriseBuild) 26 else 24 + private const val MIN_SDK_FOSS = 24 + private const val MIN_SDK_ENTERPRISE = 33 + val minSdk = if (isEnterpriseBuild) MIN_SDK_ENTERPRISE else MIN_SDK_FOSS private const val JAVA_VERSION = 21 val javaVersion: JavaVersion = JavaVersion.toVersion(JAVA_VERSION) diff --git a/plugins/src/main/kotlin/config/BuildTimeConfig.kt b/plugins/src/main/kotlin/config/BuildTimeConfig.kt new file mode 100644 index 00000000000..fb489610e86 --- /dev/null +++ b/plugins/src/main/kotlin/config/BuildTimeConfig.kt @@ -0,0 +1,36 @@ +/* + * 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 config + +object BuildTimeConfig { + const val APPLICATION_ID = "org.therevivalists.app" + const val APPLICATION_NAME = "The Revivalists" + const val GOOGLE_APP_ID_RELEASE = "1:912726360885:android:d097de99a4c23d2700427c" + const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c" + const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c" + + val METADATA_HOST_REVERSED: String? = null + val URL_WEBSITE: String? = null + val URL_LOGO: String? = null + val URL_COPYRIGHT: String? = null + val URL_ACCEPTABLE_USE: String? = null + val URL_PRIVACY: String? = null + val URL_POLICY: String? = null + val SERVICES_MAPTILER_BASE_URL: String? = null + val SERVICES_MAPTILER_APIKEY: String? = null + val SERVICES_MAPTILER_LIGHT_MAPID: String? = null + val SERVICES_MAPTILER_DARK_MAPID: String? = null + val SERVICES_POSTHOG_HOST: String? = null + val SERVICES_POSTHOG_APIKEY: String? = null + val SERVICES_SENTRY_DSN: String? = null + val BUG_REPORT_URL: String = "https://rageshake.the-revivalists.org/api/submit" + val BUG_REPORT_APP_NAME: String = "element-x-android" + + const val PUSH_CONFIG_INCLUDE_FIREBASE = true + const val PUSH_CONFIG_INCLUDE_UNIFIED_PUSH = true +} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 3e8f8f57b01..7319c031110 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -25,7 +25,7 @@ internal fun DependencyHandler.implementation(dependency: Any) = add("implementa private fun DependencyHandlerScope.implementation( dependency: Any, config: Action -) = dependencies.add("implementation", dependency, closureOf { config.execute(this) }) +) = dependencies.add("implementation", dependency, closureOf { config.execute(this) }) private fun DependencyHandlerScope.androidTestImplementation(dependency: Any) = dependencies.add("androidTestImplementation", dependency) @@ -68,7 +68,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:eventformatter:impl")) implementation(project(":libraries:indicator:impl")) implementation(project(":libraries:permissions:impl")) - implementation(project(":libraries:push:impl")) + implementation(project(":libraries:audio:impl")) implementation(project(":libraries:push:impl")) implementation(project(":libraries:featureflag:impl")) implementation(project(":libraries:pushstore:impl")) @@ -114,14 +114,30 @@ fun DependencyHandlerScope.allServicesImpl() { implementation(project(":services:toolbox:impl")) } -fun DependencyHandlerScope.allEnterpriseImpl(project: Project) = addAll(project, "enterprise", "impl") +fun DependencyHandlerScope.allEnterpriseImpl(project: Project) = addAll( + project = project, + modulePrefix = ":enterprise:features", + moduleSuffix = ":impl", +) -fun DependencyHandlerScope.allFeaturesImpl(project: Project) = addAll(project, "features", "impl") +fun DependencyHandlerScope.allFeaturesImpl(project: Project) = addAll( + project = project, + modulePrefix = ":features", + moduleSuffix = ":impl", +) -fun DependencyHandlerScope.allFeaturesApi(project: Project) = addAll(project, "features", "api") +fun DependencyHandlerScope.allFeaturesApi(project: Project) = addAll( + project = project, + modulePrefix = ":features", + moduleSuffix = ":api", +) -private fun DependencyHandlerScope.addAll(project: Project, prefix: String, suffix: String) { - val subProjects = project.rootProject.subprojects.filter { it.path.startsWith(":$prefix") && it.path.endsWith(":$suffix") } +private fun DependencyHandlerScope.addAll( + project: Project, + modulePrefix: String, + moduleSuffix: String, +) { + val subProjects = project.rootProject.subprojects.filter { it.path.startsWith(modulePrefix) && it.path.endsWith(moduleSuffix) } for (p in subProjects) { add("implementation", p) } diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index 35aa9042446..df927f48d62 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -49,12 +49,11 @@ private fun Project.kover(action: Action) { fun Project.setupKover() { // Create verify all task joining all existing verification tasks - task("koverVerifyAll") { + tasks.register("koverVerifyAll") { group = "verification" description = "Verifies the code coverage of all subprojects." val dependencies = listOf(":app:koverVerifyGplayDebug") + koverVariants.map { ":app:koverVerify${it.replaceFirstChar(Char::titlecase)}" } dependsOn(dependencies) - } // https://kotlin.github.io/kotlinx-kover/ // Run `./gradlew :app:koverHtmlReport` to get report at ./app/build/reports/kover @@ -176,11 +175,13 @@ fun Project.setupKover() { "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState", "io.element.android.libraries.maplibre.compose.SymbolState", "io.element.android.libraries.matrix.api.room.RoomMembershipState", - "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState", + "io.element.android.libraries.matrix.api.room.RoomMembersState", "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*", "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*", "io.element.android.libraries.mediaviewer.impl.local.pdf.PdfViewerState", + "io.element.android.libraries.mediaviewer.impl.local.player.MediaPlayerControllerState", "io.element.android.libraries.textcomposer.model.TextEditorState", + "io.element.android.libraries.textcomposer.components.FormattingOptionState", ) includes.classes("*State") } diff --git a/plugins/src/main/kotlin/extension/VariantDimensionExtension.kt b/plugins/src/main/kotlin/extension/VariantDimensionExtension.kt new file mode 100644 index 00000000000..a7589f02a9a --- /dev/null +++ b/plugins/src/main/kotlin/extension/VariantDimensionExtension.kt @@ -0,0 +1,32 @@ +/* + * 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 extension + +import com.android.build.api.dsl.VariantDimension + +fun VariantDimension.buildConfigFieldStr( + name: String, + value: String, +) { + buildConfigField( + type = "String", + name = name, + value = "\"$value\"" + ) +} + +fun VariantDimension.buildConfigFieldBoolean( + name: String, + value: Boolean, +) { + buildConfigField( + type = "boolean", + name = name, + value = value.toString() + ) +} diff --git a/plugins/src/main/kotlin/extension/locales.kt b/plugins/src/main/kotlin/extension/locales.kt index 4b83bac32ae..b123ac49144 100644 --- a/plugins/src/main/kotlin/extension/locales.kt +++ b/plugins/src/main/kotlin/extension/locales.kt @@ -6,12 +6,15 @@ val locales = setOf( "be", "bg", "cs", + "cy", + "da", "de", "el", "en", - "en_US", + "en-rUS", "es", "et", + "eu", "fa", "fi", "fr", @@ -19,6 +22,7 @@ val locales = setOf( "in", "it", "ka", + "lt", "nb", "nl", "pl", @@ -28,7 +32,9 @@ val locales = setOf( "ru", "sk", "sv", + "tr", "uk", + "ur", "uz", "zh-rCN", "zh-rTW", diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts deleted file mode 100644 index a6e5e27e284..00000000000 --- a/samples/minimal/build.gradle.kts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ -plugins { - id("io.element.android-compose-application") - alias(libs.plugins.kotlin.android) -} - -android { - namespace = "io.element.android.samples.minimal" - - defaultConfig { - applicationId = "io.element.android.samples.minimal" - targetSdk = Versions.TARGET_SDK - versionCode = Versions.VERSION_CODE - versionName = Versions.VERSION_NAME - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildFeatures { - buildConfig = true - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } -} - -dependencies { - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.preference) - implementation(projects.libraries.matrix.api) - implementation(projects.libraries.matrix.impl) - implementation(projects.libraries.permissions.noop) - implementation(projects.libraries.sessionStorage.implMemory) - implementation(projects.libraries.designsystem) - implementation(projects.libraries.architecture) - implementation(projects.libraries.core) - implementation(projects.libraries.network) - implementation(projects.libraries.dateformatter.impl) - implementation(projects.libraries.eventformatter.impl) - implementation(projects.libraries.fullscreenintent.impl) - implementation(projects.libraries.preferences.impl) - implementation(projects.libraries.preferences.test) - implementation(projects.libraries.indicator.impl) - implementation(projects.features.invite.impl) - implementation(projects.features.roomlist.impl) - implementation(projects.features.leaveroom.impl) - implementation(projects.features.login.impl) - implementation(projects.features.logout.impl) - implementation(projects.features.networkmonitor.impl) - implementation(projects.services.toolbox.impl) - implementation(projects.libraries.featureflag.impl) - implementation(projects.services.analytics.noop) - implementation(libs.coroutines.core) - implementation(projects.libraries.push.test) -} diff --git a/screenshots/de/appnav.loggedin_LoggedInView_Day_2_de.png b/screenshots/de/appnav.loggedin_LoggedInView_Day_2_de.png index 0feeb9dc02e..ce6fe49129a 100644 --- a/screenshots/de/appnav.loggedin_LoggedInView_Day_2_de.png +++ b/screenshots/de/appnav.loggedin_LoggedInView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87420abab71cb7bc0bdd018e18425e9c3d1813059563023af0bf49648258bc7c -size 43627 +oid sha256:99724a37f99aa3a1e89a4ece346c4f9dd6851535315de913ff7cb2d19e382444 +size 43216 diff --git a/screenshots/de/appnav.loggedin_LoggedInView_Day_3_de.png b/screenshots/de/appnav.loggedin_LoggedInView_Day_3_de.png index 4da18362e2f..d8c4713dda9 100644 --- a/screenshots/de/appnav.loggedin_LoggedInView_Day_3_de.png +++ b/screenshots/de/appnav.loggedin_LoggedInView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20431573f0b6dc78fc717e93f86e17f70b3998f70b71680c07cc098c464f233e -size 28738 +oid sha256:357e03b3d791ba37acc706b1857c774194d3f5a56f94921135bbb5744229cedb +size 27594 diff --git a/screenshots/de/appnav.room.joined_LoadingRoomNodeView_Day_1_de.png b/screenshots/de/appnav.room.joined_LoadingRoomNodeView_Day_1_de.png index 2a72550a099..d3bdd13cb3c 100644 --- a/screenshots/de/appnav.room.joined_LoadingRoomNodeView_Day_1_de.png +++ b/screenshots/de/appnav.room.joined_LoadingRoomNodeView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a27b5d56acfea6a41e0eeffc043e4a0655f8850c56543b728b01388e4b076846 -size 13021 +oid sha256:e6ec5c2ed06463e7642443c89500e3e4864e439382f5358d78db0b53cddfe2d5 +size 13019 diff --git a/screenshots/de/appnav.root_RootView_Day_0_de.png b/screenshots/de/appnav.root_RootView_Day_0_de.png index c6fc025d791..a744d9fe8ae 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:3ef98f89254f7c8e2172d9a340af7983a17b6d944d7305bddc5bd38aad72cc57 -size 26306 +oid sha256:d78407b5a9f4ff3fc9579ca734436fb9096fab35a1edcefd117c6a366a9640a7 +size 26395 diff --git a/screenshots/de/appnav.root_RootView_Day_1_de.png b/screenshots/de/appnav.root_RootView_Day_1_de.png index 72e9d4226b3..02cbca77b6d 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:87b47b5d2e77e45551ba0061d53b9a115e7ee8e6f0b46ef655aa2281017711e4 -size 30334 +oid sha256:4419206d6e95d6de97c7594d19157e93791aee7826fb881d889e1552d10989f5 +size 30785 diff --git a/screenshots/de/features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_de.png b/screenshots/de/features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_de.png index ab51fa5996b..d082e89bfce 100644 --- a/screenshots/de/features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_de.png +++ b/screenshots/de/features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b59a1a86287872774c28b99a9a0fd5f554cc6b9558228d17540e80727fdbbb07 -size 24984 +oid sha256:8725accabc4de494b460b769686787c8e16801ff4249d32e74cde7be013e339e +size 25505 diff --git a/screenshots/de/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_de.png b/screenshots/de/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_de.png new file mode 100644 index 00000000000..1b639af6f18 --- /dev/null +++ b/screenshots/de/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f94d7539ded3cd2494371dd8e7df89036946157c31c7cdbf5d646ea901942733 +size 20796 diff --git a/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_0_de.png b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_0_de.png index 03e5dd40ff8..222d7385b2f 100644 --- a/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_0_de.png +++ b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96700c75e41ee3dad9305b8dabaab0c8a46e629439884b6b5fa447cd5e1f6f63 -size 84144 +oid sha256:b6b99f8f732aacb6a77bf856bbb5e23fb1478bf7ce90eafef9e7ba7b67da215f +size 86774 diff --git a/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png new file mode 100644 index 00000000000..9aff4b7a4ab --- /dev/null +++ b/screenshots/de/features.analytics.impl_AnalyticsOptInView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11155727ceed9e3c6f22b149d90dd92b58af5e4be729e6920e7071d9f9734530 +size 84388 diff --git a/screenshots/de/features.call.impl.ui_CallScreenView_Day_1_de.png b/screenshots/de/features.call.impl.ui_CallScreenView_Day_1_de.png index 1ff5d0b1d43..0a83b121925 100644 --- a/screenshots/de/features.call.impl.ui_CallScreenView_Day_1_de.png +++ b/screenshots/de/features.call.impl.ui_CallScreenView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48beee519b4b93c8d6175c59f21f7ecac80a77562639cb5ff3800a5da1dd9314 -size 14206 +oid sha256:b06d5375521caae14aa0621493a93e562e0ec655b9e83e19d05f9c1d9ffbb628 +size 11571 diff --git a/screenshots/de/features.call.impl.ui_CallScreenView_Day_2_de.png b/screenshots/de/features.call.impl.ui_CallScreenView_Day_2_de.png index b0cdc880cf7..9367c10d954 100644 --- a/screenshots/de/features.call.impl.ui_CallScreenView_Day_2_de.png +++ b/screenshots/de/features.call.impl.ui_CallScreenView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27dc69f4e564335bfc868fce9e63448db98e9d82a60d364fc627912659e1025c -size 14077 +oid sha256:6011ceb2ef963b0410fd0bc3c01906c56a7f1b2a73f36c95f0951469c98f77dd +size 11128 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 1663a623123..15ebcffdd4f 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:240af0e27b7159df0f4f11a1d64f56ea1f209f7dc5de86998c5a9717e61ff82e -size 22008 +oid sha256:e3f09b0342378a82bbe394877fd03662f8d6554a1f91c9304e7118d171754f6d +size 19159 diff --git a/screenshots/de/features.call.impl.ui_IncomingCallScreen_Day_0_de.png b/screenshots/de/features.call.impl.ui_IncomingCallScreen_Day_0_de.png index fc00862303b..f8ca7bb279b 100644 --- a/screenshots/de/features.call.impl.ui_IncomingCallScreen_Day_0_de.png +++ b/screenshots/de/features.call.impl.ui_IncomingCallScreen_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14d558b06046c968017a7851b139f9c28a6a2a229b70ffda78ba356c8c0732e6 -size 68305 +oid sha256:99f8cabcb8d5d9301b3f9a35ea9f7220ed806046e7ed643a1261b62805f8f2fe +size 68307 diff --git a/screenshots/de/features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_de.png b/screenshots/de/features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_de.png new file mode 100644 index 00000000000..d19731fd423 --- /dev/null +++ b/screenshots/de/features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2a0bc5c60dd6014333ca545806986bf01dcae29939b6740993a3f3482ebbd72 +size 26953 diff --git a/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_0_de.png b/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_0_de.png index 202f5ec9037..4d45624b4a4 100644 --- a/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_0_de.png +++ b/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2683c2af9b6e03d8a63478a277bc08fee3938a48cf446e81fee14b8177a7a0e5 -size 17005 +oid sha256:c0aaddb6b3cfb442b17c0b2b494ee65127bb0cb7c864849c0c25652f8822f344 +size 15631 diff --git a/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_1_de.png b/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_1_de.png index 20fb2fd98d5..14f820e342b 100644 --- a/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_1_de.png +++ b/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fabdf30f91e5d14fdb41a78f337adc1781e572bbdb346be34b6de1738362b577 -size 26228 +oid sha256:d5d4134ba0b967c7639134044c64251687a44a6066b35f32c1f9f2b64611fc5e +size 25480 diff --git a/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_2_de.png b/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_2_de.png index 96eecaf37ed..57134784b84 100644 --- a/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_2_de.png +++ b/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47afc803337d253f9fe1b1b986ee0a30585fac817d76e8244f46f69462b080f4 -size 87792 +oid sha256:0236538a54605f6ebf22a2e13140d0b2c2531f857310ea52ee2dbfc1af83a48c +size 87006 diff --git a/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_3_de.png b/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_3_de.png index 27a46e84842..7c7700d4e33 100644 --- a/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_3_de.png +++ b/screenshots/de/features.createroom.impl.addpeople_AddPeopleView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00a2119357556362eea4ffc31e982e87e0a2e18af95ab713b433851d5ad99d39 -size 44744 +oid sha256:beebb1791d4b326ecab5f90c9a1d1acee6af667687438bf8510f804197848d33 +size 43490 diff --git a/screenshots/de/features.createroom.impl.components_SearchMultipleUsersResultItem_de.png b/screenshots/de/features.createroom.impl.components_SearchMultipleUsersResultItem_de.png index 4767ad627fb..47311f06491 100644 --- a/screenshots/de/features.createroom.impl.components_SearchMultipleUsersResultItem_de.png +++ b/screenshots/de/features.createroom.impl.components_SearchMultipleUsersResultItem_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f23acc587c36ba3ec30f22c235da2e687561187d68cd0cae97ef4211d64c59ab -size 99982 +oid sha256:36c2612774a37b20202d2f6e9132f9fa8aaa65567def256f072dcaa532233659 +size 100794 diff --git a/screenshots/de/features.createroom.impl.components_SearchSingleUserResultItem_de.png b/screenshots/de/features.createroom.impl.components_SearchSingleUserResultItem_de.png index 7b04660575a..2b2b08eefee 100644 --- a/screenshots/de/features.createroom.impl.components_SearchSingleUserResultItem_de.png +++ b/screenshots/de/features.createroom.impl.components_SearchSingleUserResultItem_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76ec5d3975491b745381ab49b8e6df5e3455ae0a33185f4d277680d315377d7f -size 51416 +oid sha256:742bbbcf30ae502a2d13475fc1f2b045bb239cb04995c74deadef630dacfae7d +size 51494 diff --git a/screenshots/de/features.createroom.impl.components_UserListView_Day_0_de.png b/screenshots/de/features.createroom.impl.components_UserListView_Day_0_de.png index d2a66523698..b01e8f3daa3 100644 --- a/screenshots/de/features.createroom.impl.components_UserListView_Day_0_de.png +++ b/screenshots/de/features.createroom.impl.components_UserListView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a883439100e721d0749da6690882dad0decd6962ee82599fc7902746415d63d -size 9596 +oid sha256:1f3d3e011b7b2fd9f0732e9632333d65c6f22ddb4df448128ad18bce05fdea19 +size 9392 diff --git a/screenshots/de/features.createroom.impl.components_UserListView_Day_1_de.png b/screenshots/de/features.createroom.impl.components_UserListView_Day_1_de.png index 39a409488ef..71d58d0eb70 100644 --- a/screenshots/de/features.createroom.impl.components_UserListView_Day_1_de.png +++ b/screenshots/de/features.createroom.impl.components_UserListView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9a7ec6c7c6406da82cbc68a31e1ac6af0c7cd4fcb9a162f2f222a0b98d43ace -size 20914 +oid sha256:0678bcfff323a0fa3991d45c36d45d9d16e97e6c670be714d6de612440b22bef +size 20710 diff --git a/screenshots/de/features.createroom.impl.components_UserListView_Day_2_de.png b/screenshots/de/features.createroom.impl.components_UserListView_Day_2_de.png index 7d0eda57cdd..d96220d9235 100644 --- a/screenshots/de/features.createroom.impl.components_UserListView_Day_2_de.png +++ b/screenshots/de/features.createroom.impl.components_UserListView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c02ed04dd75085c6b80fd8a9360ef90a15a3169d9eab16973fff7b8db880ecd -size 7972 +oid sha256:646f754dc9a02989008e469b2fbc8928bd12492c3de4b108c8a64508aae16ad2 +size 7970 diff --git a/screenshots/de/features.createroom.impl.components_UserListView_Day_7_de.png b/screenshots/de/features.createroom.impl.components_UserListView_Day_7_de.png index 4d60fd07be2..5e8d68e7ac2 100644 --- a/screenshots/de/features.createroom.impl.components_UserListView_Day_7_de.png +++ b/screenshots/de/features.createroom.impl.components_UserListView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:851c6245825be513539c6be9b76f396a7490f03c7c03b6df12c056a177497524 -size 12344 +oid sha256:8342dae1c9b3769d867d856ecdc3b3302fa5cd94e80c6ad9ceddf482968305f1 +size 12343 diff --git a/screenshots/de/features.createroom.impl.components_UserListView_Day_9_de.png b/screenshots/de/features.createroom.impl.components_UserListView_Day_9_de.png index 82cb2036e17..821662ba5f0 100644 --- a/screenshots/de/features.createroom.impl.components_UserListView_Day_9_de.png +++ b/screenshots/de/features.createroom.impl.components_UserListView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89fecc1b67392dae4b62e99963c440db63adddcc0c9c4f6d81408560063b870d -size 37718 +oid sha256:95148a84c9a1d4574bfbf995a15d2711f59a26015fa6b928207b5e1266b72adb +size 37540 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 00bec43e133..4afc5c3f0ca 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:457363c25841925076aaad53d728e631427f699cfb7545b8e1abb94fb6a2e774 -size 36741 +oid sha256:c86133bc1055fb12fc5648f19d7e9681aec831a6adae6fe1a6c3f4cfd36b3bf7 +size 36334 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 e09d9d4951b..86cdff977bd 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:baf2c57107cd2f639535fc6fdf5b9885224c0f839d78fdbc85e14398f7439cd2 -size 49576 +oid sha256:d373d4113ea7005d53cfce5e37e81583173d06cc07026b6e06de48d770eba322 +size 49190 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 ba26587c923..ce4ace450f6 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:eba277a783d50012cf4a51ec93c4cf02394befc9c5d04edd27ac8113e9257539 -size 63564 +oid sha256:95b4f1c1931cda1075740a4d85ec65144494195e866dd9f6731180e7901313ce +size 63156 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 09b64115e15..3d9d8680139 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:c1f26d21ba37d244fcc5adb5b7580856f8e1a7670fd2defcd4befa3e2d07b8b6 -size 63685 +oid sha256:1ea98f6f83764fba04aeb2027da015c18c3a9a3061ddba81d0851960ce2c35de +size 63713 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 81b004f86b6..090247cb6ca 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:52f75d9234a0eace30ec10cbdeda6314cea0634d0ccbd2b8f453ec184a35cd13 -size 63592 +oid sha256:57df40244063bf4e0f7055a32ffcedc1dded8501f35da9fd22f531a475c32c4c +size 63897 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 5308d79c1e9..ffd478c101a 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:6c28710d2afe583078a26c8b56c331a942c250cb599d4c00f9dc4cf04286c811 -size 63082 +oid sha256:6cae2c64edc738f54d41c96131a7c2a64f7c53f1bad4ed73672b921ce9eddfbb +size 63128 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 1f6bca734e3..938c21b3526 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:9b449faf40487c6f34253b0157ae14c3df6a8a782b2af18fcd2b6f80198fa7d2 -size 37688 +oid sha256:5d7a4d0453da7a91bcaabc426379b996648469114468f5b153c1f0fee699ae42 +size 37321 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 ba57e7b40be..1b24f9bb3e1 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:09596614dfe191f7eb046aff9d44642e6ecbbf3fbdcb89181bae75c16d6ba72d -size 50492 +oid sha256:8dbb04991ea8e60de9abc3251400c65fa9a2cc43d52d4d8117dbbf1ee9d56bd8 +size 50146 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 08823300da9..bbb18fc8fb7 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:ae642483fc05fb50555e2fa5b83ac12d84a064c4b840a53a4ebf53d419f87242 -size 65036 +oid sha256:57ddf4a6fcb6586e9d7e43574f7b832542bf43e7337355d1a1653f05b98fe130 +size 64657 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 3b9922cb362..3b9d09cd883 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:28606cedfc76d8600f8e935a42c4e98dc13c0b1d1733856bc20d064f99a39c5b -size 65826 +oid sha256:dff63fd5eeda1bcc89432edecaf0c0b446a88b0acae7851a079c44d4f1f8d340 +size 66009 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 837e2badf78..80d285c5ebd 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:3128a8b8ca419b6862d1e5a3e8713f4aefd6fccd494babf006ccbbe15391c956 -size 65747 +oid sha256:4dca087949a1b4374be7464b97f200addee8633d4aaa769a523a193aa5536932 +size 66176 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 7acd884662d..0c6ee311c51 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:54396bda85421376b596d885a43f67a19b06e9d7980d01112b90849ae08568e1 -size 65181 +oid sha256:453f0f19a42aaf92e11f9f0a3fe18a8f63371d3c4f816c6a7c81af094523fd7e +size 65264 diff --git a/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_0_de.png b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_0_de.png new file mode 100644 index 00000000000..17246e64872 --- /dev/null +++ b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b48ef870871bbdc901721b1e25caec609b4a2b4e96dd442344d87307cd15b166 +size 18236 diff --git a/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_1_de.png b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_1_de.png new file mode 100644 index 00000000000..77e03e96c52 --- /dev/null +++ b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d39af29b567ef13015fe236e982a571ab3b19ebeac0ae94d09b3987423ab41e6 +size 18259 diff --git a/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_2_de.png b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_2_de.png new file mode 100644 index 00000000000..a380d66caff --- /dev/null +++ b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0202c0c34ea077bf521161582ce081493dba6cc806375a1b1cd7173470ea3b07 +size 18463 diff --git a/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_3_de.png b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_3_de.png new file mode 100644 index 00000000000..df568726574 --- /dev/null +++ b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1530c35953f8ca11c70f2c372d8309d5a76a23b5967c9d5542b4703910c6369b +size 21701 diff --git a/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_4_de.png b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_4_de.png new file mode 100644 index 00000000000..669efaedcbb --- /dev/null +++ b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7901747c698bd155d36eb7de49e05b2cfed2d522d2548991cf66da34a3d7c9d +size 21504 diff --git a/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_5_de.png b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_5_de.png new file mode 100644 index 00000000000..ea6d55e56f2 --- /dev/null +++ b/screenshots/de/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab50dfb6f6c49c113ec3ad0d169f917989f8663e83910c375554670379ff60da +size 21709 diff --git a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_0_de.png b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_0_de.png index 2d07ffb504a..73d157ad29f 100644 --- a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_0_de.png +++ b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b34cd0b4ea524e88df1185ed2223dcce00fb7efb7582f52dece9876b188a4cc -size 21796 +oid sha256:9bc8290faf20b4ea95bf0e7a86c2306b6f9d15b5bc764a48b78051b4d7c02655 +size 28052 diff --git a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_1_de.png b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_1_de.png index d4d6de240ca..cbf5a5f9ba5 100644 --- a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_1_de.png +++ b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b0d8218dd4e2b6c4adaf8a402aac976cc7fb76cb4df925476349185cb78d29d -size 21018 +oid sha256:7faeafca006a15ed6fd89f94f737841e59c46f035b02f5d7e3dee24e0a879890 +size 21014 diff --git a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_2_de.png b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_2_de.png index b41508628a5..965335c6966 100644 --- a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_2_de.png +++ b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:405d95758fca85df0f8dc5872185767ec1d360b4ba3a69063cb8315337180244 -size 31660 +oid sha256:0280c825cece016b8f569cb8ca469d4870817b2b3500138170112479afe69be9 +size 31659 diff --git a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_3_de.png b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_3_de.png index 874582fed6d..8d399c2c6a3 100644 --- a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_3_de.png +++ b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b5433313f6359603e64477fc7cdebb10782cbe3c2a9ac8087ff2252c11ece55 -size 48294 +oid sha256:922537688cdeb949b116e699358f886314a87242b0bdfb970c04143d9df0de9d +size 55087 diff --git a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_4_de.png b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_4_de.png new file mode 100644 index 00000000000..b380c1f5e26 --- /dev/null +++ b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96f00525ce15b20bda129396fb74c40168513e3e3010056908187a53b01c7b0d +size 45678 diff --git a/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_5_de.png b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_5_de.png new file mode 100644 index 00000000000..92c28ceeb87 --- /dev/null +++ b/screenshots/de/features.createroom.impl.root_CreateRoomRootView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fa4bc20f0199ac31acf1d9d93d1457004769bfe89957a769fb06a97435becbb +size 32125 diff --git a/screenshots/de/features.ftue.impl.notifications_NotificationsOptInView_Day_0_de.png b/screenshots/de/features.ftue.impl.notifications_NotificationsOptInView_Day_0_de.png index 1637b4b00ec..6b0618101f5 100644 --- a/screenshots/de/features.ftue.impl.notifications_NotificationsOptInView_Day_0_de.png +++ b/screenshots/de/features.ftue.impl.notifications_NotificationsOptInView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dd80a73d67f1d18c52606aeebc9dcf971857a5f52e4490932499487ca350d08 -size 70530 +oid sha256:bea6b7dc353227e8954d60ac072e29cc3321883f4a59fff6d1506a810232726e +size 71375 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 new file mode 100644 index 00000000000..0eaaf469bbc --- /dev/null +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f504c27591945241b3ed522cdb73701ee06d1f270e20812fe0c1cc7ca34befa +size 38341 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 new file mode 100644 index 00000000000..1e5e3b3760e --- /dev/null +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1383b923b65dbf64e06b2ba8b4f6b342cc955000813b2131e8d0afedde891c1 +size 31598 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 new file mode 100644 index 00000000000..4d97b8f26fc --- /dev/null +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f90840c466e9fab81a98b03fe603ce3c660bdf8321f5cb7df358035ba9fce79 +size 44419 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 new file mode 100644 index 00000000000..350221a32f1 --- /dev/null +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7496ae74e51150486e96a50c30cb143af89d2944f6ef86aa95193703a4889e9d +size 38034 diff --git a/screenshots/de/features.ftue.impl.welcome_WelcomeView_Day_0_de.png b/screenshots/de/features.ftue.impl.welcome_WelcomeView_Day_0_de.png index cad600bbe21..5c1df9612de 100644 --- a/screenshots/de/features.ftue.impl.welcome_WelcomeView_Day_0_de.png +++ b/screenshots/de/features.ftue.impl.welcome_WelcomeView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94503fb4de93a765f8d1cb8d989fb798b3e764854c931d5504af2e27525cb278 +oid sha256:e750cc6220b601f5709c5e8f8d365145e269b5f904f3b5d08ada6a8a58047b11 size 273704 diff --git a/screenshots/de/features.home.impl.components_BatteryOptimizationBanner_Day_0_de.png b/screenshots/de/features.home.impl.components_BatteryOptimizationBanner_Day_0_de.png new file mode 100644 index 00000000000..9efcd392aa1 --- /dev/null +++ b/screenshots/de/features.home.impl.components_BatteryOptimizationBanner_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2aaec4eae3539e484846ed564bd3ed53d0f93e4f17c25f1fe3fc7983f6b2c96d +size 33379 diff --git a/screenshots/de/features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Day_0_de.png b/screenshots/de/features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_de.png similarity index 100% rename from screenshots/de/features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Day_0_de.png rename to screenshots/de/features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_de.png diff --git a/screenshots/de/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_de.png b/screenshots/de/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_de.png new file mode 100644 index 00000000000..81502aa2602 --- /dev/null +++ b/screenshots/de/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4da31f61c7fd79c994b703d160a31f046590ca144100dd5ad50f522af4a8d6b8 +size 40716 diff --git a/screenshots/de/features.home.impl.components_DefaultRoomListTopBar_Day_0_de.png b/screenshots/de/features.home.impl.components_DefaultRoomListTopBar_Day_0_de.png new file mode 100644 index 00000000000..c74619a286f --- /dev/null +++ b/screenshots/de/features.home.impl.components_DefaultRoomListTopBar_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7ca367d0e4117fa458470bb4230adebd3c08547a63ae54eaafb09e6522cb0e9 +size 40435 diff --git a/screenshots/de/features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_de.png b/screenshots/de/features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_de.png new file mode 100644 index 00000000000..73694f2f013 --- /dev/null +++ b/screenshots/de/features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d968d7cc5c20e3db5e8917d8ce6322278ed7c2cf71bf27067d51a1ab0f6e78 +size 35146 diff --git a/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_0_de.png b/screenshots/de/features.home.impl.components_RoomListContentView_Day_0_de.png similarity index 100% rename from screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_0_de.png rename to screenshots/de/features.home.impl.components_RoomListContentView_Day_0_de.png diff --git a/screenshots/de/features.home.impl.components_RoomListContentView_Day_1_de.png b/screenshots/de/features.home.impl.components_RoomListContentView_Day_1_de.png new file mode 100644 index 00000000000..30415e01e07 --- /dev/null +++ b/screenshots/de/features.home.impl.components_RoomListContentView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2edd2e77def7a88f30e1e835e5a3700f80cb6a91c0b854a83b61c67a20c6d29c +size 22234 diff --git a/screenshots/de/features.home.impl.components_RoomListContentView_Day_3_de.png b/screenshots/de/features.home.impl.components_RoomListContentView_Day_3_de.png new file mode 100644 index 00000000000..971888c147c --- /dev/null +++ b/screenshots/de/features.home.impl.components_RoomListContentView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8de94c413f4dae795c37e7ec2c0790cccb007ffa24f1877d0ae20d3b141574e +size 22646 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 new file mode 100644 index 00000000000..c482ab36285 --- /dev/null +++ b/screenshots/de/features.home.impl.components_RoomListContentView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67e5572f219d8b84c6c652aefc6a07fa056a66c688610939cdeb601974ae04aa +size 55975 diff --git a/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_29_de.png b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_29_de.png new file mode 100644 index 00000000000..8cf571f8141 --- /dev/null +++ b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_29_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a8f4b3792370cc2a11d236d3d8a2b31fd326369a1682db0708428c37c1d2575 +size 22729 diff --git a/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_2_de.png b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_2_de.png similarity index 100% rename from screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_2_de.png rename to screenshots/de/features.home.impl.components_RoomSummaryRow_Day_2_de.png diff --git a/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_30_de.png b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_30_de.png new file mode 100644 index 00000000000..2f32a78319a --- /dev/null +++ b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_30_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd5f51e5d8f363aefa9d77c893d371a29d5d6a53eae2ccc69e432fded1b95c9c +size 16441 diff --git a/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_31_de.png b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_31_de.png similarity index 100% rename from screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_31_de.png rename to screenshots/de/features.home.impl.components_RoomSummaryRow_Day_31_de.png diff --git a/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_32_de.png b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_32_de.png similarity index 100% rename from screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_32_de.png rename to screenshots/de/features.home.impl.components_RoomSummaryRow_Day_32_de.png diff --git a/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_33_de.png b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_33_de.png similarity index 100% rename from screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_33_de.png rename to screenshots/de/features.home.impl.components_RoomSummaryRow_Day_33_de.png diff --git a/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_34_de.png b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_34_de.png new file mode 100644 index 00000000000..466eac13dbd --- /dev/null +++ b/screenshots/de/features.home.impl.components_RoomSummaryRow_Day_34_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49516bd37dbeee3f00a6cff3f595018e4e779cbb6b4e6dfbcae525e00f78d1a9 +size 14911 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 new file mode 100644 index 00000000000..944e83e717f --- /dev/null +++ b/screenshots/de/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6a67cf2cff22301277baa89c2d03e23668aa085768c062219db5bfca5f93f6c +size 38111 diff --git a/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_0_de.png b/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_0_de.png new file mode 100644 index 00000000000..ee132950424 --- /dev/null +++ b/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c302a8b79f894fa47ceef03d3c9095276a8581a3278ec066f0468a2781d1580b +size 14269 diff --git a/screenshots/de/features.roomlist.impl.filters_RoomListFiltersView_Day_1_de.png b/screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_1_de.png similarity index 100% rename from screenshots/de/features.roomlist.impl.filters_RoomListFiltersView_Day_1_de.png rename to screenshots/de/features.home.impl.filters_RoomListFiltersView_Day_1_de.png 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 00000000000..0b9bf1ebba9 --- /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:ccf1dfef74f6e7d7237f65d51e8beccb332e562a90b736f8fb6abf5f2a2780c8 +size 29514 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 00000000000..b0a8b71423d --- /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:5f928fff19a317e056a08bbc39f28d7f44241fb1bc1a41fb36236e5e9986a240 +size 24192 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 00000000000..ac5a253bfaa --- /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:a19c853a8c36128d3df9ecfff8acdebfccc37ff1bab1b1cdaf3336cb67a0390d +size 24468 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 00000000000..52ed3cc77aa --- /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:21dc02fd3f71b1a8949c8c4b5ea1beaf030b62cc3a162bb161b5dadbe2fbc029 +size 26690 diff --git a/screenshots/de/features.roomlist.impl.search_RoomListSearchContent_Day_2_de.png b/screenshots/de/features.home.impl.search_RoomListSearchContent_Day_1_de.png similarity index 100% rename from screenshots/de/features.roomlist.impl.search_RoomListSearchContent_Day_2_de.png rename to screenshots/de/features.home.impl.search_RoomListSearchContent_Day_1_de.png diff --git a/screenshots/de/features.home.impl_HomeView_Day_0_de.png b/screenshots/de/features.home.impl_HomeView_Day_0_de.png new file mode 100644 index 00000000000..9facb9192b8 --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e4c98b0229caeb956cf81b81471da453cb8df1f8d544e33442a295e5fda4e77 +size 84014 diff --git a/screenshots/de/features.home.impl_HomeView_Day_10_de.png b/screenshots/de/features.home.impl_HomeView_Day_10_de.png new file mode 100644 index 00000000000..2b164368204 --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_10_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1125f3cc2f072a6dbb20a4b5ec2a38e6b64ce4f63999f07076978ee61aada93d +size 50269 diff --git a/screenshots/de/features.home.impl_HomeView_Day_13_de.png b/screenshots/de/features.home.impl_HomeView_Day_13_de.png new file mode 100644 index 00000000000..7054f6ad420 --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_13_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f028b80eb5328c941dc919d78776b63460385004f4c9040672f1c2c887f1d4fd +size 107800 diff --git a/screenshots/de/features.home.impl_HomeView_Day_14_de.png b/screenshots/de/features.home.impl_HomeView_Day_14_de.png new file mode 100644 index 00000000000..98fe76fa22d --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_14_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b14795260115bc3eb91066fba38bdcfcadd3cd97d7b2b63188e719c821de9739 +size 105783 diff --git a/screenshots/de/features.home.impl_HomeView_Day_1_de.png b/screenshots/de/features.home.impl_HomeView_Day_1_de.png new file mode 100644 index 00000000000..91e84e64cd6 --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10186bb917f1ac07ffea0e8fbebbdc3d2f311ba58a1eba29ffb29645eb1429c6 +size 83780 diff --git a/screenshots/de/features.home.impl_HomeView_Day_2_de.png b/screenshots/de/features.home.impl_HomeView_Day_2_de.png new file mode 100644 index 00000000000..9facb9192b8 --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e4c98b0229caeb956cf81b81471da453cb8df1f8d544e33442a295e5fda4e77 +size 84014 diff --git a/screenshots/de/features.home.impl_HomeView_Day_3_de.png b/screenshots/de/features.home.impl_HomeView_Day_3_de.png new file mode 100644 index 00000000000..994d5a4a33c --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0acc8622aa49dc827461b9714f7705862aad85a3381f7d273aedee2ee2dab74e +size 82992 diff --git a/screenshots/de/features.home.impl_HomeView_Day_5_de.png b/screenshots/de/features.home.impl_HomeView_Day_5_de.png new file mode 100644 index 00000000000..9facb9192b8 --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e4c98b0229caeb956cf81b81471da453cb8df1f8d544e33442a295e5fda4e77 +size 84014 diff --git a/screenshots/de/features.home.impl_HomeView_Day_6_de.png b/screenshots/de/features.home.impl_HomeView_Day_6_de.png new file mode 100644 index 00000000000..2cbf067c14c --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a78db5c27dc2a7a1192c4cc5ef0fd156c74639c85dfb578d246da3ac11ba6ba +size 63181 diff --git a/screenshots/de/features.home.impl_HomeView_Day_7_de.png b/screenshots/de/features.home.impl_HomeView_Day_7_de.png new file mode 100644 index 00000000000..4d7b09165ee --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c195eccd87cdd2d6cbdd1db91289332251b95d3723c0e74ec280d5294023fe30 +size 62514 diff --git a/screenshots/de/features.home.impl_HomeView_Day_8_de.png b/screenshots/de/features.home.impl_HomeView_Day_8_de.png new file mode 100644 index 00000000000..05d0af262af --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7d95599664c8ff48e13e4fd9596b0f0f868840bee286e1584c95dc7b2f6c5d3 +size 60788 diff --git a/screenshots/de/features.home.impl_HomeView_Day_9_de.png b/screenshots/de/features.home.impl_HomeView_Day_9_de.png new file mode 100644 index 00000000000..1e639ca9935 --- /dev/null +++ b/screenshots/de/features.home.impl_HomeView_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f2b6e35f63a3fa65ca5cc29f9d8ee4d4d1ab05574f5d047ad20652171ffaa22 +size 106016 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 new file mode 100644 index 00000000000..41b1a9cc8e4 --- /dev/null +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d0de166a7eaa29c991807b4f6052b75e34c504add3f58eb5e686df3013fc050 +size 24716 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 new file mode 100644 index 00000000000..397d0bbf4e6 --- /dev/null +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71125b91c537735f22e804c152024adf23cb5b662ac9a7bd39f8ab609df0cf8c +size 32102 diff --git a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_de.png b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_de.png new file mode 100644 index 00000000000..b944e9a7b2f --- /dev/null +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee8997d0c539dca790728f8b2227f6a8620c814f69656a209dcfc5fe12aa9f34 +size 22395 diff --git a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_de.png b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_de.png new file mode 100644 index 00000000000..2ec809e0f18 --- /dev/null +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97b6da2622c7491563081068c1cbe673b468d8306642c0abfc8f9a8d995c489f +size 22162 diff --git a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_de.png b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_de.png new file mode 100644 index 00000000000..b944e9a7b2f --- /dev/null +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee8997d0c539dca790728f8b2227f6a8620c814f69656a209dcfc5fe12aa9f34 +size 22395 diff --git a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_de.png b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_de.png new file mode 100644 index 00000000000..faaf39b56be --- /dev/null +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b23a9bdf58c4de0fff1e1bcca7639bfdd923672840e690799bb2cb234d400cc +size 40149 diff --git a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_de.png b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_de.png new file mode 100644 index 00000000000..3fdf7c1e232 --- /dev/null +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:872b1e7f7cd959aea66e6cdbe22619f612f0459b4f2b5e5aa7c396fbd640aa45 +size 44987 diff --git a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_de.png b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_de.png new file mode 100644 index 00000000000..27e4e2dfb92 --- /dev/null +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a377a17d91956a645b96e426f0c5220ea39c28b1e62bb45a32451e6bdedbd343 +size 40344 diff --git a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_de.png b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_de.png new file mode 100644 index 00000000000..dff5220be77 --- /dev/null +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96f8d48a72eda29ac87b3aabdb92fa6d620520ce2e1df42b428aed9984df33c7 +size 35141 diff --git a/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_de.png b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_de.png new file mode 100644 index 00000000000..f1ad659d154 --- /dev/null +++ b/screenshots/de/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9662120fe0a7596d0804df8d585311c55ba13eb2be0e37baecbe872c20274039 +size 39273 diff --git a/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_1_de.png b/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_1_de.png deleted file mode 100644 index 7f50bff7002..00000000000 --- a/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8116cf01fc86dc1bfc880567ab76fc1076b9b03b3aa042b14557c00dc2a87882 -size 25911 diff --git a/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_2_de.png b/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_2_de.png deleted file mode 100644 index d87b9642419..00000000000 --- a/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0ddd5c412727e290f0859e2584691694e10d7f543ef75b715c805cda67ca5e45 -size 25387 diff --git a/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_3_de.png b/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_3_de.png deleted file mode 100644 index 065525c2830..00000000000 --- a/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cafc872ec44168b89391fa78e3595d145c8c5733e260e23eceea8772a4099cfb -size 10185 diff --git a/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_4_de.png b/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_4_de.png deleted file mode 100644 index 065525c2830..00000000000 --- a/screenshots/de/features.invite.impl.response_AcceptDeclineInviteView_Day_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cafc872ec44168b89391fa78e3595d145c8c5733e260e23eceea8772a4099cfb -size 10185 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_10_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_10_de.png index 5cd0ea0b22f..dce73bf2b51 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_10_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de336859078455943d159d4d5d7fe710ecbce95baff983c4b74fab67ba6c16df -size 109581 +oid sha256:a399a5f72fd163ad19d6959fbc9f66248301f7b600394ae41712426f9a370921 +size 50313 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_11_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_11_de.png index 1f682b74ffa..4c8c93a210b 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_11_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19de55a1492cdbb55219720334ac6f5432518462e5a19c4783a32828d7fac899 -size 110605 +oid sha256:e5b35b4a6180ab7087ead686f92a2804db637f9812811eece972fd0ca814ade4 +size 48358 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_12_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_12_de.png index afb6b330059..5a318a8ffb8 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_12_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32c5e9a456ea8a2718e50eea5a90f719eff659ed9ada2ab2a9f8da23b2344644 -size 123199 +oid sha256:bde3d040e3b3a1d745422f7daa4b116552d797315225f3fd8f8a1f2064ceab0f +size 49605 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_13_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_13_de.png new file mode 100644 index 00000000000..ddd6ccd07a0 --- /dev/null +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_13_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7502335594b906de16b65eab01e5b53ddb88cedd6429ae9b60355552041a1893 +size 33348 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_14_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_14_de.png new file mode 100644 index 00000000000..b9f0cf17e63 --- /dev/null +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_14_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee45f1cac90a7f5004cbe5763cef5c6760c4a2f95524ed4a0b2b925369dbe40e +size 33050 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_15_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_15_de.png new file mode 100644 index 00000000000..de0a6b5b2f5 --- /dev/null +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_15_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0789f3a8606aaac930c111f060bf55d1e1fbe96831013e4e57614d41a07f8f17 +size 39940 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_16_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_16_de.png new file mode 100644 index 00000000000..5be5f707317 --- /dev/null +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_16_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:174fe7d3e527a760dfec6ee4ea08808c701bc03a41b024994e9475232d0d5aa6 +size 52860 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_1_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_1_de.png index 147fc99887e..d4ef1b6d4d3 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_1_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d233e3dedc7fd2e0876243e250c24e2cf5e3bfa7ade8f3615c641e781c04ad0 -size 113116 +oid sha256:b60a08065ce81f4ec5e4767311299e5130822fbb2657d52ed5d78cb12850bfb0 +size 42551 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_2_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_2_de.png index 4c40910f490..bddece65961 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_2_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da133e7680d61beb4367513f324ee9800bfaa6df338a3fec46ee6b69ef370566 -size 99473 +oid sha256:bde6c76f079cb2d234e626dd06ce12810567290f0a63edc5da006b7d47ecd59f +size 44360 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_3_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_3_de.png index 2fbecb35de4..2e41e77fe3e 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_3_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:203615332f2a04e244e1642e5216cf1fbe587451b4e6ef0928a02a8dbdfc83d4 -size 111758 +oid sha256:d6d386b185708760a4ce00aaeef6fc129a54599d7ea8773a57e21b584b48192a +size 32600 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_4_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_4_de.png index a0cee32c3de..bbcd6ae81a4 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_4_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45d925186a619b7fbe19211ddfc8c052a3571b53ea6fbc914d819473aed9328f -size 112045 +oid sha256:bf9f8daa72408ea1a859c3baf4a1c83c765822f24d5527c390c186c9f5465613 +size 47625 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_5_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_5_de.png index 623dada791a..9ae277ec441 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_5_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cbf98f8a0c6208379c64f62c2c49a858d67a60469f0001e496c248736a2e82f -size 120862 +oid sha256:a7215d224c586efad218d642d76792b258e784c1a8430bc9ad9e2d058e9f23c4 +size 33308 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_6_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_6_de.png index 73ad652a384..e7bf7654005 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_6_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e79835f689801de55e6c0b0db98b642d58e235d205f23e76c8eca383cbcfa1f4 -size 97392 +oid sha256:5cb20afb3c9e97728469a9f791ac2b937daaeaab00b3304b626f02f3f3689fef +size 31608 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_7_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_7_de.png index afba2b6c291..094b56a1a83 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_7_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb15d62e0dd87f76ce8c9f8e2909c4f4a5711cfcae89a81cef2bab0468db5054 -size 101310 +oid sha256:0e0551255bd0278b01196c2c5386df8f000dd46e0eaa577bec9ce2cf59544d82 +size 40468 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_8_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_8_de.png index a3aaef18244..e3c6c5bff3c 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_8_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a975e41d142e75969ca6e4148c5a87acc57c3fc78c1a1bac65207b09ce3fe38 -size 113797 +oid sha256:787f2a7755a4675f3ca3e897553fdf83bc11dcb0357d0bd2615dfc8e52c47cb3 +size 34939 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png new file mode 100644 index 00000000000..13c700e6388 --- /dev/null +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff7b93b639ed41344553337bdae929c7d60e6150a899e62ec9b0c2eff5ddbc1a +size 35962 diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_1_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_1_de.png index cf7480bdacf..8ba25e04291 100644 --- a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_1_de.png +++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2660a313aa5910965ea16ed0f522aa6b3d9ba338ad1982b6f56d8ffbc0540b8 -size 29320 +oid sha256:11bb28eb92749758f1e585b82702beae921bf658d8aca9ab86cab0f0475011c7 +size 29321 diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_3_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_3_de.png index a420dc37abd..ec5ebea7f17 100644 --- a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_3_de.png +++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d28fe185a24ddd29bad768ed4ec1e73f723d34cdb951b71ff244c8064b814eb +oid sha256:84c9a2104a9640cbb22ece5a98fdf923c9e6bba070f221cf296354f9d91874c9 size 44238 diff --git a/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_1_de.png b/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_1_de.png index 7bd8cfd6d76..361f000322c 100644 --- a/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_1_de.png +++ b/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa5f697707cfc2e6b8043575f39872b9347779447f3df1119dc486aef1031ad4 -size 21987 +oid sha256:68918b40c4e738d943f2f9fb94736b820303282d0eb9e0520f3d1c6b77169eae +size 22879 diff --git a/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_2_de.png b/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_2_de.png index f949f43d57c..0c7f70d4a9a 100644 --- a/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_2_de.png +++ b/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6f0565f64493b55beedb27fbd35cae1ddab6b25af667776d1a6802dd532ca92 -size 33375 +oid sha256:07645650921e92047a02fc99c5a3ad0f117bb7f3da27f41434e55867970b6e27 +size 35414 diff --git a/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_3_de.png b/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_3_de.png index f1086720d4c..248060117ab 100644 --- a/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_3_de.png +++ b/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb52a59a5fd4a917aea1abb9dd69025ad182fa53633904a43abbc295e36944a3 -size 35521 +oid sha256:22a2d91e9cbecf7903e70f885e3fc7807345c770ad73c1dd81487bc0655f8413 +size 39957 diff --git a/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_6_de.png b/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_6_de.png index cc04479d359..0c7f70d4a9a 100644 --- a/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_6_de.png +++ b/screenshots/de/features.leaveroom.api_LeaveRoomView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8322453763ba7dd597ee08b31a60d9ebc0e5bacfe437290b23b9764efcbd2f79 -size 36784 +oid sha256:07645650921e92047a02fc99c5a3ad0f117bb7f3da27f41434e55867970b6e27 +size 35414 diff --git a/screenshots/de/features.licenses.impl.list_DependencyLicensesListView_Day_2_de.png b/screenshots/de/features.licenses.impl.list_DependencyLicensesListView_Day_2_de.png index 7eaa94699b8..9f16ca802be 100644 --- a/screenshots/de/features.licenses.impl.list_DependencyLicensesListView_Day_2_de.png +++ b/screenshots/de/features.licenses.impl.list_DependencyLicensesListView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1527be020a0ed0264152904c495bfb02cdd928d7317f86472555346a5324244f -size 29969 +oid sha256:9fb2f05e2eea4d25a5bb87f3fc6391df2bf0923967d6ad973152557517fa7819 +size 29864 diff --git a/screenshots/de/features.licenses.impl.list_DependencyLicensesListView_Day_3_de.png b/screenshots/de/features.licenses.impl.list_DependencyLicensesListView_Day_3_de.png index fe8848d2fc0..725423f7ad5 100644 --- a/screenshots/de/features.licenses.impl.list_DependencyLicensesListView_Day_3_de.png +++ b/screenshots/de/features.licenses.impl.list_DependencyLicensesListView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60e1831f0592448fd4c284aab6ec70fbd11e5524c68815cd96a7af81a5e893f4 -size 31169 +oid sha256:c5c32861bbae702e9f45263ed3ee710d450b4eb66f1919a4f11eb5ccce7a0354 +size 31120 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 new file mode 100644 index 00000000000..dfe7bd79eba --- /dev/null +++ b/screenshots/de/features.location.api.internal_StaticMapPlaceholder_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91454ab70e3fd8a412a89ba7dba8d8e9e9009dd024b180fcb78e26c419e0b8e6 +size 440161 diff --git a/screenshots/de/features.location.api.internal_StaticMapPlaceholder_Day_1_de.png b/screenshots/de/features.location.api.internal_StaticMapPlaceholder_Day_1_de.png deleted file mode 100644 index 2f3a69e20ef..00000000000 --- a/screenshots/de/features.location.api.internal_StaticMapPlaceholder_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dadb89a4ae6e91a8d826f3970394dcd418ed59bccac262883948cc8199ef4c76 -size 256758 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_0_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_0_de.png index 1437c84ebf5..b4b2bf2eef1 100644 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_0_de.png +++ b/screenshots/de/features.location.impl.send_SendLocationView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e43851f11f36f86124741e654bac7133eea3675cf9c64d06416eeb39c9a355b -size 20547 +oid sha256:b0ff8f6f5118ddb6a9b5bf972f2f024cc347c8fd7c0fbe4b1b25d49c766c9103 +size 20549 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_1_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_1_de.png index 4f4f3959a0f..fc977b262e5 100644 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_1_de.png +++ b/screenshots/de/features.location.impl.send_SendLocationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f9ef77d59e416ca3d9c073ce0981ca911c21bb7269a5566c3af95a86ae4c90e -size 38533 +oid sha256:cd0e4c0319acaa70e64db1d20a4f6686795e0d25dfd8593dc88addc088eeeb3b +size 38535 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_2_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_2_de.png index 9c361174cbe..c55b053210d 100644 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_2_de.png +++ b/screenshots/de/features.location.impl.send_SendLocationView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:092b98c0f95648164fecf6d084197430ba8e47ad6650be8fc035421af8ad6d12 -size 35093 +oid sha256:3780be557ddc91942003163998553770c07e08cd4617fe013c95c1e2b8de18dc +size 35096 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_3_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_3_de.png index 1437c84ebf5..b4b2bf2eef1 100644 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_3_de.png +++ b/screenshots/de/features.location.impl.send_SendLocationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e43851f11f36f86124741e654bac7133eea3675cf9c64d06416eeb39c9a355b -size 20547 +oid sha256:b0ff8f6f5118ddb6a9b5bf972f2f024cc347c8fd7c0fbe4b1b25d49c766c9103 +size 20549 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_4_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_4_de.png index add5c25a7e6..711a0c27f69 100644 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_4_de.png +++ b/screenshots/de/features.location.impl.send_SendLocationView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7e2b3e52c4032fe3133f1104beacd9944bc0c8b1c7c4921d0dbcd5a01f96e6c +oid sha256:f9a20df5b62f9dc31f1b3e9d3d4753bc1884e1d648932d5088489c88c009fde0 size 20691 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 c3148072be1..dec4dcce709 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:6432c1ddbd16df737087f880c69fbd2c67d82e4b9266d5123a334852d778c837 -size 12264 +oid sha256:e0e968ca4aa0e2df6e2ffee687e1a41da4f89e504863d6948e798a310a04b863 +size 12349 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 49be6dfade7..7bcab8a706b 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:7836807a3231eb484d77057256b26cf83fc3a215fa5655a7b50ffe71bf09c03f -size 33023 +oid sha256:56df4459c4c5a3a875ba07862e60d4a38ef8be73fb6b43a8228b579ffb17f5d5 +size 33108 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 8a393cbc225..4a203057f38 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:2c0075da7c1570ce4205454dae271280d59481325257aff83d447541878595fd -size 29596 +oid sha256:5c392ffaa85b668c496f37e8477d8522085bb389056551aa5657cada56055113 +size 29678 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 c3148072be1..dec4dcce709 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:6432c1ddbd16df737087f880c69fbd2c67d82e4b9266d5123a334852d778c837 -size 12264 +oid sha256:e0e968ca4aa0e2df6e2ffee687e1a41da4f89e504863d6948e798a310a04b863 +size 12349 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 2972e83c097..e1414f1c6a2 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:1a443ecba8faaab0dfc7cf8742a37a8712b2c2a3b2bfc7c5e94925195e0a532b -size 12461 +oid sha256:0bc66e7e3e436d75b078447af2a724fb7db97f72a432266bbf1acd78a9d3a34c +size 12515 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 8a989fd1977..acb766a6671 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:e847a96b43a31df8ad991e2818b0e4c01eb3176d38e01aecde6ba34bbc929e90 -size 16033 +oid sha256:b55954ad65f8b55299d1dc38f518a5f4d27834ba1f893ca210aa53c54a34131d +size 16108 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png index 6bba72037c4..70138eedcda 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb000d2201d558a88024d54f03acb55e3ae55f4feed54021d7db609dadb5c566 -size 24616 +oid sha256:a4d3e2dadc913e7ac1773879670ac49b48a750aca22b337b435c478630e0527f +size 24693 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png index e18815516c1..75582c0b67f 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:723733e42d453d3867580827a9758588cca66fabc73d97a177047b523b0be257 -size 26989 +oid sha256:a4cca367f04eb0ffe302f85eb4cf56db4146518e93bf6a2a264050b0e523bd05 +size 27063 diff --git a/screenshots/de/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_de.png b/screenshots/de/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_de.png index f5840db3aa9..86013cc1b18 100644 --- a/screenshots/de/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_de.png +++ b/screenshots/de/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f648b1f60734adf407091fcad439b65fe683c556e06c78c5fd191e5a2e3456a -size 32825 +oid sha256:21cb68b954f19d773ce54e6be85b4edb7dc286dfb058b202affcb0b501d12a2b +size 33398 diff --git a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_0_de.png b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_0_de.png index eafb88d88b7..88b37ebb3c9 100644 --- a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_0_de.png +++ b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03a6034315859539b4093f3bf5374a1eae24e86b602a6657bb4a45b2652b51c1 -size 34208 +oid sha256:ef1ac61e5c921218a27ef5811c926de59fecda7c8db4be1dca9aee7bf73c6f44 +size 33754 diff --git a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_1_de.png b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_1_de.png index 7228641d692..8a261e33208 100644 --- a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_1_de.png +++ b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a00130381d68236eac2efed3cd59acd483a03fe5e8127f12726264edc2ad1fd3 -size 33804 +oid sha256:99905c6fdf1d85d5af348176c85bf143f226e3d61f09f96d0a199679d338dbc7 +size 33352 diff --git a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_2_de.png b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_2_de.png index 1e1ac875c76..533bb69adce 100644 --- a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_2_de.png +++ b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dfda9ef44de57d954cdf66c044223b42617ca273e97c59f86b8e1c06d32e9a3 -size 35487 +oid sha256:fdcacf07d6d8b6c89739354bd1f7b231dae39ce238a6099833dc08b4780e2ee2 +size 35018 diff --git a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_3_de.png b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_3_de.png index 4eb43507105..24f6c63bf7b 100644 --- a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_3_de.png +++ b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4bfec58fd5fead119ffd3b4a45aa29a5dea96bf36603265715abc59097dd0ec -size 28112 +oid sha256:ace58903f69fef8c157d0ea8f864bbec297ea14aaaa090be1e75e1bf4aa58ea0 +size 28125 diff --git a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_4_de.png b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_4_de.png index d82b8343334..df1d692960b 100644 --- a/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_4_de.png +++ b/screenshots/de/features.lockscreen.impl.setup.pin_SetupPinView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9a365e39d8d17a24efac2f4d346059c2233acb6a9831a1de1ea38a58407900a -size 32780 +oid sha256:ae21ad8437c46eef19986104ccc950bb04dad44b6f22b95739932ea7c03a8772 +size 33104 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_de.png index 5b95cd87ab3..56aefe6c4e6 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7575ceb261f5512f26909d1ef0dc2c1a6aceffb0ebbf45b6b66707d463dc0c34 -size 22478 +oid sha256:638a472779c1c5db8cd668c8a7b73374d5c2b182f6396bb0287b0c72ecfa86df +size 21995 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_de.png index 2bfe489728e..c11765e3017 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:192dccb6ff1889806bd26120c46a3886f7bcddd95cfe9e1572cde61c6e58184e -size 22102 +oid sha256:aadb48568349f7b4d500fc47ca5ab3cac0e1742c9a2c1b43b51f77a2750b77c6 +size 21616 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_de.png index 4ed5700780c..666330431d7 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29713522c949ea2388872ef7f52a6eaf601917a8d31a8c5f025faa76b94d4e7b -size 23454 +oid sha256:152deb117714059b4d7140f0e9b2b07bf1b632f5bcae34b25ec3dda8d82dfc9a +size 23604 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 476c638df4c..017224d6fcd 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:c1a4bc4e54eb9b8eaeae741094f000fd7a669d678a9ee156c2d084de7ad97076 -size 37322 +oid sha256:86f1e725b3142ba5ad5fdbe7bb136b4ec34cf9d1c3ce8f6dac92c9590b521f6b +size 38246 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_de.png index 14fe0e42dc1..2c19d79818a 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1637bc5e0c0ffbe8922038c85d5429e11872bb2cb8a99851086eaaf879818a00 -size 19859 +oid sha256:3e5740e9892cbf9bf28fa49d75825c7559dcfca7bceda7766242cbc37afc8a0d +size 19334 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 075a8fae30d..52cd0908cbc 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:c916779a8943f73a66591c823f5fe5485e793d382533c4621d887a588adaadff -size 34056 +oid sha256:126a58f5b49fd171b25d0f734183a3fb11767e63156d9de91b31d49a69f3d7c8 +size 35055 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 ba58d67884b..e5f57d529e8 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:f49e95baea7be19ba33d0139d602b1e9359cba9c70a919b418e61303e7b4337f -size 23401 +oid sha256:e1ad29d369b685fca84f7f3ab30cee1dfbb45c90112eaaaa3be71cdd98d13f74 +size 23073 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_de.png index 43fafdc1ada..4dd8a7ccec5 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21e2db6659a955ba726e848cfb743d5004908c73c3bdfc412722e99d95877136 -size 25006 +oid sha256:582e7c3c8832c71c035680d6e1e8b7a4491e752af572a9b5b5f83f03a894016c +size 24694 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_0_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_0_de.png index f9fc739c0e6..239643f9293 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_0_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3efeb0cfc04da7f73fa13d54676d1f3d1fca8a59c5d8ce72dc6d90fb4158320 -size 38200 +oid sha256:17429dd340f2acc19b924fa4637de7ba5ff7e02b19a37227f9b9dedf90a156d7 +size 37703 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_1_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_1_de.png index 90c3e24f01b..3eaf8765426 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_1_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd8eaca3791a645caef6283d074b3e57dc2054158c60eb60c15c2bf6dc3aea8d -size 38594 +oid sha256:8533efa18842ebaa91dbbc17b1f5f7d3abc30348c851104ad7ee26ee34a88685 +size 38091 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_2_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_2_de.png index f8ba109911d..7537132f403 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_2_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0626ba6f85dafa09da2d8df68086593aab5c6af87be82f0a38564ae5c2e2a02 -size 39176 +oid sha256:fdd6f6a7f9b880efaf632f8f2e96e94d7e35568059645f0e2cdb0fb4a8f0894f +size 39320 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 61a4369b5a5..03b949a7ab5 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:247896eb3c2e422adb2b8b90b3cf30835223e96b1962d7e1a2fad019a1fa766e -size 44123 +oid sha256:5cf69aa0aa4f04cd19454f100f09387977d5704aff611f5efad171f43df35514 +size 45047 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_4_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_4_de.png index 03b38ba7c73..0c068f8b393 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_4_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36fcf13ebb0e51451360cc4d4618bdba0b32439438dafc7bfdbedd61c8af5a6f -size 35586 +oid sha256:0b7006d31bd4e7bcf2341d6f75588bcc67cd71066805e1cd438d0438ffbdedf8 +size 35084 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 be5af02b1d2..595d5132017 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:ff84a90f74c1d77b0c6beba7b4fb944553b101edb6cbed2e151b31f950488379 -size 41007 +oid sha256:2780f174c627ba4db56116de788f7ddcfcd4d54032eb64a480813b593ee53499 +size 42000 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 66e36e0135f..47e6cef2dd4 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:d27eba0be4467be8bc779e3786d826a46d1561d7525cf9be033534cf2f5c6b84 -size 31799 +oid sha256:1fcc0eef9284384f7cf5308d8dc029951e1b5b76e036cb1606cb8e77918e7c7a +size 31448 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_7_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_7_de.png index a9d2fe1aa69..cdd24d6dc09 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_7_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89392a879c0ff72d2c33d6a072b025ff645b13a980f93bcf81fca0c092dd81ee -size 32181 +oid sha256:8820f3eb78e9a96ca92dc2628645ac1eb3608323fdf09eaf21eeb13a4b08afc1 +size 31841 diff --git a/screenshots/de/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_de.png b/screenshots/de/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_de.png new file mode 100644 index 00000000000..be38e64a2d0 --- /dev/null +++ b/screenshots/de/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf1d3c46c9e8d5e68052bbc741dc987f6b005dfc5bba8eb80976895a790d953e +size 6856 diff --git a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_2_de.png b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_2_de.png index 18a152781a0..c7b29375873 100644 --- a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_2_de.png +++ b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0dd57c3fdee7f70ae16e87c08d429263357c69d7d2258f54979745480873319 -size 20995 +oid sha256:018cedfb6449364a9b01acbdaf9320e00092accb4e2f35c5d0480cfbcc07e2c3 +size 31804 diff --git a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_3_de.png b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_3_de.png new file mode 100644 index 00000000000..bfdff4d875f --- /dev/null +++ b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8111853ec8798d779d367564b87b8002939d592d8b91e7c49acb33411e0079e +size 16935 diff --git a/screenshots/de/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_de.png b/screenshots/de/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_de.png index 18a152781a0..c7b29375873 100644 --- a/screenshots/de/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_de.png +++ b/screenshots/de/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0dd57c3fdee7f70ae16e87c08d429263357c69d7d2258f54979745480873319 -size 20995 +oid sha256:018cedfb6449364a9b01acbdaf9320e00092accb4e2f35c5d0480cfbcc07e2c3 +size 31804 diff --git a/screenshots/de/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_de.png b/screenshots/de/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_de.png index 58a3e890e2b..de8230291f6 100644 --- a/screenshots/de/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_de.png +++ b/screenshots/de/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9323e22208ceb9cd4192dd9e4ea592437a850bc0600e07ea46e75668242b35dd -size 55628 +oid sha256:791f83c78afb4cd33f82c9fa7480b9d986ca38fdae7140e0ab66d191023e6ce4 +size 55516 diff --git a/screenshots/de/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_de.png b/screenshots/de/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_de.png new file mode 100644 index 00000000000..a30ec546815 --- /dev/null +++ b/screenshots/de/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65711d9881cd4a62e7cb9b0c8f4624480adf42cd79d9207e76c2c64212a164b5 +size 52641 diff --git a/screenshots/de/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_de.png b/screenshots/de/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_de.png new file mode 100644 index 00000000000..9f8da6438e4 --- /dev/null +++ b/screenshots/de/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b09b12dfa2cfbd159197cabd487c85cd7c3cf6905323f4b12b741181a69485a3 +size 22359 diff --git a/screenshots/de/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_de.png b/screenshots/de/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_de.png new file mode 100644 index 00000000000..78fef3d846d --- /dev/null +++ b/screenshots/de/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38679151a42d89b6b414be876a9801ac3ad024dde1f8c0a5b7d8dfb158c609f6 +size 23275 diff --git a/screenshots/de/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_de.png b/screenshots/de/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_de.png new file mode 100644 index 00000000000..b04bbdedbac --- /dev/null +++ b/screenshots/de/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:165fdefa29f1cc198be80d2badf174b4d355a3e10260440409052e06c74a4733 +size 23878 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 0633aede530..08907f39cae 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:7a669a39d299b3edbcb692c13b3a4840fea376991a01b0bcb6080118c76a3c35 -size 38787 +oid sha256:3cebf81ebd8a9a4d0576b20d56dc83c393c72e89f4a9bed0cf6e79d9c7c57494 +size 39562 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 535c9d9837c..371127dc500 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:c53566dcadaec2c0503afb1e85cb777bce14cc63a28a188bef4383fa7661657c -size 39660 +oid sha256:2a282b7412ac64e5b209f00d6a70b2da1d9b717a973003f679dc37ed9694e348 +size 40411 diff --git a/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_de.png b/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_de.png index e27589dd0e6..23e64810c4a 100644 --- a/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_de.png +++ b/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f457defabbd0c06f6f9880bbf9ef9130a4d462fd482f1acb455ff437a51ddd4d -size 41009 +oid sha256:784f55a9e6780fffa237a1e55f3033ee0a59f4c1066e385a81f075cac58172a0 +size 41609 diff --git a/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_de.png b/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_de.png index 1fb877aa532..6874f9dba4f 100644 --- a/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_de.png +++ b/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:026975ff90e8ffbf4b6a9e533af783cdcafdaeda8157d87d40b02e15eee2426d -size 38647 +oid sha256:696f9587a2ef0f7d017062dab6348cfddf5e82169673530b10241dced459f728 +size 39341 diff --git a/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_de.png b/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_de.png index 1a1e95605f3..4b1c3e1bfcc 100644 --- a/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_de.png +++ b/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d99b67615d1e2d4068c7a09d034df577d991ce8dbb8df7f64890f90832440d20 -size 39851 +oid sha256:f4a8645df197255a5fa44a7ae582aa1d1126a5d00f4db56777b26e7b7321c5a8 +size 40523 diff --git a/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_de.png b/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_de.png index 16126eed3fc..c3b56e79788 100644 --- a/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_de.png +++ b/screenshots/de/features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eec1825b09d39388fbc9a464042e8deab577c69671b6531fc62de8b863eb164 -size 30014 +oid sha256:3ccf8db965c240d06ade404cca64607ebd7392660f091c0a71c875e937045222 +size 30675 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_0_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_0_de.png new file mode 100644 index 00000000000..99407b4dc59 --- /dev/null +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dfbe8da67e61d4681d24f119ba1d9e9d72582d2fb08f5da02b0e885866f800c +size 314617 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_1_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_1_de.png new file mode 100644 index 00000000000..08b5217d105 --- /dev/null +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8beef56d2ec9c0939e594a20dae2aa9d4910fbd0398a71e2fe3798041f2c2508 +size 310229 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_2_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_2_de.png new file mode 100644 index 00000000000..50a5fabbb12 --- /dev/null +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc307a1c888ba4238ee113eb62d06303e1a66ba9198bccddcac06a348f8294d5 +size 313215 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_3_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_3_de.png new file mode 100644 index 00000000000..7717deabdad --- /dev/null +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b619aa43723f2b0acefe6a8b06ad0b1c32bcab142dbdd7f2a6df89036b92323 +size 307425 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_4_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_4_de.png new file mode 100644 index 00000000000..4261a03f763 --- /dev/null +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb661c897842a0eedb95e2460205eafe34038eca930a6096a6efdd3125cdbca6 +size 307939 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_5_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_5_de.png new file mode 100644 index 00000000000..2a81bcf949f --- /dev/null +++ b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b48da126979d45bb8ee9cf1dfba84e6db4069ff81f1a8f60a8cda76a4669a8ad +size 317509 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 18beb3318a2..0f192907ae0 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:4a3afcaf358fca137b8022b2a9d741b650b1185237719766418bfb8a6ed2545d -size 38283 +oid sha256:9e14cd38e873e0dc15fcd63d27a6a5616b784ad66ac19586ccb18dfe5e090689 +size 40013 diff --git a/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_de.png b/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_de.png index b0d5842e2c2..d286dfcb243 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a53972223734714a6e1826343bf268aeedd5034daab1ae58a01024228c0b762 -size 35877 +oid sha256:421c5e4c22c85409658bba9981cd37a8bc7f31a389fa48893206f84be8df6c2b +size 35896 diff --git a/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_de.png b/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_de.png index cc64c38fb28..68b280e002c 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:270d3b0408d54fcddc07bec886758d5a914a89aa77bbf55420d20ff475ed4a94 -size 37696 +oid sha256:c37c1baf52fed09e29a08174d160aea56fee38297dd54fe390779b38f7ed0e76 +size 37727 diff --git a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_de.png b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_de.png index d1858d54839..9341b9cad14 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fedb2026fa21c863f5d1faf84e9928ddc0b5dc209d779e9972b913ad1f0b7b35 -size 24888 +oid sha256:722e5d349ced53f20ba10ecad28783f4f81ae79ae8b7af241e63cb3d8a587f24 +size 24899 diff --git a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_de.png b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_de.png index bf9fc73bf48..953b0ee025e 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c242f632e22731adbb94d346318537cb1b78d11ae8d47b1feab77d6e505aeb0 -size 21945 +oid sha256:94e58071466c9adb0f526d24d8f26868020ad7f24bea2712ae7399fd3e7d0a7d +size 21960 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 049b23ac7af..53858732649 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:24f8bb681f32f95714485411e9f3941bd86294753fd308464e1db93903fd1ef3 -size 30023 +oid sha256:4e54d5003578a3805e4bdebb59dfea85ad2adaba277c13aa9456eee47763e6b9 +size 30040 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 33a4efe845d..17b94bd272b 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:602bee5b72f820db4bb4fcdae602a6c28709a7d82099d94e7b1836313202e287 -size 38593 +oid sha256:dc8c81b8e42a4bcee2f113ee686c423d2aa2b9f9700234ba0990ec038211c216 +size 38610 diff --git a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_de.png b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_de.png index 158db552e0b..fa8511a02ca 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d2f3cb47d6ea798db5661674a9dddcf1289de1263cb88a7007a9653939be5dc -size 69240 +oid sha256:94ac6ab3ae4e6f7061477d3766c96b9bfcdd701d054ca5797f9d3536d22ad7e7 +size 69257 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 beaa2960e4d..a4c239ac54f 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:6912024a1a3eb451a319e1dfcb7e4ec59480686ab131fa806792e081d43886b6 -size 21342 +oid sha256:0c0bbda9f47701130a97bae5afb38049d27334500bb1aae888d65fdc8e7607d5 +size 21347 diff --git a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_de.png b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_de.png index 105a8b3801d..a0e9e5b36c3 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ebcf0c4b8c1106e845bb20fd5339677b25b102071ef7e59d6adad7c58feb3dd -size 24067 +oid sha256:9d0955a11bfbfb8be3178c766efc1733fdf1e306a6cf1a222d97703453cc8b99 +size 24084 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 641499630ad..d47a26620d4 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:54767f370bf50ae7c8f6d0cc27943b2832777e69d97fce3a3fe87ba4a54144a7 -size 58506 +oid sha256:de8729263671180d22c79d922add85b71d24082c66d7dc1df7638bd38d34d56d +size 58536 diff --git a/screenshots/de/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_de.png b/screenshots/de/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_de.png index 8a2b264509c..09e8c7b0e40 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b41ca9a70cf6c5e646ff444070979ec9fcc573a9b8e5d03f0f7c07e6b1819fc -size 50392 +oid sha256:94c7b1e0a1b12767c51845ac3173b9b15b07e5cdc81921bd85f2588a7c48f372 +size 50420 diff --git a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_de.png b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_de.png index d0f22584b92..1acc22f4aab 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4b37670cdf9eecc590f1704d52a3931495b65ed9e772c660ff5ed98d1b0aaad -size 14641 +oid sha256:1551c365ef7522b0ee850f2064585b86c047cce8a28337739d09e71b2458852a +size 14502 diff --git a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_de.png b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_de.png index effb283dbe1..f3cccea8c7a 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8879b0a9bb6c80576b7bf76611d3ef25ad257c5908ef20fde617146e0cb48cfa -size 19417 +oid sha256:7c18bb43dea68bded70fe2fe21f2f14140b49106ccbbf988c06506a84e1aa6ba +size 19265 diff --git a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_de.png b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_de.png index 14722275acf..2abaefddfb5 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e9c1049c6a1f831a7985775a04041a53c3366435a9aa158c0a85c090a7539e9 -size 30605 +oid sha256:25b5b715c1fc4cc67152c6b29e90fbf718eea81c1b75550292a5a4297d8a9449 +size 30479 diff --git a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_de.png b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_de.png index a5892562f4d..65d6ddcfa65 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cafe758554749db17bb9f5a73054af1c1f7164f4472d69184995b120a8695c89 -size 39187 +oid sha256:2ef41a8c3b3e672299f77eb236f2dd335d26399bb58842788813b90824d6860a +size 39081 diff --git a/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_de.png b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_de.png new file mode 100644 index 00000000000..b2d0ff95901 --- /dev/null +++ b/screenshots/de/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22df7ef05574f86e7090b91fa2b74103a778aed7029f262f57d54e9d6a886647 +size 34350 diff --git a/screenshots/de/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_de.png b/screenshots/de/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_de.png index 73bacb7e93d..caec7cbaf66 100644 --- a/screenshots/de/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_de.png +++ b/screenshots/de/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5c92dca182754b6a3622c6eb7198d6829d6a696bbb29c8e5745b818e5a7050d -size 60492 +oid sha256:21d17c76d225999c8358d4a22b17f137b8dc9a5536dda9299eae43f36d41fd4e +size 59773 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 d0eb6fc40f7..3b4a617600b 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:941c0bd316c55ce06eca86a109326acd7976f9c9b1c64ddac1673a77c8ff9764 -size 20601 +oid sha256:0fbcdc832a671f2e55b39b4c6d14860e320c9dd5e5f0087c6ac58832424d21d5 +size 19314 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 96b5954e8c3..90b11b4f58f 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:53ff3da2a5496f8df3166ad21e8bf65a9d5d25e408ca7c45c11a65e1bc25b08a -size 89302 +oid sha256:4bc16e4e009ae97d00af6f4be052d8cdfe8386ba4ab4a21514f678790efacb54 +size 88891 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 cd650136f55..77419df88fb 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:1dd9b479ba45c82fce5d0dc80088232675671200f05131f39588aff1941c1341 -size 88983 +oid sha256:64acf68b27f73fd5e1cf6e597d45e01a3dec2310b2fcd95763e3cfecd95a8a9c +size 88583 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 1c2e80220ef..aca5c3130df 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:c5c2c6aedd65bad1d80e33a6a2bc426860dc1eb29d52672412845dc8a8beb5e7 -size 82042 +oid sha256:bcbc25ce921a6af6472554a224ba954b362aa44ff90bbdaf8d63abef9ea6b97a +size 81406 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 277f207da88..2f573dc6c53 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:6f66e99b3632618df51d43688d57ebe152ab91058f4db52e9f33060c0e6b638b -size 69452 +oid sha256:44a343cd67650e5ae933e61585d72ac0ce9ce31c9e6e7e8e2928ed4971f978fe +size 69612 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 33216b46a70..f5fe67ab9e8 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:5b5704a90cf715b612f1e2e99a415e2e9c486bf70de8b8d66e7dc22625fdc82c -size 62038 +oid sha256:3077ab213a99896eae4d603cb794d67af213d8020def029beed780d5a3cb9e3c +size 61811 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 22ea1cedb1d..6045a6eb1e6 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:e79d4447f2948ddbb46c9f1739232e53c0aa8706cff54d10ef1682a7ce267986 -size 11524 +oid sha256:f4d04d8e6efd372b7d0539f009febab926b1ff34aff132ba4f3ef7b12795c5e6 +size 11534 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png new file mode 100644 index 00000000000..ec0dc76659c --- /dev/null +++ b/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26a9e3186147de9067b74b6bce4381a5ac5104e96176a7ba8aaa532811b0d9e2 +size 31565 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png new file mode 100644 index 00000000000..be526c4fa75 --- /dev/null +++ b/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc234d4ad6e3bc60da2716334e00045be60e277e3cef077b4cdc70ef1b193f0f +size 37188 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 3bbe07db7fe..0e27dd3f680 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:d546061b1dfb205c0eb518e3d1702ede3bf1ac0bbcfc56db93a96d11ff7b353c -size 47869 +oid sha256:260f4c11c53e15524747a40df3d828ba92c6273758da13e6f4d6f63ce000ea5d +size 47947 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 fc1c30de0a3..2b1bcf46fa3 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:94d800b2f29b78f72a5eac0f7c4c9372d3a229c3c8364db247e7a327836ad686 -size 32474 +oid sha256:4d4a3c53907ff8e38894c173db674369b5f3910431a48e65e5ed94f271feced9 +size 33181 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 3bbe07db7fe..0e27dd3f680 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:d546061b1dfb205c0eb518e3d1702ede3bf1ac0bbcfc56db93a96d11ff7b353c -size 47869 +oid sha256:260f4c11c53e15524747a40df3d828ba92c6273758da13e6f4d6f63ce000ea5d +size 47947 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 8ffa49837e7..1c7f3f566a3 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:29b7fdcb5cda2618bf8b138b6b35870aa4fc2163567e0fdbdca31b244084e2dd -size 27038 +oid sha256:89431cdede17d672f5c6b5f4218c006a1392b1e2f9faaecc368ed99ad87b55b9 +size 25878 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 5105b1166c0..9163d50c86b 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:df0915e35366e6c29f3538017efaabf1a9ccad26ab21f7c5c72d42ff3eb9b42a -size 16966 +oid sha256:edfc727732ca9fafcbef1c65ba633c5d5ea76951b504946a2a4e6fdab1b23dc1 +size 16952 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 c1f3456be93..60986115cd8 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:8d41d9746e0196367206af4a5a5a5e48038a6e9d5e66d9c570d015cce6e544dc -size 27478 +oid sha256:761f9d37116e247c6279e2fa7300ff523180d6c50b190f35872efb939fc24f56 +size 27468 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 a8918b85a16..18fa34fee60 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:980ec6afc37a4efaade2163dad305a3d177d59326fd87f6a5450b40bb83c7771 -size 43980 +oid sha256:647c94e0029ec0a98b9daae7a890281561a401bf66c9ead776afe852fe731b07 +size 44024 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 03246a75c51..611026e8c66 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:7f0cf79efad08fabc276daed192022be2038162c8b496c40a4d69fcfcfdb5974 -size 42438 +oid sha256:84721d8b72792d917253a09d94e704268c9f83f4a2d538d282f109227e72c5af +size 42250 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 f101107b5a0..2c482f08e52 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:ccd255518e394585c0a936486747cb311c53af1b1374b39d57ebaf3e2b128d36 -size 40258 +oid sha256:c0eea7215dddad22a8f1244ccef589625e26df3f97f9f68c00c3d8823b084913 +size 42019 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png index a42d6591126..3cae6a2dd7e 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a28abbf58a0ab094c4620bfb7175497a3154501c1d5eafa374ed63325143b72 -size 35015 +oid sha256:4f6f9840e4c7d5ffec60904a380bc0686eb9b4b65a52c6d5ff15742f336deca1 +size 34987 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png index 2436bb53f10..d58b9fd0f68 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f59b02eb5ffbc1906c85fa6b69c633f999901a6849d8299768080c703ee49b56 -size 54214 +oid sha256:bd18fcf4780fa4a1af25023c60aa24fe14754fdfa45786bc59842cc5f90f96fb +size 54250 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png index 0c594a2f7f4..627e53bdbcd 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3d59e5ebd66103bb795ded6f0218af2c8c9476216e0d67e84acf7dad6b6e7bf -size 54301 +oid sha256:5b2dd0277d3168107fbc66f13a83b8b1775e949916f995eab620982fdbc28f8d +size 54850 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png index 216d9442395..563eb1bbfed 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44de4119ef8f9904ea23a49b4e1152f299a2bc797eeb95865b0866a1ab0ffd49 -size 46308 +oid sha256:9f51ed97c6dd2f2200724d107580c488b37393eb7a7e47c73dcfea57f2af3f19 +size 46312 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png index 7bb012259f3..9873251bbe5 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56270914ab2221a0d52bd8d520ec84f1cb6fa72222897df87f566e4f0209191a -size 50543 +oid sha256:3a9bd8ac8760c1a42e864bdf53d6e14a75e3c2e3f40d234299c7beffbc8f5d65 +size 50547 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png index f2602fbfec6..b66d4d9fcc2 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fc7cf8fd8cfe783637279f59568586012775d34a499add80de74456f9ba89a0 -size 48975 +oid sha256:c39e96bc859a653ec9c0e8111812af791f810e54e125f537eba4bf302986d040 +size 48978 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png index c0cb4be6689..19dba560691 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4310f6a632bb3e19b7c1725538e90455af137e66859019a38ebe39e1a643f92c -size 44249 +oid sha256:e3d9a9b2524a8d9a558b0599d0c2a04bcf2185818e5ce2f1d31cd6e54132cafb +size 44305 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png index a4621f10cb7..56a51be48e0 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db60d232df5f085feab1aa3fb3970baa8034a69a4420c1dd75ed84f425ab9701 -size 49243 +oid sha256:97e8757d1843852f843627ca84f454761c6313b989b34f3526bb7c7e77ecd895 +size 49249 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png index 18fa49aa83a..43f18f85c54 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cc39e72d80318b907749f103107f76bd7a8944ba446c92e9acb0cfb5b1566fd -size 45424 +oid sha256:db596bcdbdf5f88f327f553df085f3f241564b640b6b2697293779de9fd492c0 +size 45471 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png index 20ddbb93c73..0d8aed3fd43 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d364bb84497288d2d863c8653b54b640bb3169809264801b73d7454f2e19b2d6 -size 48473 +oid sha256:ae7924d514d862187cd092850659fb5352dae0f2d102936dec2472f86360b11d +size 48479 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png index 0d6a7dc0317..204376c0513 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6adf449492b72e3054b3ea7d7a432b92ee5086e060884366fc2953e3a1957f43 -size 38097 +oid sha256:66cab8345e9abedcf15435ce8713a4acb2e08e9a0f20754a9ea65c4fad7111e1 +size 38096 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_0_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_0_de.png index 8988c0e9e29..d9a7c9b57c1 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_0_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ccd9d75d94f50b0deba10aa5cce3a8053e22f125b02b841f08a5b97f98f4d47 -size 396664 +oid sha256:6844f7713a4b428a0a1ccfef5980721dc3a561d6921168134e0231cb4b8a7391 +size 396487 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_1_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_1_de.png deleted file mode 100644 index 8988c0e9e29..00000000000 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9ccd9d75d94f50b0deba10aa5cce3a8053e22f125b02b841f08a5b97f98f4d47 -size 396664 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_2_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_2_de.png index c1c8843eec4..9db17042ddc 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_2_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe2284acf2a5d906181e44d06cd13d5fb8dae51396f348f8484528e75bf00fd1 -size 54684 +oid sha256:1397fed8a9781b1f0075fabae221d3bff827fc25aa3b918919202be2a2650d66 +size 54541 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_3_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_3_de.png index 8988c0e9e29..d9a7c9b57c1 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_3_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ccd9d75d94f50b0deba10aa5cce3a8053e22f125b02b841f08a5b97f98f4d47 -size 396664 +oid sha256:6844f7713a4b428a0a1ccfef5980721dc3a561d6921168134e0231cb4b8a7391 +size 396487 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_4_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_4_de.png index 0dba7fd3a3f..a64ef3cb97e 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_4_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:694ca900f166ac3a23d87e60020211e8b98c02c16afece44364949ff1c19c149 -size 54650 +oid sha256:b7cf927b76093686e123f7f8e45b8ba17f58e15f58bdc1999cdc1a7c2d1f0af6 +size 54509 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_5_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_5_de.png index c53014b5f44..18148b3dd91 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_5_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70e9eeca276f65583b36611aa3b752cac5da5328762b8007878eab9e59301e42 -size 55838 +oid sha256:e9bde8eb56bf3df43bc4f58122db1b4229736d9949878e8d1162e9b50bf8314c +size 55693 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_7_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_7_de.png index 8988c0e9e29..d9a7c9b57c1 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_7_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ccd9d75d94f50b0deba10aa5cce3a8053e22f125b02b841f08a5b97f98f4d47 -size 396664 +oid sha256:6844f7713a4b428a0a1ccfef5980721dc3a561d6921168134e0231cb4b8a7391 +size 396487 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 39555bdd05b..f006ac08d3c 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:eb6855006f988e1a37a61067c1cb3525ec3e9b9b387cafd3d0253dd779923adc -size 25949 +oid sha256:a2a9e4c1bf333604bb0a1b17c83af340e7d17a4dd3c465e8a49394dd236c3b45 +size 25844 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 98699796a98..8a2a525f26d 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:09c8f4cc22e3dab09d99370628243573c19f8b2a12f27dd7ea2b85e5b1187730 -size 25938 +oid sha256:59eb255368d39bf029237c84977c84a616994193b5c1d1ed43db403bb63e289b +size 29959 diff --git a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_de.png b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_de.png index 29851f55314..05cd75b735f 100644 --- a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_de.png +++ b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af210770ea4a7010b728b531ec60df390b628148c320c703ae79e8911e3bd58e -size 54968 +oid sha256:37d52ac68f524755b1245b8bfddc137e9ae609b2096f958b9920d2f1c6bebba2 +size 54958 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 8804f2db298..4b9d2d095eb 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:e3663eb44bf7a2b3283c9fd5ac25ed0f559941defdb499b18952c9d3cd534f30 -size 65854 +oid sha256:0733dabade5399a419fa04c553a719c32c4925826d34091f7a64a0b4eb929e23 +size 65637 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 3c79f992897..ab80c733558 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:758189b90b2fa9114cc46ba6acc91434b8d7b351b24ee0e9dcd2ecee029fbc1b -size 65914 +oid sha256:927c67c23735b8eaae8873aaf8e62e0678f9a5418fbfcabf8ee436ee5785a647 +size 67317 diff --git a/screenshots/de/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_de.png b/screenshots/de/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_de.png index 5e223eefb85..eb9c71a7247 100644 --- a/screenshots/de/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1655a4fe1d8d6be2762265e2c183716b4f181abd0ff8a69d121ba47e498ed95 -size 63422 +oid sha256:439fcd91d80e7621cb4ab67596d37349f8de9ef40f2362933eaeb4de8e50d473 +size 65849 diff --git a/screenshots/de/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_de.png b/screenshots/de/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_de.png index 5dd455a3135..4fd955ab0e3 100644 --- a/screenshots/de/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_de.png +++ b/screenshots/de/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfed5e9b301307563d609fe7fd634068ef3a452eb08d5fb055e438aa59925e88 -size 66988 +oid sha256:52a994b5aebddd00efeeca7d0add567445cbf881cd19d13f7c608645fe0d4b86 +size 65834 diff --git a/screenshots/de/features.messages.impl.link_LinkView_Day_1_de.png b/screenshots/de/features.messages.impl.link_LinkView_Day_1_de.png new file mode 100644 index 00000000000..ac0cb24cc0e --- /dev/null +++ b/screenshots/de/features.messages.impl.link_LinkView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd7ba46af8c9846828dcd4711915044038201591853c714ed707266e51f19a92 +size 33770 diff --git a/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png b/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png index 762d8261bb1..8c05ba9cdcc 100644 --- a/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b420f57f6ac010b9e3cfd3f3d5bff3dfd50474a1dfe18fa295f066273d7b2dae -size 21317 +oid sha256:982e1555cf3dc1a9e954ad5894ff695612daa54352125f905b033e462787df9a +size 21314 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 822d653ffd2..0a8e9ef0884 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:3c7acade4c33474cdd20013b73facaa02e437db636808e6748038fc774109c7a -size 26510 +oid sha256:5f6c1d589f99a2f5722253f0e868eda2c60c464955d29828a3e8bca0b3db1a20 +size 26566 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 1155be2a8ef..6643ce9eff4 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:3511403903945359efbd0ac6e8ca83d0f88577674283b0f11ef8aa1ccc0c28d9 -size 15348 +oid sha256:e4e58b90fc0168991058340c2e29f3a2146c57598ea471aa7ba0b68b950c5574 +size 18507 diff --git a/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_de.png b/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_de.png index 234d12ae601..3afb335f1bb 100644 --- a/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9025883082e794b6ab7a22e222abb9f001505f6454352474025a68b7ddfe3386 -size 42697 +oid sha256:f08c84f2f4e4334e1e3062f3971e2e9ef0211a9af6c76de7ec90f34a470d0119 +size 42777 diff --git a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_0_de.png b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_0_de.png index 05fea4f534a..cadfe7fde5b 100644 --- a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be6070a89ee999b288241cd481e72aabf6a492f619ec593bc2d8d74b0a85f8f8 -size 48708 +oid sha256:ee0e18147ee167f32a39fa7dc04ff121982e920a3c8e1a754bbae5d789128d33 +size 48962 diff --git a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_1_de.png b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_1_de.png index 2df8f5eba23..b39f00cdd41 100644 --- a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eefea14c2a77f0230b4dd9b112ce73d6aa6a79d712e0cb357cf472c2882ca25a -size 49712 +oid sha256:bab8b0cde9ef83f61261f25f8174b6a3129c3a542590e1bc74a056521082a014 +size 49952 diff --git a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_2_de.png b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_2_de.png index ec231d6d89a..99fc4fe6b97 100644 --- a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_2_de.png +++ b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53e5b70eff1acd9e76c522b5a2f6b6a8eb95e918c43b7dac912c3a57409a3745 -size 49265 +oid sha256:3075a505cd110ab7657830878dea94f4fba92261fb492685bdd5ee37686252dc +size 49458 diff --git a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_3_de.png b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_3_de.png index fa22bc328d7..692de9f3a2c 100644 --- a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:251236d638c52f3c35c32f42f1f8bef6ec761079cec094e6ac71f42b49968d4d -size 48846 +oid sha256:623e8bfd20bddcd57b8b24d2077b3c3b8f7d6ac393b02c89ed0fe7af7970543a +size 49046 diff --git a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_4_de.png b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_4_de.png index c2dce2be222..e8a2ae3d597 100644 --- a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_4_de.png +++ b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aee180d2e0a75df51888686ca72af1c98073926b82cec2cba4c056887ed04773 -size 28871 +oid sha256:7740aa5ce68ea8f5408e9463bd367a985d91570bbef40829eb7b15bab3f14d47 +size 28865 diff --git a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_5_de.png b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_5_de.png index ec231d6d89a..99fc4fe6b97 100644 --- a/screenshots/de/features.messages.impl.report_ReportMessageView_Day_5_de.png +++ b/screenshots/de/features.messages.impl.report_ReportMessageView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53e5b70eff1acd9e76c522b5a2f6b6a8eb95e918c43b7dac912c3a57409a3745 -size 49265 +oid sha256:3075a505cd110ab7657830878dea94f4fba92261fb492685bdd5ee37686252dc +size 49458 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_de.png index 9b73be525c1..3210c0c98f5 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:665225778f53330804639e9725206d54938e20647a3a7b28e88f77bf2ba87f18 -size 11363 +oid sha256:089c2cc89b7606a71c178d330294603b98c754ae95c5c0e1a167ef1450e353ce +size 11620 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 435710b5212..a4f5226235e 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:9da05541fb126929f9e48f4e64db3373979f9535da19f91ef8d9ac911ec9b9f9 -size 13683 +oid sha256:71feb6e54801136ba27235cd4ac059439ae8a53ecc4f21dafa4fabaa054b9457 +size 13673 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_de.png index 5e1081e563d..9095074efc3 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:221a1d06bb60843e032235b6d67afe5db60aff6f842d4b256fac03e54ad36119 +oid sha256:97eb49d7750c53156c35e17a3df7e63f7aa988d82348f0e3365e155cb19b1af1 size 11073 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_de.png index 4c366e8e5e5..e595b4ee2c4 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed80cafdbc8c53fd8c031818fd4df8d7a63f85cc896d1262301d77bfb0ea9c6f -size 14415 +oid sha256:dffd8fbb09aeb23b8eee2e3b8d670c0ef17c2972aa6d6f176e7567ce90dcb037 +size 14425 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_de.png index a9014ce7c2f..89843596f74 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69f2dbffeeaa9cdacaf8c0a6df9890eff764ed623fe7b40784b6f939b7ec6933 -size 18245 +oid sha256:6ccd7ec64520729366305c2daf46056027757bdf9b581873fd6e1c765d27c721 +size 19784 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 225fbedca2b..f55ae6b423c 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:c377c4db1993dc4d81c4726e3cd4d5e50a5ce4e953132f630d5ad3b34c2b34d3 -size 24991 +oid sha256:8136b3009216ac85839cec2cba2156dc5edb8b7557f75677793234a0ce7be34e +size 24994 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_de.png index 1793f2cd28f..092e7b61612 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9fa624de81147b6cc30b329c776fcd1230ab2049283ccf5190cf30bd95b2a29 -size 11154 +oid sha256:832b41afeabe29ea4e9c699806cd9c1cf79a1ede47ad6ea8140cdae5e4602ff0 +size 11299 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_de.png index 99d11a266f6..e591e216c63 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dec121edb0e8e1aebae588f7d618b191849d9ad17fcef07120f978e0322d6a46 -size 17522 +oid sha256:b8a373459c04470f1febd57120fc5c14a7de39aeada3d96dffbacc9ef91ad301 +size 17525 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_de.png index ad8e799df9c..3995a91a26e 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bc45f2776c67e0c81f3369286b6894b0fa4044008076a26b096d79f060d6703 -size 48347 +oid sha256:da326c358b6e63c56997c91331fcb577194625d146c1f19e3140126bc87c97c4 +size 48356 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_de.png index e1aee96a8a6..7cfd51b4b76 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3fe7075b82acf3855ea43564a8be7c9d6932d9745f4cb730132054f1697b240 -size 51038 +oid sha256:8e806543ceb3c25c58562785cec74d2f5064d2c94e6ae558811ce0fd76ebfe76 +size 50774 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_de.png index 3904710dc35..a4573547e6e 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:137c7963233622f4297e561f7a22063249d515c11a58d0bcbd15b8bc8a86237f -size 52463 +oid sha256:c579ccb8997881839455946315be561cedf55c2beffc7b4162b6a354792f66a4 +size 52470 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_de.png index 3cc43ffe477..21ec0741cba 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17ece29ae2e57cfe924048f4a4122410141531e03afe4e074d9e17ab1fdc7e91 -size 52820 +oid sha256:38e53aa9cbff5725b93945ecd93d7b35d7e3fccbee9e14acf4732a6dc635a06c +size 52831 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_de.png index 7995611d965..29050424d47 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcba83320bbb4fe947def1834a584dfadb2a5ed7978967068c649e34866f9511 -size 7403 +oid sha256:941c6fde084447131f8af4281079b89698e575948690c5c2ac35064765852ccc +size 7410 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_de.png index 429420e6771..0dc692ba528 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:684412b2969cf913ad5f9b3cd52b9d1496d8e3fc8132b631bafbed95491d9f28 -size 9012 +oid sha256:dbd1750148dbab35e364e87653d209f7d7061dd8c566e9f38f780d02907f6e95 +size 9011 diff --git a/screenshots/de/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_de.png index dbe19c419cf..634b60cfcc9 100644 --- a/screenshots/de/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b43202763e975eb13bac33fab6914faa0565e96c668d515dcec16e88faad5b61 -size 4173 +oid sha256:c14787c550def03ba6823107856be7df3531e3db0578bfded1196fbe1d4c1360 +size 4223 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 d0b692554d2..f579737b444 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:78be9296ea0102e3738bffcf76ba11596f996a71355133846b5611b1c9b9f988 -size 14606 +oid sha256:afba149de7c094ec7c49c094df22e22e5482dbc2196decd2af029f00921ac5e2 +size 56367 diff --git a/screenshots/de/features.messages.impl.timeline.components_MessageShieldView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_MessageShieldView_Day_0_de.png index 321e3a90825..c89a3fc1300 100644 --- a/screenshots/de/features.messages.impl.timeline.components_MessageShieldView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_MessageShieldView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79bf43b0902076002f3ddea0ebedbd1ac4be933e55c7fb503b5fabdaa0f6e12a -size 48183 +oid sha256:b7095fe731139c16b1f56bab2d7314e6df0bb56778d900bc611d3ddbaa6f4298 +size 58692 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_de.png index f3e5b478a9e..a7ba0f04ec9 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aca35422649b289a139eeb34ec00fd1ac984115439827932bcfa7039c7de671c -size 6714 +oid sha256:3ff70ab2e23f935cbc277c89d0a7f4dad7520ee6cd57ce5be1f12230fc69f383 +size 6711 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 28340febd31..98acc146d71 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:9367f66dcb7b5aaac0fd38dca80fe4789158da4ad9feeda0818fd533bb6edbe7 -size 42519 +oid sha256:f81a93d139c6904f85395b7bab5576bc6686b8057d18af7d698fd41cc041df1f +size 41829 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_de.png index 903928e3683..7b5265b4b1b 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bde168175cbe02263bc47ce77072a9a254c152127f05a4e0e51873da861372f1 -size 34072 +oid sha256:b63e95b0309690eb2907f5390d068dba3199e9f91de0693f2b2a1e1c387344e3 +size 34026 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_de.png index 4cf1faed8f1..c17fee2d09a 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc9452163af66b684598c020b54a15ce530969f2eb4ce264a36591a76b93c3ed -size 35448 +oid sha256:82665bcfa8bac9fd29058bb093fe4cbfbd119a8b446f3104a78cbfca6df422d3 +size 35413 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_de.png new file mode 100644 index 00000000000..757be6e7b1a --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4c20415dd48053f55274361837c3b181d70c050c4a5b5fc3300fdcbe4114e65 +size 11201 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 0362d1fd5ab..0653c9bc74a 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:882a80aee379aaa550dd33f70d24deca6fb4faf93cc11f830a2080c9279fcb6c -size 37983 +oid sha256:842994dcbfe82a3bf6874e2167d717c2a0bbe75c37a3499b82c7856fb4432372 +size 38520 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_de.png index bc4a2e99d14..5ccb5b1d481 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fc2ca4c106dbf0b7907c9f2fb493799afe21ee55c1c9933dfd460e3960ea4c4 -size 79341 +oid sha256:5b62274b645a661271cdd83dae6c55947d76f19aee580d701a096b46d625089e +size 79353 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png index 024c6e92b2e..f183602a075 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3dbde9e29f3b2a47689958e0a6cf12c9d0defc8cb8a2219e8fb9f135c421440 -size 151346 +oid sha256:cb8a3fad1817764b52e0db7c3f5c6b7e103438928399f4344713272a53374662 +size 364750 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png index bfd10d8d88d..6cedac977c1 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40668ef04dc1c8a44b74dbf046ca812cc49ae8b6761d65a002392e88fdefa086 -size 157214 +oid sha256:51df75495180e7665befca85e0e986d26481660cf6f6f6e1cc95b4caecd36955 +size 370225 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png index 6d9082abcf4..951b754bfd9 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e948e173f81dff5a7b9c26af31ff8fa814a76ac89245b43c1c5aa27ac1644de -size 150550 +oid sha256:84f9227018c9ece9ca911e33b50073f3674cf5c3d2473b109b35c8e3af597fce +size 363930 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png index 747aab9e909..4e388fe7505 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28fc5693f332060f41852d2ea3825881cd1c34acd447d6e0c64d055b5ccdb9bf -size 152708 +oid sha256:fdc57799e07bcddd56f9edde75113bc4cffa1ba8c02370ae3fe0d07b1106da51 +size 365987 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_de.png index b4f5cd76b99..55e117e656a 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87660fca1351a5b0ab931acdda4c137e30e35309b554a29ce68e51baeb7d302e -size 54613 +oid sha256:0db1de722615d9320694873a12af1bfa6347ffba937587c11248fde4e4845fbf +size 54499 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_de.png index be1129155d7..85c4f1d8b79 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aceb78a72088d65a22efcb4c9aef38d6630bce916f9e3509b8c955cf220a3d14 -size 8095 +oid sha256:baa8a8f2240cc62e68dd9442e1d08c58a2a56af25aaf098f0ed04477a415a140 +size 8088 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_de.png index ea4a3a9cd0f..f876010a484 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb7083152d7a6f3d4a1209e1847730e87b3a5b847350d60262483bfd02156d07 -size 17209 +oid sha256:bd33a193848c1e8183c224f1df06e1f9aa4f500a63b24ab7d22de9c505e390e2 +size 17206 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_de.png index 9fc01bfb259..b313d3d9da6 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aa1058437a0b6560c148c5e3f61b864e04fb543b7a1262bba90c334e018158d -size 25889 +oid sha256:ddcbafff84fb68f49f6b379b60f877b837eec22a3bb3ffa8067c5643d41afd6b +size 25885 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_de.png index 8575f327129..388583a46cb 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbed02736da4d754698e2d085916c80c5e945106f1a26d4572aef42bf5ca369a -size 24497 +oid sha256:ed0ea1d7b8803af4aff5d495fd9987e77f040d1ec77cd3cc6a93ab22fc70c66e +size 24502 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_de.png index ef9f6f13206..a0ef01c2d33 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5436bfadb7659d27759a2f9df4205c8e2f59f24380e332f1b79eeadaf4c414c1 -size 24414 +oid sha256:81fee4f5c0227b65420ee1b78f2e2c4b20ea2f8997a1e3df5d5a5cb22a5828d3 +size 24417 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_de.png b/screenshots/de/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_de.png index c7d4da8073d..a0581286377 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21e6d5500deb7a1533e38cec756a7f4bc265562ea25c146b3dbb66689108de08 -size 37590 +oid sha256:696794f694b55ba1e21d0031b22d10537be836188d8eaa6ea1dfbad4be0ca616 +size 37566 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_0_de.png index 655ff7d819e..812a37e1a9b 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02ff3103350fb27a614b7ea590e0b0002500d3c3310146e2763390388824c1ec -size 50423 +oid sha256:9a6d76006cefee9da331704ebdd847efdd0f833569db0177a61d2115a43858ce +size 50431 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png index 9dc9c0ce536..a3d2247b40c 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47834691177f145f00fd2e0672c635741ff3bf0e4f4fb6bce779a1a04e3b4326 -size 84125 +oid sha256:f3cfcf4a2c883cc3db395d14d02b96a4b211b4a9dc24191c222169700b167276 +size 84080 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png index e3a6fc2c32d..fc9527c1a6a 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95e6fcbee41e2ae80b7ae94d9e59034cf9e86f3ec703ca1efc3bc2edeb1eed48 -size 51910 +oid sha256:a51aa3750c9174ced3bf8d50784fb64a1cc568ad81f271881adea87a753952ed +size 51907 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png index c00f90f4ee4..03defa6d0fb 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db933395df1e1a354accd0956e727e33be72c6af50821477c34e97bb0feb308b -size 63469 +oid sha256:a3b4a5e7816b5ea30e6d11142aec791b3099770302a082cf93462c33c57e8d25 +size 63467 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png index 73ee44636e0..880fdca99b9 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:884e174f1a6fba9137493d821dca9e88a3393b3a620cf8092416e8d043b17fd3 -size 48332 +oid sha256:60ddfc47e4f1065935045277669facc066160d86f2b74ef4b909bacbb3d08ae1 +size 48321 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png index d756889011b..178d082f008 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:332d421d80802e1c88471f98fba5884dec399d09ab56ae08dd30269471eba6b1 -size 72028 +oid sha256:34d53f3e263791f2c432aefcd566d27fffb4034268f35e92333c932947c7f698 +size 72075 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png index 49d4c9002ed..579bcf9954c 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7eaec18f60e71583055aa2feea1053602167dfe20e2e748b07cdced3f1f2432 -size 58063 +oid sha256:404480e7e7e5cddd60d1c097d8892520b51f70030c393b3a16bff3286f16fcf1 +size 58061 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png index 7dad3e4dc0c..007d075c363 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ddc24acf7fadb4d70781bb2df7417c6389553ce59cbbc68a939f1e2f1a498a7 -size 64365 +oid sha256:24e32b2c8207e0aaf37d21d82cf0c920e20a56e6a4ba01ade77115b6574cbc64 +size 64357 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_1_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_1_de.png index a2e15d5e83b..5c46087fe47 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77f8bc13c8f58379a15d676fde2b8e56eff70434b24d10bd6db805b9eac7a8ac -size 71827 +oid sha256:f9c0079dbf4dffcf7ee49d66da6403d35e78768a375e04975c5a9a07bd637d4a +size 71828 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png index ec6fb4d5ec1..547f4aa714a 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c991fe686a800fa172d371f109c7cddcacfccd2777c0d5e39ae0fe5fb212aa51 -size 70511 +oid sha256:58196ecddea174d5e941fac0766f40f04e64e7bb2cd82c1540537fb2e9f04bbe +size 70528 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png index e88f8536feb..7ea447b8fb0 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a586bcc9d36e042471c001ca8f94c3fdf971d32dc8217ed7f5e6cb210bf75f5 -size 73011 +oid sha256:eb5472ae58f7f4eb4e278d2c93c4eeaad0df2721ec7811027f76bb3f602edbc3 +size 73590 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_8_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_8_de.png index f7612534e8b..e728d86f6f9 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_8_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0ae852f23e3954610f4f5fe91276a57423d8dade99a0720aebabe912487e411 -size 53431 +oid sha256:f74a7a73434450cf5d984a476433140a4857b8ee958498db528a2e32f3b23f56 +size 53423 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 b3c0d8a1a9c..6f776b670b4 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:ece8c46ed93d567c0c7e083d6b35dcf58d515c418f1348bc1a596ae5cd0e415e -size 57824 +oid sha256:ada5d53903f469cb0b0b981546826b6664ce3f983e5dcab74b7d99bc8d568cbc +size 57832 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 144d57de771..36b43e7db73 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:95b86ce0454c63decc5283ce07a81f76773e3312eca6a27ef0026959041d30fc -size 57852 +oid sha256:092117db4970bb43f37aa04ae4ea3e91129bae8ab3ed05fd594fc3fb964bb317 +size 57860 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png index e741b958589..f2e38e02d8d 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d186fcc63bb524c23b6b42e6c4fab6f902271c755fc9c7de21ff609b2ab6483 -size 61170 +oid sha256:47eb746b2ab54a1228e213d4f6920105cb863afa3bc98be46ba7d5f1a186d8a7 +size 61180 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_12_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_12_de.png new file mode 100644 index 00000000000..f4cbcfc1a5e --- /dev/null +++ b/screenshots/de/features.messages.impl_MessagesView_Day_12_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73033c71b99d30498239bb98c959f27cc37d7217e4bf1c0fcf3f56665cfc8357 +size 60151 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_13_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_13_de.png new file mode 100644 index 00000000000..3f21c36631b --- /dev/null +++ b/screenshots/de/features.messages.impl_MessagesView_Day_13_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2029d42a8c696f597438c9dfa38639eadd1f10a00ee404ccb07bd17c9f480f4e +size 60078 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_14_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_14_de.png new file mode 100644 index 00000000000..720695a5cd2 --- /dev/null +++ b/screenshots/de/features.messages.impl_MessagesView_Day_14_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a117fd360262156cef595eabfaee2648cdb185678527d9a0aec602160b7823f4 +size 64801 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 d40b5db1ad7..1f6447f3807 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:6ccc4aa5c095c7c01585d66f5caf146eba7060ba361ec03719c8474abb6f5ccf -size 57002 +oid sha256:ffce521170b76b53868a9b9a2b51216dce35bebe616acc097b2593977974184b +size 57024 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_2_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_2_de.png index d9a35aff1b6..8472234a142 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_2_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a64c1a71b6d93f91243cf368c0b50cc319afe0846f1ec5eca41358015ac61dc3 -size 41944 +oid sha256:72f4a3f26ca44a47596b497cfed8954b556dafcfaeb568cce81c6d35ff8d7dea +size 41987 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 f99d9c7d6d5..b822f7d1064 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:d529b4fd0e5b5ba965bfcd6ebaebc3a00e85d06d56ece90e00c9a64bcef4d966 -size 60422 +oid sha256:164f3bacc2ca1c35a93fe5ec1f8fb6356c5fca7507422ee6400f28cfb6556743 +size 60419 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 8181e49ac81..8e60bb99a27 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:c188ba8f8c22d02041c40273bacf3bf1cb668899eca76b53d31fe7f53631ea00 -size 55777 +oid sha256:2acc6a4f19c60d5ff416e02ac6d024bf8291f95a08ea2fabb29049c1240abcf0 +size 55524 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 66ed2c1d1c3..cad0d74da6e 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:da56456bff1a3c3227dac7fd307ea2e65b6ed06d18198321d23db71e282370c8 -size 55629 +oid sha256:f4224b4cb9df1cba1c51d93a8454ac397442e4ddc25226455bc4de6d753c62d8 +size 59300 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_6_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_6_de.png index 89b2e22c005..40434488137 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_6_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9be22b8fabe1142e77d64fc3b90587e174f12c0df666fa0280061ddde994681 -size 54119 +oid sha256:4810560dc63bdb17ae199183685f1854f9bfff24515efa1e0cdf634be761b646 +size 54170 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 c547485e94c..112279203a9 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:2ab09fb365a49668712764bbe4ba75a55e3de0b1bf80f7a5c74b5c8400aee3a0 -size 59684 +oid sha256:a87cc7029191fb410f72868115f27ec4863e53b23f4ff29536a77a77608c19be +size 59689 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 e6d07e3ba6b..a25c8f37245 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:76d16e02ae85372018b0e4a75640344fcf6ef6ccfe628a3d0df3d39cd0cad0fe -size 60292 +oid sha256:f6a19f900b395bc2f9f4b7eb1ebca10d4ed9d685fec3cb211f102345c345be1c +size 60303 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 5cbd4c34f8a..b032acd9355 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:deba118cd08fa05c0e3338545616f9aedc0581d19ea9f3968bdd30aa33cde749 -size 47876 +oid sha256:62b2496a0b101ad4d9aa48b5bde9f70c2d18fbea3eef7b743a27383a42c9b32f +size 47866 diff --git a/screenshots/de/features.migration.impl_MigrationView_Day_1_de.png b/screenshots/de/features.migration.impl_MigrationView_Day_1_de.png index 99dec9a60a6..5fcd3f82169 100644 --- a/screenshots/de/features.migration.impl_MigrationView_Day_1_de.png +++ b/screenshots/de/features.migration.impl_MigrationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c18a6c4037767617e5547f7191f99e59a5481b11ebda90346ee8db7af8f8deeb -size 7912 +oid sha256:247ab4981812b290f010be44eee2d7c72b08ef650ad844dbf1e973da3b100833 +size 7913 diff --git a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_0_de.png b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_0_de.png deleted file mode 100644 index 735b7b68346..00000000000 --- a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:832fe4b9ed7b53c161374cc72438c49f254569b33d64ded5f661152976b0d883 -size 315128 diff --git a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_1_de.png b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_1_de.png deleted file mode 100644 index 441a167459f..00000000000 --- a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a5bb6a1bec8b521c06651f31825f2245441232cd89c4da53280ddc29c3554f91 -size 310728 diff --git a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_2_de.png b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_2_de.png deleted file mode 100644 index b77eaee61d0..00000000000 --- a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d9dff767d267c198d1cdcdc79c313b7fd04d1d90436dd754fc0e07b69a2dddc5 -size 313727 diff --git a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_3_de.png b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_3_de.png deleted file mode 100644 index 42ac955c15c..00000000000 --- a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fead83f04b6fa70a2f3b1cd9c05e99a37b5ab8d8a6fab2c0d3d665c6d0838613 -size 307936 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_de.png index 33b9c1cd57c..7b854cc7d81 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00fe186037077e2756d6d067f39622143e106f281ee49929a4e7dcf2caffbb82 -size 21920 +oid sha256:1806e21e6eeb04d301bf1700be683eabd546694561a4cd5930fd8d0b3217da9d +size 21948 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_de.png index 155b91bbea7..3a974eaf9bd 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec05d31c306a5066f88d644395143f131824c848df718109611b8d1ca3f5aa0d -size 21758 +oid sha256:05ddf0870ca90ef4ebc51639821d740d173ad2ae4de38faed40280dd2cac8efc +size 21686 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_de.png index 98a0ae335fe..0695cb177b9 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d47da269f00ff61627923ac38c863decf3b764e95c2d96c460673bff36d79879 -size 21824 +oid sha256:e6235052e32413babb1563da8c29bd9543d221b23844b9d77d854017dc9ac7a7 +size 21782 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_de.png index c56bcf57d2f..8e58d01b1f8 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:763a54f1ff4d60801fde21232ee140f6a1b71c3a894bbfd410ad863cf87f1c9b -size 22033 +oid sha256:a871ba2d6889369304a18e050c5a20a6e244821a6d0ca7eb5e12257a522937bf +size 22079 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_de.png index 595be110b1a..6e30354e307 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f34f7ac171692c367ca4d28e3e65910e3226d015036cc8e47e798373ba9efa6 -size 22002 +oid sha256:4ce476e748ac3aa87081c67751a4690371fb6e560885bdb64abc6a54ba333252 +size 21959 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_de.png index 70d830ad7e1..fab607df183 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a89ea2ea209f98899fb9754b963099509d7049fa9459a8dfb91198c1bb167385 -size 52760 +oid sha256:b8e1534e7031a85120422d6cf55ec7f201d31b5f4fb41f1cd4149c08775e4593 +size 52765 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_de.png index c70824b12d6..7f401479bf7 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:641167924c8b1ce1485a4c2d8f5c62d80c00b26920e7fd8878728b1c5d776766 -size 48691 +oid sha256:ddc44dabe3e41569d1e13932f0d93cf3735c658880c9b3c1a917e238189e9777 +size 48713 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreator_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreator_Day_0_de.png index 3904710dc35..a4573547e6e 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreator_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreator_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:137c7963233622f4297e561f7a22063249d515c11a58d0bcbd15b8bc8a86237f -size 52463 +oid sha256:c579ccb8997881839455946315be561cedf55c2beffc7b4162b6a354792f66a4 +size 52470 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_de.png index ad8e799df9c..3995a91a26e 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bc45f2776c67e0c81f3369286b6894b0fa4044008076a26b096d79f060d6703 -size 48347 +oid sha256:da326c358b6e63c56997c91331fcb577194625d146c1f19e3140126bc87c97c4 +size 48356 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewEnded_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewEnded_Day_0_de.png index c70824b12d6..7f401479bf7 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewEnded_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewEnded_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:641167924c8b1ce1485a4c2d8f5c62d80c00b26920e7fd8878728b1c5d776766 -size 48691 +oid sha256:ddc44dabe3e41569d1e13932f0d93cf3735c658880c9b3c1a917e238189e9777 +size 48713 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_de.png index 6b2a228413e..174016671b6 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7d474526126782bbe3ec9e84fe3c2fdde207da6cece295145572210a83d4594 -size 46312 +oid sha256:e0b487a828e25112c2c60a2bec570c4965a5c30de8bea785da32d75af4b807a6 +size 46071 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_0_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_0_de.png index b4c47816669..bbe187127e7 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_0_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa1963b726c746016db7ef50f332ea1ad9bf16e2571a506462ef992a5b02292c -size 39394 +oid sha256:400fa3effebadae2a8ec24a177a6d497cf8f363fe6caecac90dfccaf67de31c6 +size 39525 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_1_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_1_de.png index 0c361d7b902..2ae47a638e0 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_1_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad923904e450ff20c9eb3b922569b4411e77ada904220aa0696729957617f0fc -size 42235 +oid sha256:e0f985f494027e1d6c34e09762cdec83d8878dcaf9b57d44717cd6a38447f8bd +size 42247 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_2_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_2_de.png index 6a6ec031fcb..c065190f2c8 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_2_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f7d50ab8ba39fddc59e4ec269bd5a642c89850b63bf49ed3070297aec81f219 -size 42347 +oid sha256:6661c38abe342c607c2d848185a9e005420f91b55b8f0276c99d787a348d2444 +size 42622 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_3_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_3_de.png index 38948a8d3f7..10887ccef1c 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_3_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e74b1bd8782053a2cae360f4adb80fc915b500d2aadb4df477ec7ebd48adb1fa -size 49758 +oid sha256:b79f1ddb00cfd72f16bbda98724bc6d43c1e2b7ebf9e8792ec7efdb18256c649 +size 49828 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_4_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_4_de.png index 9fa65da9032..dab4688b5cb 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_4_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d7d9291ccb6140f7ce7773a2392b4e0be77c0260e7d505c9cd3486658b90b0a -size 27797 +oid sha256:2c829dc25e54af1fa2d0db02e1acf3a6f561ee06633c27efd8b0354d9a8c4567 +size 27835 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_5_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_5_de.png index 25343cc444d..7ffb3dcd7a2 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_5_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d8c229a1808b31425794732f06dce68f48b30bd1adc88d5574c2c88fdc87cf2 -size 118546 +oid sha256:ec4720ea8139527df60cb28b482c773e1af96555df87bcc8a96ea34dd42502bc +size 118487 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_6_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_6_de.png index 2cef8a96938..3383e2faff4 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_6_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b37aaed9c0795ad7475df9a416b0481123677556d9a2d99cf2080b1225d3b8a -size 43141 +oid sha256:8f3dd51cb2a0fca30e4864f777262cc8b6a2f3e0f7295c3fe9ece67275d8ad4f +size 43270 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_7_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_7_de.png index b0e8239e69c..f0da4ef4933 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_7_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b192b6c245f9e58636a74e759b93bdfcaa8bb782ddc4d87acfb02f2428a517ba -size 42517 +oid sha256:fe62e13e9f9ebd24d2f930158fa0985e11f4ebaf974a96d334e26f89afda8e5f +size 40791 diff --git a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_0_de.png b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_0_de.png index 4e8a2c92f6b..dd992779f3e 100644 --- a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_0_de.png +++ b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df5bf68df433df6bd6dee44603453a96a3f7162e129ff2c45506a2ddefed077e -size 60568 +oid sha256:1f7cd75461e1aa9716bd458b171c9cda3f8155b74af2a9b308b8a29a3d934f39 +size 60854 diff --git a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_1_de.png b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_1_de.png index 3fc4226d55d..3cf570131de 100644 --- a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_1_de.png +++ b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e24ce06fd5668bb8b8431a72477fb80a647eb431522ec5016e0ea4433ff88b00 -size 64794 +oid sha256:c8aa53b03ecc65005e9fd96e690af55a39ee6f94f18ca5ab7fbf99b9caf5ac7d +size 65016 diff --git a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_2_de.png b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_2_de.png index 8d96d4c44ca..b8c08d67c99 100644 --- a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_2_de.png +++ b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c261be0a1602178bbbfe2c480005aa4c0cd286171f09687cf7dcd6cdf3bfa65 -size 17848 +oid sha256:79bacc1b730cbcc9ebe3431a142d336018308cbcbd1857c79ee7152151d59523 +size 18209 diff --git a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_3_de.png b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_3_de.png index f05df52be37..38499891ee6 100644 --- a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_3_de.png +++ b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a0c118fe9b1f48c061dd92d073ded45ffc6d54fc6d8d0b6dcd8666b1300402e -size 18657 +oid sha256:5c7f4d33b8b309e98a06c151ffb125dfd5983f51778a9a310023d312dc136b0e +size 18469 diff --git a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_4_de.png b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_4_de.png index 9eaf088ddf0..985fbc86a3c 100644 --- a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_4_de.png +++ b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d46460d4289863c1eb3982917832fe66e2fdaacce48d0c54984612900ed8b08a -size 22325 +oid sha256:d05a3a87854480e5a15ca99e70d5cac728c2dedb736ee790961324f54c355dd2 +size 22164 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png new file mode 100644 index 00000000000..62c9543ea0e --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13606b63d8495af917a26c321ed76d71abdde374b6805d425b87b7d1ec6fca09 +size 53413 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png new file mode 100644 index 00000000000..1439a1b05ac --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31dd452f0fdeeca9c967233c311e89a2f1f9ad971f4491627bfb2dc71cf6e50b +size 53280 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png new file mode 100644 index 00000000000..45bd10225be --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c49e44437d70878a7a556d5c9ffa97d3f05e8087c06adbfb67b5cbd8daa16b0b +size 53260 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png new file mode 100644 index 00000000000..ca9163797f1 --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ceef44f0b46b4421da80a40af5d2143fe28c886ceb10561f6b5788d93e8d56c +size 53271 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png new file mode 100644 index 00000000000..c8efa957f1d --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d015747828affb18a881a5aa5e386ae6093f1dd1412abec72a3d3f8559b75213 +size 53219 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png new file mode 100644 index 00000000000..a8934515c76 --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0342fc12f6b773a74bc3cb7ba0cb1315b855f4c8a8cb7a6173030f3cc82c1d7 +size 53401 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png new file mode 100644 index 00000000000..33bb12fde22 --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff242493b28a6e8ae6847a5674ad05a8752cf35d890430acbc31b366a3ceb7f +size 53227 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png new file mode 100644 index 00000000000..0d4a2fc84d6 --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22e7284a93253845d513f8f1205b19c87a98c8d78ef3192be9f91900204e6999 +size 52935 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png new file mode 100644 index 00000000000..f8b4c8797c6 --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66202c3504557cb9849aa3a9e766d4ed7a040191b99db0aec25f198384b9e64b +size 55597 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png new file mode 100644 index 00000000000..518024d78dc --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43b0d62d8560186efae35ebf5231a7b874c229cac972153fd207037e623a32e3 +size 55477 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png new file mode 100644 index 00000000000..1d1ace7a39a --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39d8b2935db860e023fa26883ff792de65497852ca7cadc8375548be5822cde3 +size 55486 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png new file mode 100644 index 00000000000..6b882d18b29 --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5bb3c9f74ec80f53921d7212d7835cd8707b9563be76aa35a56f52d5c0a503d +size 55489 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png new file mode 100644 index 00000000000..d04ce0e88cb --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfb18049747451794530cb1a85a3a374ccd034ffdd44ffb3ac063049106eabc6 +size 55372 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png new file mode 100644 index 00000000000..04f2c240880 --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:857cb3fbadd27fe715a3e430d273565a6e36504fd3fbef077b896e880b0f21ca +size 55604 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png new file mode 100644 index 00000000000..cac1549c902 --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6daa75017ffff81889d2640179c2608a74f8fbea7c22fb0a5ef76a716db24a80 +size 55225 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png new file mode 100644 index 00000000000..b53f27674a3 --- /dev/null +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aee7d2d131c88fd9f98a4b2793d6b24e568d962015470788639572ec4824a782 +size 54790 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_de.png deleted file mode 100644 index 7aea7b47a7f..00000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e8ce478fe32b1fb8479efb86f5305611e5f1237a98f12392acb1a79bfe43af55 -size 68549 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_de.png deleted file mode 100644 index 55c94e44cff..00000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5fdd3f6aff2518164dd958d3f4b3ff014e2c4c5f6714525ee9fc15eec6df76cb -size 68305 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_de.png deleted file mode 100644 index a2a2640deae..00000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff632eb1ebd692ce89833cfa0912cdbb2d9eebd1b03fee1dc5c57717bb373c1a -size 34395 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_de.png deleted file mode 100644 index 50010c62050..00000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80413c20e81da286a97ff62630426bb0700fd48e7fdbf2d039a89c77485c8b36 -size 68356 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_de.png deleted file mode 100644 index b908d63738e..00000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9182d5dbaad926abfaf55d80a4af3aa7da6e45054ed2a02fdaefdea081b66506 -size 68344 diff --git a/screenshots/de/features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_de.png b/screenshots/de/features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_de.png index 677f100cbb7..0da6e2bf8ad 100644 --- a/screenshots/de/features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_de.png +++ b/screenshots/de/features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:848bdb220be53e683c02744d2b3023cb80c3a9e5cbc14e3db69986ccc424463e -size 28478 +oid sha256:1c86db28bbcb1da58f4dc0c4accb81689590af1dbaf1e78da76fff173cb1d0a6 +size 28915 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 d818b6ec2fc..b3592059e9f 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:3f27a5e403b15f4c9583dab04ecc346ace073288aff2c8a230d23356869c752b -size 60388 +oid sha256:4261bab9dc77c85a8c379831cc35c72348a06d5caca8a9a6ca2d2754dc18dbe1 +size 56393 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 d818b6ec2fc..b3592059e9f 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:3f27a5e403b15f4c9583dab04ecc346ace073288aff2c8a230d23356869c752b -size 60388 +oid sha256:4261bab9dc77c85a8c379831cc35c72348a06d5caca8a9a6ca2d2754dc18dbe1 +size 56393 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 96d554d7993..e77301bcd18 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:a74f58d01f0646913ae0f25a6fcc46c910226a2b1fcba8d318e8a90733564ed0 -size 58097 +oid sha256:b8991543cf3bfd4e9fbde2a4c217c2980fb4fc4e751f0755d0abf14faea70058 +size 54130 diff --git a/screenshots/de/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_de.png b/screenshots/de/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_de.png index fbbf551a922..645d300be83 100644 --- a/screenshots/de/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_de.png +++ b/screenshots/de/features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65229995bf44ef326fdad042a368d79a402b085ab3c8134e9a4a3626c54ed854 -size 41451 +oid sha256:7954629e2be92fb6cb230a772c4e8043566ef2e172038e6df6e1df2aed843717 +size 42099 diff --git a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_de.png b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_de.png index 0d90d8d9582..e5de53bbc41 100644 --- a/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_de.png +++ b/screenshots/de/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc06581eca561009514bba2c947c5d759af9b8cecf4c50cc9224f543e295c47e -size 67919 +oid sha256:3cb10e8db83dfa6a36b3f5e23efe98904135f1b1d043f1244749214a548c4806 +size 68493 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_10_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_10_de.png index 096f418e16a..83bee2bfdbf 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_10_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d30ce90a44498f585fa8616b4ef1af3ae71d505dce8dffb7b043dfdfedc0fd2 -size 42758 +oid sha256:9c23f266c573db33bd933d563f290d915d0e759e371c9e0180035f8c1ca9a30e +size 48610 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_11_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_11_de.png index 5794bfc059c..096f418e16a 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_11_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7250a25f1bda5867605057d808d235b44b59e062eef499d08631222635e8ff72 -size 72404 +oid sha256:3d30ce90a44498f585fa8616b4ef1af3ae71d505dce8dffb7b043dfdfedc0fd2 +size 42758 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_12_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_12_de.png index b212c7f186f..829ef3f49fe 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_12_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5ffdb0bd0677cc532acdd67d2d08a74d1dfdf85924ddbd2ff0ecb2a6d1d225b -size 19602 +oid sha256:69b9f48d4a7eeea156f23172be4796c2e695cf237b09351b68574d537c1981f8 +size 74321 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_13_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_13_de.png new file mode 100644 index 00000000000..b212c7f186f --- /dev/null +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_13_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5ffdb0bd0677cc532acdd67d2d08a74d1dfdf85924ddbd2ff0ecb2a6d1d225b +size 19602 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_6_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_6_de.png index cf4a0f237a5..602c12990dc 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_6_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:146c9ef4555aac5cfa99a2c637f3a969061a95e06586f12eb341dddd8bcdd380 -size 47008 +oid sha256:7c10adfcd02d9fb59ab9bf631f79b075b60d643e668a589597a8c700d612ef38 +size 47153 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_7_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_7_de.png index c8ae2f42090..66896dc9edf 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_7_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:691154991e241304d8e2f48da468e4b0ed43090bac56e1f2c9f26e0fe900cdbc -size 54663 +oid sha256:484b957e2f6299574b5718266584e58b4af3e0c3a64414b9da40bd4a979e62f0 +size 52251 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_9_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_9_de.png index 83bee2bfdbf..c8ae2f42090 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_9_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c23f266c573db33bd933d563f290d915d0e759e371c9e0180035f8c1ca9a30e -size 48610 +oid sha256:691154991e241304d8e2f48da468e4b0ed43090bac56e1f2c9f26e0fe900cdbc +size 54663 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 da6e99ba937..56ab3254da7 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:464208156f1f00cb806034f881f9dd053aaff5f2e647cdd1bf6835ad0ea87a12 -size 39252 +oid sha256:fa9a8c5b045119e98440ab781caad2112298d3c9f415a812da02e6da5e4d7a79 +size 39042 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 65b4bb29dfb..0a0e1fa6e34 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:5311c11a79c453567a6aa1b8a73710815c1fa7b12934c3ad70087b05c74315e9 -size 39006 +oid sha256:6ac064c6ad092caad1035b4d028cdca8f9d74cabe523a46a58746f135fb56cc8 +size 38779 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 6d4f1a1e19b..3170d71794c 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:e0b514e7cb497380f8d4e63ee3a5f29a19e91724aff0e748eaf7ba1510e2302a -size 40261 +oid sha256:ef2e214a067224f7a7d5381f264e8bdf4fda921add8aa9f2871e773728b19c5c +size 40096 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 5c7bba8e96a..80dee42a6df 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:c498c73c00b8e9b3dacfaab6be5faf49f1f8a4c0b31c7590471cfb152bd85ba0 -size 40264 +oid sha256:ca1dfbd176b0d820d5b43d3b3af662ffd2c87985a4b018815c7c76baf43c5021 +size 40099 diff --git a/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_de.png b/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_de.png new file mode 100644 index 00000000000..fed4e6873dd --- /dev/null +++ b/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78688c13e131c5000e7dec50addad0053bb5d3f7dd90ac092eab81623dac69cc +size 70524 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 d75932ca37b..11c218360b1 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:0ffa68144d62b53507e8f4bd9d96b2a8349457b0c4647e385cd3ec67d647331e -size 24864 +oid sha256:2c063991616a5bdee93cf1cc121a4959a225053592a5c0cc3a0bda7416072226 +size 24943 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 6ecd0ec289a..348ee350449 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:4c6ef6d8a266e8da6473ec3f50ffce1d42d25c62dc698aa41742791ec1855579 -size 28644 +oid sha256:120d0f6f4d2221e405544688c16688523a0a5fb5fa2ff789758482cc2a3aefbf +size 29117 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_0_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_0_de.png index 32700b0b7eb..f9336cfa383 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_0_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9f41b578db2e8e8f2e3ba3f6f65e20d7e3225510bdcbf5d1ebe9e68bee8290a -size 79647 +oid sha256:26bd919aa064af2fd0b66120a7a18b1d942e629343ea73789c23d8b96eff581c +size 82179 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_1_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_1_de.png index faa108a66ba..f418041a365 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_1_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4e08e2cd8817acc2a6fe9dc65d39134f94a2f16962ac0ef7fcb3757d05c8f5f -size 98899 +oid sha256:28baf0d6180bf3fa723152612499098c5489d746c419be445d7954c72c4623d5 +size 88463 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_2_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_2_de.png index 7167008af83..1e9d761258e 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_2_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a43926c96a5cfefe179091e79083ea3148b4f6d66c4f8356168ce8ba165d6a1 -size 75125 +oid sha256:00c70fff807ae67d65b909a98dd270108ffa92a6c5ba5351353e276db6547b18 +size 77891 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_3_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_3_de.png index 32700b0b7eb..f9336cfa383 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_3_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9f41b578db2e8e8f2e3ba3f6f65e20d7e3225510bdcbf5d1ebe9e68bee8290a -size 79647 +oid sha256:26bd919aa064af2fd0b66120a7a18b1d942e629343ea73789c23d8b96eff581c +size 82179 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_4_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_4_de.png index 2c533571505..249198d34c5 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_4_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f229d3f123b86e4d5ea4e59ea35a87cac0c48cb39ba7fd6ab424a2e6cf38432 -size 57076 +oid sha256:fe9dea15ec1c9b8f00d5acfb7ea96d49a8e74c02abd538d1d7461a8ccb20721f +size 59337 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_0_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_0_de.png new file mode 100644 index 00000000000..739d14a5e97 --- /dev/null +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3b1e80e6d7e3cd377883da370017cc959d0b8d7fff3894ccff3cbe79fdb1b54 +size 34374 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_1_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_1_de.png new file mode 100644 index 00000000000..49568a385fb --- /dev/null +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:497fc391210a0cd4dfe3e2573316e08af9472dd7cc8e93dc0dd9bbd1894b27e7 +size 34179 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_2_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_2_de.png new file mode 100644 index 00000000000..872123f5093 --- /dev/null +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb7ebd6e806d621083e651ba4158d55e9ffd05dbbcba85acb1e5d46196e1926f +size 33971 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_3_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_3_de.png new file mode 100644 index 00000000000..708590ec5b6 --- /dev/null +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f371558881fb39504af28ab5633c1458df2c1352c2cc89710220b3601a4a6e5 +size 30634 diff --git a/screenshots/de/features.reportroom.impl_ReportRoomView_Day_4_de.png b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_4_de.png new file mode 100644 index 00000000000..0ff8944f454 --- /dev/null +++ b/screenshots/de/features.reportroom.impl_ReportRoomView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67645229b7efbe53411ebaf1e5fe662d9204630a6fa9edd6b5a3d2f565d2c690 +size 33315 diff --git a/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_de.png b/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_de.png new file mode 100644 index 00000000000..e3c6c5bff3c --- /dev/null +++ b/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:787f2a7755a4675f3ca3e897553fdf83bc11dcb0357d0bd2615dfc8e52c47cb3 +size 34939 diff --git a/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_de.png b/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_de.png index 49f1b7fc83e..5a362ae5345 100644 --- a/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_de.png +++ b/screenshots/de/features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb04686d6e7c4e26edacc4cf24a4e29adfcbff0527be23e89f58b7b09aea152f -size 101743 +oid sha256:1a35596ef35cd3fac5b9deb82df89b7066949c8c272bcb484c6ca52434b61a06 +size 27031 diff --git a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_de.png b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_de.png index 28b48c96729..af77b3d9169 100644 --- a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c8c2f78930685af16b95527f5781783693f0be8280808b412d8bfa581013aba -size 15649 +oid sha256:016833cdfd1d7dc4b50766a611fb90c1389edb634139d13f5aba456f0eddcb8e +size 14698 diff --git a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_de.png b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_de.png index 22bd6ac780c..ea306c4dc77 100644 --- a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_de.png +++ b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd1a9370e7482947abe8d9ac447116683db85aa8ebc8cca99d12d5cc3246fdca -size 26222 +oid sha256:3368acb55a193d98ff2208c9d2e3bb1b1214c61ba4468a88a3f9049ac5db2fc6 +size 25458 diff --git a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_de.png b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_de.png index ff27991d2d7..317117e7939 100644 --- a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_de.png +++ b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe91eae1e1f718013495f5e0b597d0614ece5b5fab0e0c5c51cfcd4c9e8826e1 -size 12566 +oid sha256:266041ac236302fef0f1f6b40f0271574bbe8a34d4f56a156c1af00e54ed58be +size 11816 diff --git a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_de.png b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_de.png index 1f540d8d35c..faf1062c3bb 100644 --- a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_de.png +++ b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21ee719709175be9f7f3f65f95c1ef9f4f6362cd03ea97aef7566e5525fa9d9b -size 23693 +oid sha256:22c5455db75e82a22e2af5a5a420abe8e8c622cfa42a92a920620ae2c8f20ee7 +size 23020 diff --git a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_de.png b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_de.png index 8cc4273ed62..e08d86610cf 100644 --- a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_de.png +++ b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7052d42b5386566bab5dc8e8e724b753bc284917b953efe958ee5f307a5edbf8 -size 15834 +oid sha256:fa968b2e0d72f9c66be0dd39b5b2ca33148bcd60b9bf9f6987b1a867c8d9d579 +size 15082 diff --git a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_de.png b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_de.png index ee673e6b80b..19d8f833f48 100644 --- a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_de.png +++ b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cd2a7af32f37a1353825277c3e3b1da29e6e0b9c63d0db75f8178879e1827d9 -size 44765 +oid sha256:d00b52b81391dc4770e6850dc30a20702081b4db3fa39a38d6a33d5ad7d914c6 +size 44038 diff --git a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_de.png b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_de.png index 481e6f0ce15..cd416194a70 100644 --- a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_de.png +++ b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd0de838207d7dd52f64e6020929dd7b35ea6a94790dd46279ff825b86a8d368 -size 44076 +oid sha256:45804afdb362b238baf689feabf6aee00e5c0cf766ab41941097df48114cabaa +size 43381 diff --git a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_de.png b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_de.png index 2915691783d..9149bb8ac98 100644 --- a/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_de.png +++ b/screenshots/de/features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9ba6f3abeba55d3b5ac7af9530416dd99cd857d9d490469ed4addc586fd1fcb -size 36598 +oid sha256:8d946653a9a52e8a92b0c88dce6fb0cf52950fb54fc5ea8687044426b8df0833 +size 35729 diff --git a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_de.png b/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_de.png deleted file mode 100644 index 71ca8749850..00000000000 --- a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13b9a41b5649f3b94a11570d0056f0c0efe1d6b6ec606b5ccd9373de34fb2999 -size 19916 diff --git a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_de.png b/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_de.png deleted file mode 100644 index ac0e8fe3892..00000000000 --- a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31922c2250e5f5c767aa24be1dba0a16e9ec135fb56670383de9badeb069056d -size 24304 diff --git a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_de.png b/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_de.png deleted file mode 100644 index a800a18ee4c..00000000000 --- a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9bba9b4a73d8a2ff98223d2054cc18c64b6b8c4064d09fd86b3a05d9535f956f -size 30026 diff --git a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_de.png b/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_de.png deleted file mode 100644 index e15d3745bbb..00000000000 --- a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cc604387773fc50481303c325a57813f354ea14724a2f0ea2d46f059ef9e5204 -size 9294 diff --git a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_de.png b/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_de.png deleted file mode 100644 index cb2b0d0502c..00000000000 --- a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b5da94309ef91e21e8058de89a135ab6efafa2c9c176df606f7cc62a9857122d -size 34612 diff --git a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_de.png b/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_de.png deleted file mode 100644 index f01a1fe8f3c..00000000000 --- a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:244eb76ef937bac78604faa38d8fbbbd1ce9ed7d51cf979f7533f7e23ef95471 -size 28531 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_de.png index 9a6ec6a07a5..2b58644d33c 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2c4d3664c29c0260da80d9e10784ee79f7697da67ef07b56a44385961bdddd0 -size 34061 +oid sha256:6e1b139382c574ef78bcedd2107378ee3e4e395a07bc888e11192bac270aafee +size 12284 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_de.png index 296d5e1fe3a..c7ee196df8e 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9050c23765ecd53f43848512031e9b49dfc07369cb107cbf35538bbfba80ed73 -size 34359 +oid sha256:c263b0b8fd4c6bdf53118aa5ed06ef919ae5973ed370580cd3d6290c455f9c39 +size 12524 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_de.png index 9894e670596..2b58644d33c 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:492892e916387121192876e1e90e8a2a8862f65179e166910e87cf7ba67a2ef5 -size 25745 +oid sha256:6e1b139382c574ef78bcedd2107378ee3e4e395a07bc888e11192bac270aafee +size 12284 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_0_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_0_de.png index 1abe5f31555..f0078197358 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:471fbb4c02418814037434bda896789e60b0fd7d0d63579b6e368eb1b57a1eb9 -size 47040 +oid sha256:0b8333deb905a5e1431597f59fe58b14591cb2fc424c80804fbb1ce340ce8bb8 +size 46941 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_1_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_1_de.png index c71d234d43f..c2342b8b0b9 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_1_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20f7a2b75af02a76ef6d3ca99e5babeca32e1a25de075385e0a7f0965d6dee80 -size 12708 +oid sha256:dbedf5fc4141f6aa00097a41da7ce5becde0f005b2424fd7f9eeba566da4e846 +size 47986 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_2_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_2_de.png index 86a2ae8a043..c7ee196df8e 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_2_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:545e31ee319083c052eb97fb54cac5e4f97ac3d199289bdb544dda908fc2e941 -size 14062 +oid sha256:c263b0b8fd4c6bdf53118aa5ed06ef919ae5973ed370580cd3d6290c455f9c39 +size 12524 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_3_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_3_de.png index c71d234d43f..4bdd05fb187 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_3_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20f7a2b75af02a76ef6d3ca99e5babeca32e1a25de075385e0a7f0965d6dee80 -size 12708 +oid sha256:1fc655569ee74f678cf7355bc2cd69349d119c5bee233604f4c11815766371e8 +size 13876 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_4_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_4_de.png index 7d0eda57cdd..c7ee196df8e 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_4_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c02ed04dd75085c6b80fd8a9360ef90a15a3169d9eab16973fff7b8db880ecd -size 7972 +oid sha256:c263b0b8fd4c6bdf53118aa5ed06ef919ae5973ed370580cd3d6290c455f9c39 +size 12524 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_5_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_5_de.png new file mode 100644 index 00000000000..d96220d9235 --- /dev/null +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:646f754dc9a02989008e469b2fbc8928bd12492c3de4b108c8a64508aae16ad2 +size 7970 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_6_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_6_de.png deleted file mode 100644 index b59e5e5e578..00000000000 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_6_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f602692433ac07c50562188383e876f463ad269de8d6f9df1e0a21c4afe885a0 -size 25504 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_7_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_7_de.png index 4d60fd07be2..52c1896d28c 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_7_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:851c6245825be513539c6be9b76f396a7490f03c7c03b6df12c056a177497524 -size 12344 +oid sha256:59a0d786c26f707d3db071f2845fd95b6762e33b37ebe9b9ab1b696d6c3b5452 +size 25504 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_8_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_8_de.png index 8055489688b..5e8d68e7ac2 100644 --- a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_8_de.png +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f36c4fc25cfc9ba31e564d5c0f5520f4f3b08eb7bf839d6ed35bc4e1e1c9898 -size 21857 +oid sha256:8342dae1c9b3769d867d856ecdc3b3302fa5cd94e80c6ad9ceddf482968305f1 +size 12343 diff --git a/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_9_de.png b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_9_de.png new file mode 100644 index 00000000000..9886eb1f07d --- /dev/null +++ b/screenshots/de/features.roomdetails.impl.members_RoomMemberListView_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f29deb95619350659f5c1932c72d06edb952c06bb894d2afec87282503ce266 +size 21647 diff --git a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_de.png b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_de.png index fa6e49238fc..f7863fc9cbb 100644 --- a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce1703ab421f8bb5ad331faadd3f4a04ad61d4f6cf3b5a241f2658945e4ad2e9 -size 35395 +oid sha256:bff8a7beb2b60b0cd089a84c514757d99bdc3eabd672d02fa47aabcb9260a4d6 +size 36108 diff --git a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_de.png b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_de.png index 413a74a7fb6..4a21c88bd00 100644 --- a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59bdccb73987b701b93fc097e06783cd244107b5dfd6076e5fe24a46ce1c2e7d -size 48537 +oid sha256:348e60ce0a5e1f0ae233ccd31e7737c383d8982726972c90f9f5bcf576b9dfde +size 44126 diff --git a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_de.png b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_de.png index f0842b4d789..4bdd14e23ba 100644 --- a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_de.png +++ b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8083186cfef27c2db79f04ff8b3f37f221b0e9f9537f816d6a1ee317b13d1fba -size 50140 +oid sha256:9691b46c25e47bf856aa3c6aff42746c732c2e74af40348d5904c32c07dbb2aa +size 46429 diff --git a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_de.png b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_de.png index 2b1558dcee1..20037ff02f7 100644 --- a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_de.png +++ b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf7d1f09bab959388076676f703e452bf38b897992cce533c65459dff6a84358 -size 41018 +oid sha256:596170a7b2c00ac6eb404480a1759a1906922684c04bc7c2f43792c7bb05ae53 +size 37312 diff --git a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_de.png b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_de.png index 0e3220cc19a..0179ead8fb7 100644 --- a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_de.png +++ b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c1990356bc445d80fded93320e5f9ec067d2cdf03730dd4e528af6655f9bf40 -size 42841 +oid sha256:f749d6fa0d163cf7375a7b541ce19972ab53d66d4c85e0aa19fd9ca74ecb6cf4 +size 39354 diff --git a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_de.png b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_de.png index 2b1558dcee1..20037ff02f7 100644 --- a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_de.png +++ b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf7d1f09bab959388076676f703e452bf38b897992cce533c65459dff6a84358 -size 41018 +oid sha256:596170a7b2c00ac6eb404480a1759a1906922684c04bc7c2f43792c7bb05ae53 +size 37312 diff --git a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_de.png b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_de.png index 0e3220cc19a..0179ead8fb7 100644 --- a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_de.png +++ b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c1990356bc445d80fded93320e5f9ec067d2cdf03730dd4e528af6655f9bf40 -size 42841 +oid sha256:f749d6fa0d163cf7375a7b541ce19972ab53d66d4c85e0aa19fd9ca74ecb6cf4 +size 39354 diff --git a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_de.png b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_de.png index 413a74a7fb6..4a21c88bd00 100644 --- a/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_de.png +++ b/screenshots/de/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59bdccb73987b701b93fc097e06783cd244107b5dfd6076e5fe24a46ce1c2e7d -size 48537 +oid sha256:348e60ce0a5e1f0ae233ccd31e7737c383d8982726972c90f9f5bcf576b9dfde +size 44126 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_de.png index 88f9bc02cc7..ed7482c50a1 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:821fffec529240bef83cb0230fd262fa8775d07fbf9775e5af4bced0597b7384 -size 16283 +oid sha256:5a2c8c098a7d8f23342f561de6ab7f6edacdbe4fe5eeec53428e19a2ff2dd078 +size 16089 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_de.png index f4da3cdc35a..71af31f332f 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2400bc1dd8fc0b1a8fe9b31724f28cbcdc26cf7824ae3ac49f289e9192fba5eb -size 51854 +oid sha256:133864d58cc04374073951dc02d58662260bdd9c2017a09f4049fc346ce3550b +size 51721 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_de.png index 0ceaad45679..1e4f2970134 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72a5fff0aa68f63d730611c91f9a83092bb9ff93c398e04c33f434e4bc55ef59 -size 73466 +oid sha256:6e964c116b3dd244021abbeb70e0a612e1d913b306f7d5aaab759f6fb101d086 +size 73293 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_de.png index 152d19c0d4f..6a4b90c0a79 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc88eb3057bb53e39f63b66d9f209ab1a0ff1baf981c708291c10d0924f12e3d -size 66529 +oid sha256:18fe56867c3d0efc4e17b2d08c195fd66ebb4f27df0d6ce567bf5c6133ae42b2 +size 66339 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_de.png index 9a3f586b9a8..cf912dd5cdb 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66e47c84c4eb0235a3f6b6e3525d0dd0b4df538f74a530042b9f07ee6632101f -size 66347 +oid sha256:d327354425d4fe637dfa63e2cc95931dc9f40a75ce88201248e069b06df29a4d +size 66150 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_de.png index b11456191e4..c285ad9b0a7 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f7137edd680e494c65683501a1b763c8ed60c19a0746ebc4f62204713945247 -size 61311 +oid sha256:b8de342bd67e9dddc743f8716da6c41dffa6e8c1d3fdc64bb092a3561170f71a +size 61148 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_de.png index e53ce02e59c..85dea672cd9 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd6de262127916fa937b2dd1630b0ca3a0b467a2cf3174b697835f7b05d03b86 +oid sha256:f7dcb912daf865e95d04a8932f89e05100c336e90d5b3f1b37b25833094d4294 size 14441 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_de.png index 58931853338..1f1e640898a 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b0c5481c868eb7dc61aca6b5f0232aa1bda2db9f144b2ec3c5b9af38a8d45d1 -size 64488 +oid sha256:988a20be1ec38f4c4b6360508f6f7d5155fe71adc9f0e7137b297e22cedb2455 +size 64545 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_de.png index b669e7a0dbe..600947a5a3a 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e20fcee247e6481b0a8572e62406a6ca1860c213d21c6fa0d5dac0c96fd31516 -size 65931 +oid sha256:9012d60a71a6058057c7877ebf596a85ecabe49601832f80ac9f5a89bd3eee12 +size 67048 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_de.png index b7f4ba7952d..ef70ed71645 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06cfe3f276744cdc2e062713a5f0740fb4f3183e25d9c6e774d03c54b4e0834f -size 55992 +oid sha256:d51626bc080f77c4cfd8166780a4cca6cfb6f587beab7c88cebf131d8d606db6 +size 55879 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_de.png index 2fa5bd13f9d..76a4d898a59 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8246376167a0ec50efc38d493304380a6c3b74d472d44525001a75f4ca6b5ead -size 68844 +oid sha256:1b2f7973608d2a6cf008240270f42621f6d90640df828f892777ac0b477d11b2 +size 68666 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_de.png index 58a80ad74d4..191c3606730 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cee74685fe9d76a5a1389f500fbcb22517ca092625ff0f033bd870713e053537 -size 15121 +oid sha256:dda7469b3c6a9940d7dc9e6c29a52707788af32ca4c17960eb0a29aab0eff4c7 +size 15134 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_de.png index ef2dabc3aed..37eecc0061a 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b66faf280b9fb10f1efaed0c3966e8347a3c7d48a9654abc6206ea72fab28fd -size 45212 +oid sha256:9715ce2c7051cb20ce078291357043c0f635fee1f9c814bf7e1b2a0ce92525f5 +size 45213 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_de.png index 4235ab07540..3b475feec83 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32e86e9252d92805d19cc06502e6c2e89964368f5c7f5d2e650482ea750003a2 -size 43951 +oid sha256:b9a3d19e2c1b9fa0c7589a2c6125b9c46342392381b2f08303131397562908d0 +size 43954 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_de.png index b210749c31e..73fcdc31383 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:627ee88bde325353004b53975ef770fcb21e3300e53ad057bd04de2335c0936c -size 51954 +oid sha256:a43eb0c9fce4145c42028e1bbf6e77961fb3a70afae22d28863abd2b6060a2fc +size 51952 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_de.png index cbc764c54b8..9b6a15a0731 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83a8d268fc9606232514f042ef24eef58e627e0d1fe19f31de690295f43dc590 -size 45115 +oid sha256:bc3b429ac5a2903871041f19b7328bdf9d4aa3ee40f2f3bf67e9aed5ef4bf623 +size 45117 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_de.png index e81f45f9173..33fdf473191 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cca87adff907ee6feb905fd4d920e110a10e527aa5f817da51d9e4d862f0841 -size 41774 +oid sha256:7da1d43fddbe35a0d6717bdd502bcb77cf565bdf65a41c7b74d0785168a93e10 +size 41771 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_de.png index 5dad76b83a7..bd8f2394635 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da27a51a86da8abf8d54c8ed697284737f0c4559496318274ee387404dc0bc96 -size 42992 +oid sha256:19ee0f3ea570cfe8788d01f2708d1a1105c3c53fbd502c6d195aa2a4070a657d +size 42989 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_de.png index 85e5e6f490f..2815f86a89d 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1839187595e2bef27c8ac84db515d0e88ccbb596b48e6a5b809c9f75fccf975d -size 52348 +oid sha256:4330932d8f3fdf2f5a7d078b3c64c496305fb7e64448b987fb7a6d5330ee6ec8 +size 52487 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_de.png index f566830b8dc..caafaf7ca61 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f085827fde896c6f5e1989848cd3e57ee205820b11d7f2b3b84e549e0994e183 -size 49579 +oid sha256:d28585e9ea9181479d9663f363f37fd0c13a9f9f588f2b0b6db7d719aef458a4 +size 49684 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_de.png index e12710cc3ba..4194794abb8 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8438d90317ab54f6383f5850f2933a367b632583e063f9f96ee35dc68d0d8381 -size 49283 +oid sha256:5a461db38dd589c5ee1ee941039d2eb18c057d2434ab0bfe783f1b1709d45aa5 +size 49352 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_de.png index 0fdf484df4a..597368e467d 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a87c123ddd3fc5ee55217df2f1425da7c4f7bde155e9dc3c0547584c6a8c8252 -size 63370 +oid sha256:f45396e7dae438b4a1b7cc4884bf2d1bcfe6e43d9ec1593a060bfec074b0cbcf +size 63438 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_de.png index 6b4561fa9bb..7253c239ffa 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85b8cc1dc241900961fa2a75561b21e1beb86fef54738d770b9f5f538569c268 -size 44682 +oid sha256:f83b333a0e6b8307899acda628be6d67ecfd747c69bc2fb4f4214e0a22d7edd8 +size 44797 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_de.png index 722e1f0b3a3..ff83b562f59 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0677ed85b17d228a2a2b8a1a5e22486bf84eec79f366dd2623e17476bc9edc9 -size 42881 +oid sha256:4c4cb56fe86cdd2eeacb75867669f3dc3fe3cb8671265fde7bde6f7deed1451a +size 42998 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_de.png index 5a1fd48b3f5..689500ca1ac 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bf1a4fe6f0531eb6cc43c8ab16f0c7b8f44f22947ebaba667c361c584a2df49 -size 56001 +oid sha256:ba623b0e9e168e86fb9be1af81323de754ee05f659ba627bd6e40178bf27f433 +size 56134 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_de.png index 6b4561fa9bb..7253c239ffa 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85b8cc1dc241900961fa2a75561b21e1beb86fef54738d770b9f5f538569c268 -size 44682 +oid sha256:f83b333a0e6b8307899acda628be6d67ecfd747c69bc2fb4f4214e0a22d7edd8 +size 44797 diff --git a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_de.png b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_de.png index 6a353d53af8..6acbbaba8ed 100644 --- a/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_de.png +++ b/screenshots/de/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da45f9250a73519ee1a9de98c2ad7e8a4dd4e53b15bb6b7b96e7f46387ee434b -size 42336 +oid sha256:0e36fa2831ff08a7406e60c7a99754e1f7d7d9e53469585ff37180fc056bb6d5 +size 42498 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_de.png index d7011f09706..2de39ff24c5 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c38f68735fa75e9815f1c2cb41830ce3a4257eb7074c506b41ef5105c4b75477 -size 37994 +oid sha256:1cb55e863d132250aae622e906dbf1662e8d2295984d251b7eb4aab79efddc39 +size 38001 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_de.png index 9e000d6b085..27a2ad65edd 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b75eb997c29863256ded8a4a980d206c4e64a3638ad9877d934373eaa456fdf6 -size 34141 +oid sha256:25c40c64b98657618e88308472602574ab9c9290d8b2ae65bf193b424eacfaf0 +size 34145 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_de.png index 72994deb643..bff2553eb90 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14d765a01eae72aac266ed43f76dc90d22b7266d7101d0ff0375e57659024b1d -size 47752 +oid sha256:4a28c191aabe13caaa2e86577c97f87280b31e9b94d1887308d5e72880235464 +size 51431 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_de.png index 7b08fa1778b..cac4a440b51 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9175dd8bae4d6b6e3dc083df268d54614be201dc892d092920b270159eb21a6f -size 70104 +oid sha256:727ba0ef7abe78aae4f299ebbc9a706c8a0a153ef21d2c15b76a7b434353176b +size 68510 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_de.png index c4bcab84d1b..ca7b50f2584 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25913f33e8fea06f17385015b5b1a88c98b34d99438baea85909c46d7b1b82a0 -size 70236 +oid sha256:3b257faefa1abd9dd521bc70d7539cd9d0837a317e3eb06f6f6a85147b12abf9 +size 68643 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_de.png index c9daaac280b..af3f1b35de1 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d85d68bc2dc825d6805623dbd193a5ef1b412a5cdf900441f01fcb2e318348 -size 68328 +oid sha256:825f00224a82b62796968c103be33b3daded002d5bd5367ee95a0535650ec32c +size 67971 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_de.png index 0cc055f73cb..22aaa594012 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:502681d6b4b59763d7d5a82297431bbaf1ce3d80f355d778df24ce627dfb8a7c -size 69739 +oid sha256:a9acb42aab5048e790bcb2041e0f2cdb18a721b706282b090b5432f7f32e4c51 +size 68168 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_de.png index bf80d60947d..1597d94b3f9 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2949e189567007b37f6c81e281a8669318db77f3d02457efb544905a010b5354 -size 47515 +oid sha256:5e3c39fd5676d85079371ecdf28f8f2351e6a998e7ea0658b5bc20f84c81b313 +size 51192 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_de.png index bf80d60947d..1597d94b3f9 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2949e189567007b37f6c81e281a8669318db77f3d02457efb544905a010b5354 -size 47515 +oid sha256:5e3c39fd5676d85079371ecdf28f8f2351e6a998e7ea0658b5bc20f84c81b313 +size 51192 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_de.png index 26a397618f9..16f51ec2361 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12cd5e58293045f53097220dabe12cc112b58e048baace0fe16441d882fdb3b6 -size 50175 +oid sha256:897a788e5ef08a0b2a52b3e8066022635e297048d3f23e5aaeb38bd3c366a628 +size 54065 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_de.png index 0a0c20b3ef6..fa8822689fb 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fb0f53a1226c45749c3c5a610f6b61bc2f32735cd8a8191dba789be6bc233ef -size 41812 +oid sha256:a20e2f48614f64654b0dfdf4fb9183afcf8bd901375f209b688655b760408ae0 +size 45243 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_de.png index 76af4e60a95..8d7e6b9c73a 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87fe0c72b32ec3763e55e006ef2e8cd4fa136fe51909c1e2a014acbdf67f5acb -size 49984 +oid sha256:14768120dc544c2edd64d17fe169e5f9e361c007b61417f30be7a639c06f2a4f +size 53552 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_de.png index f0cddc93c07..0f2dcbf0efd 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20f6f802c0efde55dede378f3931ba91954a343b85e20622254edb9536eb8520 -size 72749 +oid sha256:e25521eed0cdfbe79f59aae942e73527fe7795eee2fa9744422932e87fb63d79 +size 71175 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_de.png index 5d7c7fa59fd..5e953073364 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c86e60565c0c48536af6eb9335c6fbf8787338f40942873c6e9761546ae69fe -size 72980 +oid sha256:dc7e4d24dace1b44700c07178fb048fe033f4ad0c10de06cea19ffdbe8d0092a +size 71400 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_de.png index a7601a169fe..55073ed0ad0 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44c111edf5a396ca4f8e2ec330a4ad911a74c031ff338936c629271831e8c4f6 -size 70624 +oid sha256:31dd38ef2609fce774cbbc606ce91c10c735bde3ded73b9bb1643db08ea558cb +size 70349 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_de.png index 4d3fed3a267..1706bc1cce9 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7771b32497709c40670f60d5fd8f7d8690ced4970944780f9ac300fd616aff2e -size 72328 +oid sha256:276f6a59cc9a99bc123b7363e1acd1c88b6998697180ff9a68644f4c0b824516 +size 70759 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_de.png index 783d558a72d..bededdd9922 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a5fac2cbe4257347412c281d4bee85ecce39045630a20516290dc120a75eb96 -size 49835 +oid sha256:5830dbf1d4aed28c4614473026820f6803c5b1a9f7a53398fac78dbafa7f4342 +size 53386 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_de.png index 783d558a72d..bededdd9922 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a5fac2cbe4257347412c281d4bee85ecce39045630a20516290dc120a75eb96 -size 49835 +oid sha256:5830dbf1d4aed28c4614473026820f6803c5b1a9f7a53398fac78dbafa7f4342 +size 53386 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_de.png index cfca1deb8f6..c8c0723e74e 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33974316edc62a80fa1279aebaf875449b721826c0696f0f280c7f966195dfa5 -size 52473 +oid sha256:c8194be0e2575a8a0851c9661aef416bf9020feaf9e6fb1218ecee6597cb6db7 +size 56628 diff --git a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_de.png b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_de.png index 3382031c79e..3d21fdc805f 100644 --- a/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_de.png +++ b/screenshots/de/features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:308c12cb5c1db09b7cb0ae42504b9f5df790f03e142ff023480451adeaa2f102 -size 43148 +oid sha256:80c3256f801119397d9665c584234d48bf8ef6b20f13967f90f8d031a2c363b4 +size 46742 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png index 957a508864b..0d29f5f0172 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:38a82eb6a9566462b70435d47d8f77f2e40fdd9cd80de2b1862ebb7a6ba1c509 -size 45864 +oid sha256:ca8ec24b1775b2e2d65e4bf31c9d832243cf450ab8bf0e275c958f55d891a3ae +size 45915 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png index 83cb5cdb4de..ac8f3c8942c 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:fe8b8053c5d318944ca032ba80c8573c9de641eb28ea30fc5ac86571b8cfa34e -size 44723 +oid sha256:7f4f16ee1872417ecde6933ed01db60513b02bd7b6126652633438d8d6ddab53 +size 44794 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png index 5572e330c7f..2f1eab1256b 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:65fc6ca99e1c0e42e0834075e3466821d24389b7152c103cdd579012ca33dc47 -size 43199 +oid sha256:70b48f288a251760efc84dc0a9caf9315cc1ef65ec5d7fd22df04b81e4c7156a +size 43211 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png index 635f1fddb80..476f83cdfe4 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:b5f500b02e56c85d0ce15074262320b199cf1b197b97c569ef7a8a14ebeedafb -size 46987 +oid sha256:a8bf7a762a9c26e2e089e8bea43dca197ba2301534a110b73e9d848c350b17ca +size 47054 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png index ceecc2a81c3..7eb6c50bddf 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:f4bfe9a8967ced4459c1aaa651da8cce5c577eb0277f9383b337df7cffdd790f -size 45103 +oid sha256:f629c7b6edec8f093f330b92eaa4738ee6917aed10b18a87741ae809e4770948 +size 45146 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png index 7d1c6596de7..2aebacfe9ea 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:3ddecfbdaa31864cca067a81e1a277e94ffca245fbfaac8a8aa791f841564903 -size 45609 +oid sha256:be31534fa702a2894fd2216f28dc77b7ba5fb6a1564bda2c30eb6c6d3e904cf9 +size 45655 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png index 50193a8e6e7..6d022fa45a5 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:7919cdd7467ee63a1cdc30e1cea69ae93cc1a68c26b085bb7cbc1711fc86a4cf -size 46185 +oid sha256:5edd8ce73500098587c532e8889baca2c0072cf4194303cb0ddd65e7d26ff398 +size 46184 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png new file mode 100644 index 00000000000..2ece4cebd6b --- /dev/null +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70b116284cf877004b78e26381e7dd9b581c87898d27a7dc8f5fa7f511e88964 +size 45475 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png new file mode 100644 index 00000000000..4fed40e2ee4 --- /dev/null +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52191d77cc9fb34885078c1861eeb25364eacb3d65e0e25b4ca2ebf2a02d3045 +size 44705 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png new file mode 100644 index 00000000000..a06d8b5f9fd --- /dev/null +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:400de13e3e446bc18b1a5d7819fd4b874ffa4bcbc5b80684d84c203b7f5fb897 +size 41836 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png new file mode 100644 index 00000000000..cce1b0aa7ab --- /dev/null +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1de394c34446754f68f028a11fb4112aa56dbbbc69d8d4d5f8f5f48606bd011c +size 41792 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png index af8edbf499e..af0713f1590 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:53e80f5e14bbb19780f7638bdb230e75080ebcaf88f9af0e04ca422cbc192b88 -size 36310 +oid sha256:567249653f7aad689badeed227f1fb1844151dbe7e839b2ff79ead5ddd5f91a4 +size 42060 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png index 4b9249e8168..ca68f35bac8 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:783f4f1891a23e82dbc0099d2de44688d8ccd720e6b69613dd46074a57721f57 -size 38887 +oid sha256:037a9a1db806c3ef0fdb99fecad1c5ce48e47eea02a34872ed47cd20145fba4e +size 39780 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png index 06f84083919..876238748ae 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:451407d4065b52881ecb3be9be31a20e12bf4cc625bba97c381cb16af312b4da -size 45320 +oid sha256:3706e4e1cee1bb0cc01aedbe28a24670acdbd2d9c9ed710eb28f4c7cf194cb3e +size 45410 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png index efccf56fbf2..442ef303691 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:9ecdd948e5d99005023274f316dae6efa5354804d3a703de078d425f054442de -size 44285 +oid sha256:aab3abf7dbad2621907481b01b87aabf0f0840579a3cf0983728975ec96d1333 +size 45007 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png index 744bfc05e61..4284e4a80b5 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:332f4818fffd8a4ff0b967bb21500e5afc0d2b32d42c1cddb2566389ce1764c7 -size 42582 +oid sha256:4282d57b82f7e5077580d1571bf3af69e24252a97b317cab88a01e780bab3fad +size 41477 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png index a9bf3ea533f..ee40a011ea0 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:c2ac16417f6f6c31b64f0a769369309e730214a335d82a12b099644a7fa2a5cc -size 46431 +oid sha256:1355053d6adeff0e3d14e32ffbaaea37dd7ec8f77d59aedbd6183230d175696c +size 45148 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png index 3a4987f3034..e6eab41fe06 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:4332ee6d707da6c6434e42fd6a74cd07ac8071bb29088461171bf3dfefb81daf -size 46265 +oid sha256:2ab35dca344a870da451ae6c082095531dfa4eac753d22b7283f0e6a0c12a3e2 +size 46377 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png index 291ded18eda..71d91f126ec 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:a0ca5bb933055850addf6ba405aa2d1a6d3b8148e00d0f63111f62689c67046c -size 45098 +oid sha256:7d5017df108eb110757e3bc19175ee0cd04418ef9643cf363b9909e5eb7ac741 +size 45169 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png index b4e355365e0..0c065e43ac4 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:3ae24330ed400f70d71270a2714126902e19b4faad2b7a5103d5eb97e424975b -size 43847 +oid sha256:cd400e4d9e78af0102246ff534d9de19f82aa0a086f38a7f17fce223189bf2b9 +size 44217 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png index ecd4677e263..802111b30ed 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:e29db8866ca91240d9473057e1a4953be8a8dd35c4a230b50b6691b417f6ad62 -size 47091 +oid sha256:3cb3b25bd7ef8a02f0210d83d507014a692c53e8508599f848cebfbf5ed5a6a0 +size 46993 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png index 24fa190ac93..e113c648450 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:d8ad9428813de263681957fe4a70fe3459f4d767327646930d76d25a8eb8fd25 -size 45893 +oid sha256:d09627b117addc28886870355099a343379877442d58a2a958aee0bfb83fbdf6 +size 45834 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png index 40c6b2c3fad..81dc586be1e 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:702de1c76e046154df9aab2d14c5dbfe5a60732a81595de88f46355a6e2addf6 -size 44323 +oid sha256:8d5a9bb03221a836e84604d9c0c59915a46f8bbcbe89e073998a31256bd6c0db +size 44299 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png index fc6daac549f..8a2971802e9 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:74d85b696a0726010320893d2f093dea46989b8603c0d8a9ba42c74efb15dfa9 -size 47721 +oid sha256:44f4df753891993e28e8292b060d03adc25f71d727a5c56a94ffe5fc41ae85a4 +size 47658 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png index ea6a22847f0..e75133fa27b 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:ff5c3c0fd6a970800540deebf447468ecfaa417dbb1b51f0f823e165414fc39f -size 46257 +oid sha256:b534a40e67c218e254d6677459a9b3ea1c6ae855c97e7b41aaf72da14b76e22b +size 46159 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png index e839cbea4a5..d6644f26ba9 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:3a4389ff6ec3701dab89cb1e0a9516fc1d5f9e2dbdb3768db93458f50792b734 -size 46745 +oid sha256:0f2dbc96fc7398f92ddabb0a316f1cfb7cca6941b891d669367311c682d67eae +size 46661 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png index 67382c6b983..b1e316aa94e 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:ef989f6d00c6fefcf1b4f0783a608370fb6a19539f46946981a983d65bdc62de -size 47338 +oid sha256:6ed807652ec6311e514c37ca76d4ab496760396da45e9b165838470b899a6a19 +size 47255 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png new file mode 100644 index 00000000000..dfc67208ec4 --- /dev/null +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d18f053ea3ff393891f510bbd5ee3e4c9a181ba735c92f14ed8de9d39c5c0aa +size 46506 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png new file mode 100644 index 00000000000..ec64a5d2a1c --- /dev/null +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be8d3f7dbdc083db4c7698bd8579a33b99fc15e5fae1076a8f8b06b8fb906afd +size 45981 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png new file mode 100644 index 00000000000..9eb485f2775 --- /dev/null +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:422f724e69ca057e5e3031f73bae86fad2fa533af748b930f836c430079c20b0 +size 42861 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png new file mode 100644 index 00000000000..dfc9a435000 --- /dev/null +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f180e16ec70466bcd5b03bc067ce5a017ea2f345776aa38b8b2a54ed534f1cc5 +size 42739 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png index 85e42ca9435..e6eaca702ab 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:67d8b1efb0248259068d0d185e31f9284c31fa794f156c84dc13398395e901a2 -size 37394 +oid sha256:100f6e208e22a312b1ce1009102d5b8d1891d28d0e326e514334e3b53849a65d +size 43192 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png index 90d10762603..ceb248fec62 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:954722447cce64ee74b45840c5646308138f135f0e3b4528bbcf25a59fb1a520 -size 40092 +oid sha256:3470657e3f74d9069e5a25334f694b3d46cffe2a3e4e0e5fee053a4632bf6802 +size 40904 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png index 0d3c4258962..b1fb65bd140 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:3e58e12b3ffb5a3424a4d042e4e8a0d9fe2a8635c45a185bdfded13384559ec1 -size 46459 +oid sha256:77782c8df1325ffb6da59fa963a8d77345b4f88669aead0aa722028d39eade45 +size 46360 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png index 2676f2cedf5..182e5470db0 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:767762a5af014ac84cf43ba04e2e7f4bb6d296ec897dcf3c8fe48b6afd20fdc6 -size 45417 +oid sha256:b396844a2de1712a6a49746861f8d2840a485fc18b47a45b3d0ac53dfe727b8a +size 46022 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png index 43986a46d7e..b3d645e87db 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:470b4e8fe4ab17e9498481bda5775fa2a21544ce78d9cea778180ac5aa5a33a5 -size 43624 +oid sha256:1dcda0c7c3b5cf60787034b2a4d77a466d49680863896a9a0b50e7714b8c9efd +size 42437 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png index cd335bf225c..92e7a851fe0 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:4ee9ea60ea3b0cb387daed3d3f1f46a85d37ae8adc3c63af4948d1db65e9a83f -size 47776 +oid sha256:b7d00bd3a98e05dd7e73d66bcff21e9da7179009fab1ccbb892a979acc608764 +size 46218 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png index 76179ad5034..cb093979cdf 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:b30025b2b98d507a4a2f9a7036af9b63883b737674d4bfda2c2d0314fdacdd35 -size 47518 +oid sha256:fbf5bbd6b81babd0658dece6801f37440678a69bdaec9b227a37b12fe94c02d4 +size 47485 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png index 47a6ef8ecb5..a910e248d8e 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:706fc28342b4c334435cd221e166aa0b756e9263ce801e4f2e64f62f18bd731c -size 46317 +oid sha256:6a02074e2ba8d9fba8f89f5dca0a52bcfd8fcaca70b31038dcf7856135113416 +size 46256 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png index 7c80c2f792d..4fd471f5a2f 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:505d0a8b15753750aaf81d45d88a78972574d6aca5e689c3e152074a122417e0 -size 44942 +oid sha256:001e2d6a6428e72e1b1f62927a36701dfceeec0a743ac96c9e52cd1c93b5106c +size 45159 diff --git a/screenshots/de/features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_de.png b/screenshots/de/features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_de.png deleted file mode 100644 index 91a14d58496..00000000000 --- a/screenshots/de/features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8f1916b9375c21a24a5b65df8664a82bec527ecb130ae74db94f44fd05061090 -size 40959 diff --git a/screenshots/de/features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_de.png b/screenshots/de/features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_de.png deleted file mode 100644 index b112134e281..00000000000 --- a/screenshots/de/features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1cba3a9ab954ec6ab1c6f8141d4bf701d638ef26c5c03fc99ed2e12daa422528 -size 40670 diff --git a/screenshots/de/features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_de.png b/screenshots/de/features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_de.png deleted file mode 100644 index a8dac1b680d..00000000000 --- a/screenshots/de/features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2b8fc03bb03b0296d2bfde1316c65d7ba1e7c22ffe537fe0f875a18df4222e0f -size 33216 diff --git a/screenshots/de/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_de.png b/screenshots/de/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_de.png deleted file mode 100644 index 62e6527c72d..00000000000 --- a/screenshots/de/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:51194d958dc599d2dfb518df716b1a1cd5839a79630ca656ab4ff0dc81fadf98 -size 46332 diff --git a/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_1_de.png b/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_1_de.png deleted file mode 100644 index f6b9c4fb310..00000000000 --- a/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0a1b94bca7c1f2ed158fc7faef8f7cf4cd8d1b380af1a7a604a47e40dc06ce5 -size 19860 diff --git a/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_3_de.png b/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_3_de.png deleted file mode 100644 index 094b195c6fd..00000000000 --- a/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2febd239295c0b31c8e4c1fc8d0eba2b556b5d3db01baa2a31968ac564d41883 -size 21343 diff --git a/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_4_de.png b/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_4_de.png deleted file mode 100644 index 46ae9107b2f..00000000000 --- a/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc1ef0475cd643a4b0ddf9fd44e3895491f1ec99924d113880eb574b1fb2130f -size 55622 diff --git a/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_5_de.png b/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_5_de.png deleted file mode 100644 index 6c09a6c3625..00000000000 --- a/screenshots/de/features.roomlist.impl.components_RoomListContentView_Day_5_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:029dd3d61183058f618b890d244fd6092db09e10dd06c7e4764d03b3abf6a860 -size 83409 diff --git a/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_29_de.png b/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_29_de.png deleted file mode 100644 index e5a2899c680..00000000000 --- a/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_29_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0774bcd58a29a30201977cc738c7635637b20a633ec90b3add6dd63b2a02325c -size 25403 diff --git a/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_30_de.png b/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_30_de.png deleted file mode 100644 index 44479cb1750..00000000000 --- a/screenshots/de/features.roomlist.impl.components_RoomSummaryRow_Day_30_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5381f248d6731dd75627bd74df015af82f4cc0d15cfa9a84571ad2c83ee8d436 -size 16803 diff --git a/screenshots/de/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png b/screenshots/de/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png deleted file mode 100644 index d4ce6f782d1..00000000000 --- a/screenshots/de/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6e1d5c3d46e4ec85f10a7c5b9ab5554d7b55f6736e770b95fa69e41b6e5e76d0 -size 39043 diff --git a/screenshots/de/features.roomlist.impl.filters_RoomListFiltersView_Day_0_de.png b/screenshots/de/features.roomlist.impl.filters_RoomListFiltersView_Day_0_de.png deleted file mode 100644 index 167f23b1c83..00000000000 --- a/screenshots/de/features.roomlist.impl.filters_RoomListFiltersView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba750821df2c8a7b7fddf7fcccc5ce5fcc42c5eceb2fc39cfe4835af1840f5cb -size 14426 diff --git a/screenshots/de/features.roomlist.impl.search_RoomListSearchContent_Day_1_de.png b/screenshots/de/features.roomlist.impl.search_RoomListSearchContent_Day_1_de.png deleted file mode 100644 index c3bf8d11559..00000000000 --- a/screenshots/de/features.roomlist.impl.search_RoomListSearchContent_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0c8cff05bbdbfb24c265ebb1aa38af384f54dfa764da0471e14d01c643a90a69 -size 20133 diff --git a/screenshots/de/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_de.png b/screenshots/de/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_de.png deleted file mode 100644 index eff2d04e64e..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4ae2ee18bb34d37b4e67a7613733c37732eb9f14104ff0e21651acdb33105660 -size 20397 diff --git a/screenshots/de/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_de.png b/screenshots/de/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_de.png deleted file mode 100644 index 0b9d9da00c1..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eca1fb0321c9576b7e2e530623f13f77465b29806a73cd799fb36e7790e4429e -size 22378 diff --git a/screenshots/de/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_de.png b/screenshots/de/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_de.png deleted file mode 100644 index 1c62a377340..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7fec74487e84a10c929c9f6feb4ea169316f50c1603a37df1624fb0b2d07de9f -size 22725 diff --git a/screenshots/de/features.roomlist.impl_RoomListView_Day_0_de.png b/screenshots/de/features.roomlist.impl_RoomListView_Day_0_de.png deleted file mode 100644 index ecfa87f5f1f..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6f2085b15d13c4196e092df35bd2fd1c6f8ec9b806ada18660afe9087a42c15a -size 84721 diff --git a/screenshots/de/features.roomlist.impl_RoomListView_Day_10_de.png b/screenshots/de/features.roomlist.impl_RoomListView_Day_10_de.png deleted file mode 100644 index 412dbaaed35..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListView_Day_10_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0fc7c88fb07642a471169c901f94b122730f8245fc5ea851e284ced2e123f839 -size 109493 diff --git a/screenshots/de/features.roomlist.impl_RoomListView_Day_1_de.png b/screenshots/de/features.roomlist.impl_RoomListView_Day_1_de.png deleted file mode 100644 index ecfa87f5f1f..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6f2085b15d13c4196e092df35bd2fd1c6f8ec9b806ada18660afe9087a42c15a -size 84721 diff --git a/screenshots/de/features.roomlist.impl_RoomListView_Day_2_de.png b/screenshots/de/features.roomlist.impl_RoomListView_Day_2_de.png deleted file mode 100644 index ad8b3213a93..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListView_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6c76b32365cafc9e68eca88f0f9f0ce4ec1c290cd1bc248bd02f1ae813462906 -size 84468 diff --git a/screenshots/de/features.roomlist.impl_RoomListView_Day_3_de.png b/screenshots/de/features.roomlist.impl_RoomListView_Day_3_de.png deleted file mode 100644 index 56f928ce8dc..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2f1f2b97e998afdb21478e37509a129c88e0c07d337fd421d7d010baa3e62672 -size 65836 diff --git a/screenshots/de/features.roomlist.impl_RoomListView_Day_4_de.png b/screenshots/de/features.roomlist.impl_RoomListView_Day_4_de.png deleted file mode 100644 index 7226a27fcfd..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListView_Day_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a6ee5448448d0b40c9a4fc7446da20285f3f73f065356ac9cbebb2633c9856e4 -size 65203 diff --git a/screenshots/de/features.roomlist.impl_RoomListView_Day_5_de.png b/screenshots/de/features.roomlist.impl_RoomListView_Day_5_de.png deleted file mode 100644 index ee5d53a6908..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListView_Day_5_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca5dc05932c3c094060402e27aef229cd520a5a07bace84a2157b15d77217307 -size 63404 diff --git a/screenshots/de/features.roomlist.impl_RoomListView_Day_6_de.png b/screenshots/de/features.roomlist.impl_RoomListView_Day_6_de.png deleted file mode 100644 index 8b197b8e04d..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListView_Day_6_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:471a42c11b92d727556f99c96a84129e2091261a457068ba02b55b59b0ebd886 -size 106721 diff --git a/screenshots/de/features.roomlist.impl_RoomListView_Day_7_de.png b/screenshots/de/features.roomlist.impl_RoomListView_Day_7_de.png deleted file mode 100644 index 310e7c70f6c..00000000000 --- a/screenshots/de/features.roomlist.impl_RoomListView_Day_7_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:568514f2eb3a842032d3564554cf609c3028b174d80cc28d10116d6c45cdd774 -size 49717 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_de.png new file mode 100644 index 00000000000..5cab90546a1 --- /dev/null +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8edf8f2d1b29db9dfe47756dcd3fcc01fc908736b0c009320cf8bac535881416 +size 18252 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_de.png new file mode 100644 index 00000000000..8596d7d68c2 --- /dev/null +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7203569b41833fe19935273838d59086031b10660f2e12ab5982432e867e5c1c +size 22128 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_de.png new file mode 100644 index 00000000000..c3d7c681a1f --- /dev/null +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec50fa44bf08be308a39eb0d607803b079875c12dfd608f2a70ea4b53e164c39 +size 27566 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png new file mode 100644 index 00000000000..f2b320ec01e --- /dev/null +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a577c026a1502e2fe3bef1ff5f39e3857c06a0ab314c2ef4c384bceb7b9088b1 +size 28630 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png new file mode 100644 index 00000000000..b11c62696e3 --- /dev/null +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c302540a09bcaa5db3296b3825f3086e593a61d5cd2b4c834f23fc6827b91325 +size 34422 diff --git a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_de.png similarity index 100% rename from screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_de.png rename to screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_de.png diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png new file mode 100644 index 00000000000..58218ad8238 --- /dev/null +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fd3f5cc0b71a34bd10bd9ceffe1f6cfe7f266b0b82fefd93c18df78135ab213 +size 34551 diff --git a/screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_de.png similarity index 100% rename from screenshots/de/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_de.png rename to screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_de.png 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 6735fc5a86b..f2f91815ce7 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:f1b122ef1923016968db25815391af5bb372068f554d092cce520a46bfdcf77e -size 72958 +oid sha256:01b5e7fd76bd27d28420d6dcae61cc6381a9240e3bca78f4b1191a015eb395e2 +size 73458 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 6735fc5a86b..f2f91815ce7 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:f1b122ef1923016968db25815391af5bb372068f554d092cce520a46bfdcf77e -size 72958 +oid sha256:01b5e7fd76bd27d28420d6dcae61cc6381a9240e3bca78f4b1191a015eb395e2 +size 73458 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 5e332619bab..24f0ee5f73c 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:695b215f1e215b93cf82f64814b21162cb472549a375ccf493d98663b1915bf0 -size 73482 +oid sha256:362fa3a6e8da97f67c0c3502be9d53929cf2cec1f7bd6c15ccaa95070d65b24e +size 74006 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 40dc778980a..bd4e5a26b32 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:e90121c90b84c9c56ec18e42f293fa2dc8ecc7963d83c77829f428e9808d4491 -size 43073 +oid sha256:e80fa798011e89eb0cd931dbd384b7c800120dd2df8b2227d6e5d8ec93ef238f +size 46816 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 d25e0de53a2..a3f7eca10f9 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:0e49141bdd0efb68227de5957cbc973e10552e4596786d4687a64256aadc82a1 -size 44720 +oid sha256:af5b6edbc4ee55c99c8aff0cb3a8b99f6c48433697a2b071b36b820a8c0b89ac +size 44700 diff --git a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_de.png b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_de.png index e1b8666a473..bfdd53ce501 100644 --- a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ff26d8fad93fd5c6c0abb70911bae92974ab1977f46104cef3babee23519811 -size 54013 +oid sha256:03ec094b991196ae554829edf0f81397a9ad8baaac9bb06b6a88a17710e5007b +size 54011 diff --git a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_de.png b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_de.png index 6c9464c6b90..ff8ec58db75 100644 --- a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_de.png +++ b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:527c0361bb776783a7b776e5a6ecd61c84868b26aa9fe0da96f665ef17674bfa -size 54078 +oid sha256:b62106916621aa500c296a921a43d3567739166d6a4050fee3b2246dc8b5a47c +size 54120 diff --git a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_de.png b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_de.png index 8ba74813991..474fd363e97 100644 --- a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_de.png +++ b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07963d63250d08ab2fd71b9a4483ed443f54801657e1f0033e782d51721c16df -size 51484 +oid sha256:ef64c212baf51a9e0a96293c549feba2604bf153457b7aded2b19ada306d26a2 +size 51423 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 1328b1ef4d7..1eebe02c061 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:37349901ece92abe0270a8e0215228a4b0264e4b7dbc4eeb84c70d95d9bf3b8f -size 30648 +oid sha256:04f58df72ebdcb770b9b9fe6dd003c0e63d49b2b8db532312ffa5b2a1701b3f2 +size 31116 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 9ca131aad61..26afe9811a2 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:ca8b644688135b0373307b3ff366a279e392de04b52241327f9be4209c2a72cb -size 29686 +oid sha256:31428c1b7058a831ac4ccfdd30425bb459f7d09eca08107656ed18fe1496f1c7 +size 29965 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 9ca131aad61..26afe9811a2 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:ca8b644688135b0373307b3ff366a279e392de04b52241327f9be4209c2a72cb -size 29686 +oid sha256:31428c1b7058a831ac4ccfdd30425bb459f7d09eca08107656ed18fe1496f1c7 +size 29965 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 716cbe05b0f..0540d995d8a 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:41f22d7dc92f856b780a5c11f1d31c982131a5bbfdd85562eaa9866cfd611282 -size 44582 +oid sha256:ebab59af812c6a4cea61ecf7c73b835c9f47837c84b4e702db5aa6887e5980e3 +size 45092 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 19c723089f7..37a6a0a0e9c 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:82c0bcdb555e40c173bb194c3139697cb3b3e2b4785ee7ee64fe03cf774da733 -size 80628 +oid sha256:54862a355da93704e0e6eb779d6a6011296ca2a968117d36cc4eae8fd96173ac +size 69526 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 d6609425764..a3886a1e3d5 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:18c850f1eedb2fed3d2b34e5c69cab8a1728f33822c087c3c2dcec062032a45a -size 58554 +oid sha256:f953a19a979cdee1503766531545b4683bc8b672a26903af8283820768e4a637 +size 53428 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 8670a3339f6..e21a6c0b86c 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:953284ce01229f35b10299ed63200cbb3f43d85cc9a44508651f79696feb3883 -size 68248 +oid sha256:07bbdf3556e48016e4dc011b603ee9361f9488f375b76c9e3fc58d9cbcaaf8d9 +size 68285 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 c5633cd09ba..009c89fd9a3 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:b431982e400c16c24b8e97ab4bcc70c8c5af25f929a2f7ae2679c32483cb142d -size 47347 +oid sha256:a7a354cd460888edd65a2185602d967314c620a2b3092d7c3eadc6e203d5cb13 +size 47214 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 10736488df4..7862c07b34d 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:2430051d6900a991fcc0247ddd1416a1de111b9115b5896b174e96b24f0a5967 -size 57439 +oid sha256:c41db568ef86660c72181c40c9394031d5d12f5e794ebc513869991b37a7d957 +size 51836 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 5b9f55257ea..9c83d065f0a 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:9672ee64b289f62eca96a707d90c452ce6abefc27a17bfd07b5ed26f26e14d19 -size 53848 +oid sha256:a06c9f9245cf3b02229fcb838976a4d2272d44e40338e36e026dea7677e70ac9 +size 48416 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_de.png index 3fee8764381..933993102ab 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4329c805593e10c00b495a8f6c61c3ffcbf7bef4620180a6e888751ba7d2e89c -size 65577 +oid sha256:a3766ba621a245967c15b7b83fa72bd9e4d7546a37f5ea4caf83c6788a1aa5b6 +size 61181 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_de.png index 3fee8764381..933993102ab 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4329c805593e10c00b495a8f6c61c3ffcbf7bef4620180a6e888751ba7d2e89c -size 65577 +oid sha256:a3766ba621a245967c15b7b83fa72bd9e4d7546a37f5ea4caf83c6788a1aa5b6 +size 61181 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_de.png index 35cd52488f9..45fee6dd265 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28e01a8624d4c4635dbfc4648e4e77b2b9e7b5cd5d5548f678196756c50ddcd5 -size 55067 +oid sha256:2fe92f15ed85ca629e56dad8c30487aee018a51fc4e9eaf6eab29a2eb4c27d42 +size 51695 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 95968672c85..ac9e47eac1d 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:e80ab49387cb88152d3dfb4d894ee895550e6da51f9466188f59f509f0dcccc3 -size 62501 +oid sha256:f4915dcc73c8761e491613c91542f65ea0e549bac1852b10770b9087b1f90a48 +size 61415 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 ceda9720484..6f657e67f86 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:b05ec0fb67463ccbbfd7c9c7efb177d090ad18a922c641a060d90b2a55192580 -size 59169 +oid sha256:e1fe049a477258416fb2964230845ebf009e35d0a7baabefa624842cf1e8ff89 +size 57768 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_de.png index 3fee8764381..933993102ab 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4329c805593e10c00b495a8f6c61c3ffcbf7bef4620180a6e888751ba7d2e89c -size 65577 +oid sha256:a3766ba621a245967c15b7b83fa72bd9e4d7546a37f5ea4caf83c6788a1aa5b6 +size 61181 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_de.png index 3fee8764381..933993102ab 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4329c805593e10c00b495a8f6c61c3ffcbf7bef4620180a6e888751ba7d2e89c -size 65577 +oid sha256:a3766ba621a245967c15b7b83fa72bd9e4d7546a37f5ea4caf83c6788a1aa5b6 +size 61181 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_de.png index 35cd52488f9..45fee6dd265 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28e01a8624d4c4635dbfc4648e4e77b2b9e7b5cd5d5548f678196756c50ddcd5 -size 55067 +oid sha256:2fe92f15ed85ca629e56dad8c30487aee018a51fc4e9eaf6eab29a2eb4c27d42 +size 51695 diff --git a/screenshots/de/features.signedout.impl_SignedOutView_Day_0_de.png b/screenshots/de/features.signedout.impl_SignedOutView_Day_0_de.png index c8e8418f3d0..9c6f85a915d 100644 --- a/screenshots/de/features.signedout.impl_SignedOutView_Day_0_de.png +++ b/screenshots/de/features.signedout.impl_SignedOutView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0107cb27d8b29149357e62bfd1c3e8f307ac326d41144f9722fbf7d946fdef8 -size 61475 +oid sha256:5e794da71cadd6f1e9f0662a359325c9cae1e8ebc9877537a1fdba6d1c006ce6 +size 63399 diff --git a/screenshots/de/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_de.png b/screenshots/de/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_de.png new file mode 100644 index 00000000000..0347f23cbed --- /dev/null +++ b/screenshots/de/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fd007d20fc02d44d2230aa5471c2b7242cc3aff862025517a11a3ec2b8acfb8 +size 25057 diff --git a/screenshots/de/features.userprofile.shared_UserProfileHeaderSection_Day_0_de.png b/screenshots/de/features.userprofile.shared_UserProfileHeaderSection_Day_0_de.png index 97877dcb479..2fe608431bd 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileHeaderSection_Day_0_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileHeaderSection_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3a07ae4167cf29cfe165d3acc04e64b0c614d81bc542d57ed1d7a3b68605342 -size 15442 +oid sha256:d900e961850cc8dc3ceefc1f7d7a9296e6737a0e6b92ddc5e7223a42df7ddf41 +size 15312 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_0_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_0_de.png index f13d3331493..9b265262bc0 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_0_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:116f716ad3bc6b519afbc3f6fcd5b019ef61e2846f4c20d516c68bbdce819895 -size 34316 +oid sha256:c306232fc144adf7089d62cd867daf9a9abab26dadc3970330c3115196b3d712 +size 25733 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_1_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_1_de.png index b4a0a02716c..b388b44d13d 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_1_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3df860d4a1953ea868def4b83337dcd46b1e5922a4705739408f36432afd860c -size 31952 +oid sha256:1693b3caa69c3da46ca80b7d36edd6ae69b8596b376c92f0c446457329f58e9e +size 23749 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_2_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_2_de.png index 18409948e05..6c14bfade12 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_2_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7a0515afdd3cb84d9eb3fee0e893c9070bedfc3d17590fc4b443f22af5060f0 -size 25230 +oid sha256:c6783cffd4de09a1c9f065240c56432971c8cc089770a04e5d8ca8c62b1dc1a1 +size 25090 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_3_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_3_de.png index c4eff5f3e95..eaed9fdd2a0 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_3_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76238adc371356f9de30ddc6d9f2a0f48e970ea4b6c59a28bb2cf06b39403a42 -size 45177 +oid sha256:1224eff2644a6aaea8b429f9a2e228dc16ec6d5adaa753197f9f580dc16dc43a +size 43605 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_4_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_4_de.png index 5fd4655696e..8e839d09c03 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_4_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:665e40e59ed48eeeab622b33bac5c926cd58dc564127dbcceb63b6b93f25240e -size 39476 +oid sha256:9762e6ba62819e125f207ff88638383e8af90ea57b1e02b083718ce0df3e9391 +size 37210 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_5_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_5_de.png index 63372f6e026..83c27f4994b 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_5_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9c31c26897606c0668ed64472c7e5aac1ba47625e44047494dcd607527f41a0 -size 23263 +oid sha256:52ec49829640d5dc45f5b3f57d0bf8ecefcca2b4423d43c13718fb9ee3988f04 +size 23206 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_6_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_6_de.png index ac1e1a2eaf6..a49110d286c 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_6_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9bcc4faf5570bf743811b7a187a480049e4c859e9c2e827734cf50e85a969d9 -size 31297 +oid sha256:cf53dfe0613ded9f9560231b44b91513626ffc7bc207bc907f85d210fa02468d +size 25327 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_7_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_7_de.png index ff87178f4df..1473c91fda6 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_7_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0bbb74d86ed26f103141dd43838f6840d514902371d6d379bd1401357a8d155 -size 35357 +oid sha256:97fbfad0ad0fb7f73d04cfeeb1e3f452ea0704a32828036bf5023a7b94f12379 +size 26839 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png new file mode 100644 index 00000000000..3f043c2172f --- /dev/null +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67a063a65537c571c9ce463c26304478d0ddf8557c1a2ce1ace6b4b4291aa3f2 +size 37306 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_9_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_9_de.png new file mode 100644 index 00000000000..cbec65fcb84 --- /dev/null +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16a5afedfc4fc23f47af53cf6767cce771ab4df495e79916816179ebc09a3306 +size 34210 diff --git a/screenshots/de/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_de.png b/screenshots/de/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_de.png index 030bf361b0a..e1bb4f0e81d 100644 --- a/screenshots/de/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_de.png +++ b/screenshots/de/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c36389a6ce4e57de1dc537cf005d1c0479d65b992c51a73fece0af6f437b3c23 -size 14593 +oid sha256:84f130f6a61accfade1a5424a7da9ff1e7d314477dbcf464062467d72adab78d +size 14590 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_de.png index c7477e89ab7..5432f843d75 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69d0b4dd3db9575aa4663f78763c215594858afbabc549657d14050212ee8d27 -size 50042 +oid sha256:7b5b8f059789167f8e1f2eb20cf79be5e90b6dc97ed45bc9cf64ebacaa29f85d +size 50063 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_de.png new file mode 100644 index 00000000000..2eaf5fa55fc --- /dev/null +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c28614a63922d6f87c37573037b5c6d3beff0ffdb0456a7c6c6cafecc2e68ca0 +size 36295 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 new file mode 100644 index 00000000000..a5f97ddeade --- /dev/null +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91603c6bd26efcf41d06cb12e21795d61c9c6b24048610365be052416aa18a06 +size 29744 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_de.png new file mode 100644 index 00000000000..5913be82d4e --- /dev/null +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:748818c809f99f2fdadf30f29565ca959fa5e023817563f70621cdb2628150f5 +size 35187 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_de.png new file mode 100644 index 00000000000..5913be82d4e --- /dev/null +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:748818c809f99f2fdadf30f29565ca959fa5e023817563f70621cdb2628150f5 +size 35187 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_de.png index 1d8bd2b8030..5432f843d75 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:781c009e36cc392fc1fe22e27b6354993e7a9b9bc47e3e0db8b6186e5e0ab6c8 -size 51102 +oid sha256:7b5b8f059789167f8e1f2eb20cf79be5e90b6dc97ed45bc9cf64ebacaa29f85d +size 50063 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 340196e5517..c265e62b22a 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:91da377954ad5d2058a34ff1dbccc9f54e4ed490186901edc53e7bf4423525d5 -size 53244 +oid sha256:f14d3f79cba2389579654cc81e942deded5b575bbf7ff721e5a5fd9743d21eb2 +size 41463 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_de.png index 9c6279482ab..d4a9dbd6b2e 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7f9d341ab004ccc1e9292984b6337d22ec5a75b43ff8c603d2eb7107883c941 -size 49983 +oid sha256:4f2ae7b8478ce6be0a40d594b9a79cf16940de4811fed16f988f006157aed55a +size 46639 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 f79c3fe4439..b23f1584eeb 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:4ec5652409e18bb7076f07abfdef63c0e26b43103f7a11850daa192fb0b25574 -size 35687 +oid sha256:0b049ba6c08eda18c5d2d023b3997a8781715922d9c90473eaef2c98cd0198fe +size 36830 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_de.png index 1c1b31a57e6..3df05cb2c14 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e528f5dc047db23984a78359b4997a83c8bdfed49403906fe4322bf2736ac202 -size 23866 +oid sha256:232c8a93fff37940b058795447f43388cd8d61de1a3ca5aab1545885b3bc566d +size 53704 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_de.png index d7e91ecc8c7..10f61efd984 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c02bd36e06f22e03032829e6bc3a291bf8706fda27e5cbd56138c3cd036e14ea -size 34833 +oid sha256:9150cab47a9a9560df48ffefb09e7481f23f675a80877dfaf29168a535f4fee2 +size 55496 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_de.png index d7e91ecc8c7..fddbf4418c4 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c02bd36e06f22e03032829e6bc3a291bf8706fda27e5cbd56138c3cd036e14ea -size 34833 +oid sha256:0a2c484e54b7b0a051c99a472d78efa6d5b9171e23b07fbfd37e66b56df96898 +size 44713 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_de.png new file mode 100644 index 00000000000..d772ada0263 --- /dev/null +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eea647e460084d645b91ade9261677ceef281d410d6463e5e0688b41c9754aed +size 46617 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 new file mode 100644 index 00000000000..d63a010d559 --- /dev/null +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0ae7bbbde765a903beb419aff127b795bb4d49921b93dc60dd1d5218055fbd8 +size 35666 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_de.png new file mode 100644 index 00000000000..a49c1f674e4 --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef8a7a9c83f8fb4a325b76244f58f9aa1484b14038a22c2d896887d0d1f610d4 +size 40512 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_de.png new file mode 100644 index 00000000000..b0948aceeec --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef5540e690a92a418c6bc78375b341ddb7acf65fd336d0ed710fae66c85721f5 +size 30741 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 new file mode 100644 index 00000000000..7f506bfc38a --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d94fc082312e356145540c27959d454604dfdee4d7dade7a0ef5a6b94e223a7f +size 29895 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_de.png new file mode 100644 index 00000000000..d707bf8bffe --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6b07a3c79685c5d5358f9b21dde72b32331cfcf686d9378cb89076a1cc42695 +size 43862 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_de.png new file mode 100644 index 00000000000..45d5da6ce8b --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bb7b2631bfa9c7829b3320f0374137d9157ff9465a6d6a0843476cb1c1aef33 +size 29952 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_de.png new file mode 100644 index 00000000000..b1d180c087e --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d2466557382bab1b8222e22e8f559ae90bc2b156e8b62eba911721f85ad6c60 +size 26066 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_de.png new file mode 100644 index 00000000000..3df05cb2c14 --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:232c8a93fff37940b058795447f43388cd8d61de1a3ca5aab1545885b3bc566d +size 53704 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_de.png new file mode 100644 index 00000000000..10f61efd984 --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9150cab47a9a9560df48ffefb09e7481f23f675a80877dfaf29168a535f4fee2 +size 55496 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_de.png new file mode 100644 index 00000000000..fddbf4418c4 --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a2c484e54b7b0a051c99a472d78efa6d5b9171e23b07fbfd37e66b56df96898 +size 44713 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_de.png new file mode 100644 index 00000000000..ee0007f60af --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29ef55e994b568cb85c0ce53b085fd4b33cc9bc754c9f9ce26f63fe73250213e +size 35188 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_de.png new file mode 100644 index 00000000000..72aff64a030 --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae750c80d5be931a0b488777165d0a3bfed97dd4bf3b4f4667752a034c6f6b84 +size 21441 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 new file mode 100644 index 00000000000..d63a010d559 --- /dev/null +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0ae7bbbde765a903beb419aff127b795bb4d49921b93dc60dd1d5218055fbd8 +size 35666 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_de.png deleted file mode 100644 index 35af29059ec..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23de0727192bb64e3ba28a2cd77685b764279e15eb76d35b166c7d7b2d623b68 -size 45057 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_de.png deleted file mode 100644 index d20c22a8792..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cf14c725efbcab351df2fd6b81478c44b0d1f708e8d4c272c54f5d8886ceb0ac -size 41630 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_de.png deleted file mode 100644 index f4b83605430..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:609cdca094e16bdfca58df4b2d5493bc55755afd3ffbb0dbd5687677f6526e8f -size 40087 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_de.png deleted file mode 100644 index 432c5d369e8..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80d2cf2d1558993992d8304d0dc83a04dbed8e6a72653ecbf309819095c13a40 -size 35820 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_de.png deleted file mode 100644 index 340196e5517..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:91da377954ad5d2058a34ff1dbccc9f54e4ed490186901edc53e7bf4423525d5 -size 53244 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_de.png deleted file mode 100644 index 9c6279482ab..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e7f9d341ab004ccc1e9292984b6337d22ec5a75b43ff8c603d2eb7107883c941 -size 49983 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_de.png deleted file mode 100644 index edb0cf10f38..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c49b74e9b38e3534d1c1bf9c8652f61f09ec3a1c3995886992c0825e4d6b683 -size 34784 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_de.png deleted file mode 100644 index acd690231ae..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0583efe5ebb0cd55bdccbf3bb3e7165d15350cf4eff14b9a17038b6ef8f878b0 -size 21509 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_de.png deleted file mode 100644 index f79c3fe4439..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4ec5652409e18bb7076f07abfdef63c0e26b43103f7a11850daa192fb0b25574 -size 35687 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_de.png deleted file mode 100644 index 35af29059ec..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23de0727192bb64e3ba28a2cd77685b764279e15eb76d35b166c7d7b2d623b68 -size 45057 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_de.png deleted file mode 100644 index 9821002f34e..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b9b3d509178d5cebf46d5c03705d6f6f31cace4b9ff8a6951c594a4b64de5943 -size 38994 diff --git a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_de.png b/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_de.png deleted file mode 100644 index e5cd7a92ff0..00000000000 --- a/screenshots/de/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21ea4c13f56345e1b7f1503362ddc510f9955daa2b516b0309925f8aa272bb0f -size 21572 diff --git a/screenshots/de/features.viewfolder.impl.file_ViewFileView_Day_3_de.png b/screenshots/de/features.viewfolder.impl.file_ViewFileView_Day_3_de.png new file mode 100644 index 00000000000..11a065837e6 --- /dev/null +++ b/screenshots/de/features.viewfolder.impl.file_ViewFileView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15bf00066a3280085e83f07308dc0dcc4eb75a05a0a6d7bd762b7d646154949a +size 9230 diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_0_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_0_de.png index 077cef9b36d..6bb6d7fe086 100644 --- a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_0_de.png +++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fd650a2d3ce6b86f46a064568cc69f36770f073c8e8d00f6ba8bf409abde7c0 -size 100417 +oid sha256:da9eddf528f2af6eb3ba35e9322c90abd468d6d6d7367d559c10b3ebf84d5c4e +size 98472 diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_1_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_1_de.png index 4bd7a285469..59bdd4f5775 100644 --- a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_1_de.png +++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aeb984d7df7478254a2e3b7845d2dc1927bab93a389b8a9d262d1056c7a077dd -size 88308 +oid sha256:324f72f4b302266562437511e956b726718dfa637ba6dff224e1b885949e0122 +size 88116 diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_2_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_2_de.png index 8f8a7a74911..7666ed50dab 100644 --- a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_2_de.png +++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2887716bc49f1d01054943c769f8f4677418b660ed1921b80d858b2fedaaf93a -size 88076 +oid sha256:a85899d835ae7fc78c9fa9bbb90de3bcdd0fbc92ef61c87178e850fdaf2e2857 +size 87898 diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_3_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_3_de.png index aabde636870..86b89bccb8f 100644 --- a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_3_de.png +++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa7f1637bf0e53951f0d8494fbef3ad735d0eeefb519473d5e86b906b1b19ae4 -size 78277 +oid sha256:d5e6bb5f0c189d6e18efec5c620cfd50ef0810421212966c2dba5c42bf2950b0 +size 78098 diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_4_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_4_de.png index a54be7b245d..e79505a6f22 100644 --- a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_4_de.png +++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a322b5841301e5102ead45a9de8f18e56de6d5bc83bf6ef46ac373cc225b5667 -size 75896 +oid sha256:57fc4f25189522495e5e9d91b01104188e4e3c19a55219587d89887b3dc84aff +size 75700 diff --git a/screenshots/de/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_de.png b/screenshots/de/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_de.png index c17f45c270b..134c2e067ee 100644 --- a/screenshots/de/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_de.png +++ b/screenshots/de/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94a51726f68be3f7019c35db4584065a96a16f52a87d14008a0df027172575d6 -size 20694 +oid sha256:cab55f00bc1f59fae859ae25ff7de9000277a7863e61c03194b304e764109b0b +size 20277 diff --git a/screenshots/de/libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_de.png b/screenshots/de/libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_de.png new file mode 100644 index 00000000000..bde5f11c9cd --- /dev/null +++ b/screenshots/de/libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dff9f674b9d6ac618a59f8ba9a1b7d85218e720f40c7a313f8bf06254f0b9de1 +size 19237 diff --git a/screenshots/de/libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_de.png b/screenshots/de/libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_de.png new file mode 100644 index 00000000000..2ebb09a9278 --- /dev/null +++ b/screenshots/de/libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caccab75ed386837ef568242893d41bc235b0cb33c62fa02d298a86b2bd5e491 +size 13869 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_de.png index b1bb373b780..dbad303a621 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26f21bdb866ee13fbcfd0edca7b57425bc14a79c878e31330ac41b375fd18222 -size 31092 +oid sha256:9a552260805265802772756fc44a4e6944f2fa6b3b715c77958753cb52f48a18 +size 31452 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_de.png index afc79d353a4..fbe511f8647 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efa139e3f0ab0c8978a5d22909ede6e3da4c5772ae9c3af578bbca824ccb1735 -size 31581 +oid sha256:dd9ce1166232db781b60f870a249789e8e723fe3ff9feeed64214774b4a049de +size 31986 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 6938d08fba5..b076d47f0b5 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:3ef4c802367119821d1ed1f0edbd1d452f8627df294d2401f1f73975ef114052 -size 36413 +oid sha256:c3fed1d9178d29f0e6af94d98753e8b07333e73cb363770427f604119dca64a3 +size 36434 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png index 6d4d513d58a..89ff0cb67f0 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:490cdffaa906580ba57a19160178ed8f4e495f3598299037eae28ed4fab3208f -size 26224 +oid sha256:995c11981fea3d0d2130fc6a7550c9b19d7a8ce1c340e56c52aefe2083f46d62 +size 26227 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png index cd1f51ecca8..3a1ab5b73ab 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e80fb0ab1e1283b8b7506f4ddc4b26d2e387b41e59befa9dc5d591e7fbb7c3ea -size 25611 +oid sha256:dab78f38ab66795b8a7f9c442916065fcc33534ca1e41d562c1cd889e71f5604 +size 25642 diff --git a/screenshots/de/libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_de.png b/screenshots/de/libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_de.png index 01986852751..f43bc6e9561 100644 --- a/screenshots/de/libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_de.png +++ b/screenshots/de/libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2582879fea69b2f93c4b436283a653c455db8c4d6ad90953158b5be643bb3290 -size 10404 +oid sha256:97f94090cc566f0df97fe30b18d5e775a8b46d3d33f7ba110786a6a465005e74 +size 10407 diff --git a/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png index aecf7a8719e..f96562d2dc5 100644 --- a/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4014d1e0117e4753c37008ac468147bd075b3d6c337c1f84b5e3ba5b6a27339 -size 14182 +oid sha256:0b7fec73fbfc84ec71386b10d6c69dbb22b5d6075f26430e6840829ae80e7063 +size 14188 diff --git a/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png b/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png index 74ceb339587..0a2c8f25a40 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:14b6f03d9721ebd54f3bfe37dd38368f7cb58842b89ad7926f3f37f218b7b665 -size 114342 +oid sha256:135a117af875a1b8ebecbaf4bf4ef86b69ad1ff2d6e207e6d8be5991ba92e658 +size 130475 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 new file mode 100644 index 00000000000..2e3d534bf50 --- /dev/null +++ b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4221f512a64596717845eb51f9316f51b955f9b823a06eb33cf875db4ac65194 +size 28590 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 new file mode 100644 index 00000000000..22e7367fb7e --- /dev/null +++ b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca667c0a03937e313117c5f5cfa1097813b98733e99561bb400d285420907dd7 +size 26961 diff --git a/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png b/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png index d7cfcc04994..f41c9309ecb 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:75584de6b160f72e6bddf7802eda77380cf4bec11f40af4034f512e98f6bb21c -size 40391 +oid sha256:ac4dea47d3ad41ada5222a3b5f234b68b4d71189ce5454aa35551de7d3cdf786 +size 40444 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 2d283d2e386..e19fb7d37be 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:28c727a69814205176b15400a5ce382cbb98b22c517179c409844c4a6285f24e -size 36472 +oid sha256:4154f4050650d940c5fd4efe4923f636625afc6472880ec3b3d28d8224346f10 +size 36519 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 aee817830f9..ac4e7614846 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:e2513f81f392eff600e63309049f8b9cce5876c540237689f114c0d33361210c -size 42017 +oid sha256:daa565bfe2140f1048ee5d49fa24b296919de197f89b4a251344d43c76dc6365 +size 42994 diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_de.png index 19e7937d63d..9f825ec840a 100644 --- a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45a56e54b7b6ed617c7c2a3413b35a4d699ded6d933789dc4b3d471ed404405b -size 22205 +oid sha256:31ba33141179bb2249d0ed3821e630289fe3bb7a85cf4f1b99795c85a507fb52 +size 529081 diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_de.png index 3a85a9cc1f3..9c5268a6f06 100644 --- a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:001d30db0990f08f950aca8c75869b9e8fde5a8a9b30421fa823a235dc3bb330 -size 82769 +oid sha256:10a52fb677a9ab77e3f982716a1207b6364b707f376fddb878b6b89eaf0bbf76 +size 82784 diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_de.png index 09f262390a7..0c61fdb501d 100644 --- a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df526b404d126def56865f095708d83300204a8a7ea9443f1bc580cc396f8672 -size 33084 +oid sha256:0e96fd4b395e2752e3ff4feee266e23501021fb21ffd4399f2e464cddaee0e27 +size 33240 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 49976369b0f..573176271fc 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:9bb5b2effcbff26f496ac1f15de1eb03ee671f4eab8447fcd157e2c5734d721e -size 44809 +oid sha256:f856e324b80969e52dc04eb775047756901d913f6bc9293d33607b82c2efabd3 +size 45789 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 6e5069e73f1..dc8b7b0be08 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:649cb9dd3bbfc4a1af438d993febd66d8fc4e2b36f05dc4dfe92cb184c5a3421 -size 41578 +oid sha256:80e41f7bd9db1206cfaf9ed19f141988e9415ad4f4198f859e8203455145cdf4 +size 42530 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 aa42e719de3..60a3759c27c 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:2d6cfaae8882f070cab8ad04c5570c732d080d15c5ef44c920b2251e9dd99d5e -size 35930 +oid sha256:757f2787e4e3cb1aa6fb8e5d60a852811db68921fc904acca0cf958420795e9c +size 35971 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 07a97d27a5d..d4551d33316 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:22ef46a10c5291a2f52b595924746104d0f9c854b5aeb01e493590e726aa40b1 -size 45972 +oid sha256:71dda303c03a8e8d5aa0d959f726c6a74013ac6d8326a3fbb40e2b85cfe8c3bd +size 45983 diff --git a/screenshots/de/libraries.permissions.api_PermissionsView_Day_0_de.png b/screenshots/de/libraries.permissions.api_PermissionsView_Day_0_de.png index 411feb8410f..103e1688daa 100644 --- a/screenshots/de/libraries.permissions.api_PermissionsView_Day_0_de.png +++ b/screenshots/de/libraries.permissions.api_PermissionsView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22132852b0988b54c0f4cafab0fccdc63abaf6a32bbb41d91f3cb0f326f88d17 -size 30879 +oid sha256:809786fd1963b6ffc673655e2eec299c7488636e3bab0f064d513a0901cfd1b3 +size 32773 diff --git a/screenshots/de/libraries.permissions.api_PermissionsView_Day_1_de.png b/screenshots/de/libraries.permissions.api_PermissionsView_Day_1_de.png index 78242a8f8b3..b3ffaebba18 100644 --- a/screenshots/de/libraries.permissions.api_PermissionsView_Day_1_de.png +++ b/screenshots/de/libraries.permissions.api_PermissionsView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5524f7e67bbdd4c79bda393f727ab0e06892bfce4b1097252a5555e445dd1fc -size 29434 +oid sha256:81e091e214ac8e9ddc8b5980396b9c12433ef62a1a41f35dd48b96116b6974da +size 29998 diff --git a/screenshots/de/libraries.permissions.api_PermissionsView_Day_2_de.png b/screenshots/de/libraries.permissions.api_PermissionsView_Day_2_de.png index 8e2ba408220..8e0666e26a4 100644 --- a/screenshots/de/libraries.permissions.api_PermissionsView_Day_2_de.png +++ b/screenshots/de/libraries.permissions.api_PermissionsView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d526e93686e98471a0d245a75bf3f2afd7533863ba32cf64731f27c7ecf6ece -size 29778 +oid sha256:b4553f6ebb23dad5fda6ca56340c47771b42a8d214c47a627a223d33a96d6d59 +size 30362 diff --git a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_0_de.png b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_0_de.png index b6d92817467..3b66447591a 100644 --- a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_0_de.png +++ b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd763383c2d6f47b24dfe3f47d278973e29b00450096ff6f949931aa09ca75f8 -size 14089 +oid sha256:6ff19ac2b9dbf379714490d1c8b6b7a84e88f2227bcbb3fd693729cf97ce397c +size 13959 diff --git a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_1_de.png b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_1_de.png index 7ff48b1fc3d..f91480b4f7f 100644 --- a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_1_de.png +++ b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0b31e3495d061112c3639035998e3e12d734b03b431f4d491ac7c849ede634c -size 11972 +oid sha256:22822883cff44f202ed52c8673c2f0339eb786db936ab6e7d44228519be866d3 +size 11974 diff --git a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_2_de.png b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_2_de.png index ace652b5df4..48294c4a85a 100644 --- a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_2_de.png +++ b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0bfdf376aea0bd2954f29415ae0f494a18d7182b6da2cb71a2199ff4e53a380 -size 32427 +oid sha256:5b11f9a61532b3b5b7179bbae28cd9834128ed31b5b2d6fa18a5b58b87840c6d +size 32291 diff --git a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_3_de.png b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_3_de.png index ffac9025c7e..bfcfcb5348e 100644 --- a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_3_de.png +++ b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6d9419559e01556642b6c75fe93b3cf05a4c14a9c3fbc8a11185a8beeecc89d +oid sha256:3f7f7be5d21620ea969c560863c0745928c3c8cb8706b6ab7c9ac45294388ea9 size 30594 diff --git a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_4_de.png b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_4_de.png index 7e5e27556d9..dd9ab432461 100644 --- a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_4_de.png +++ b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd428e8a3ac7574c68650e7a57233b4365d040ad8d80c6d3cccf93f3654ce4de -size 34953 +oid sha256:3e92d9906af1ac2fd4aad50ab37756e81a839f28efe1c20d9675f893fb925d53 +size 34952 diff --git a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_5_de.png b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_5_de.png index 4cce3588aa0..cee48fe4c4c 100644 --- a/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_5_de.png +++ b/screenshots/de/libraries.roomselect.impl_RoomSelectView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98501963992b0a158c299eaba73af2d3de09c8d73e6ef35b7042c291ce21c32e -size 30102 +oid sha256:3f506cdaf8275b1174541b7f50e19f5a2b7f6bd07136333392fbeac8449b8f38 +size 29963 diff --git a/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png b/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png index 034b838c950..0f81e3a2f1d 100644 --- a/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:581bc19df178842dc2168c5a4a5b68e252064dfa2ac464b33669f64d931f0ce3 -size 21056 +oid sha256:6b1fb180aaae1b9925b22fd1ba41f115ab2fee7992a4b7a81d1b82410d30dcdb +size 21086 diff --git a/screenshots/de/libraries.textcomposer_ComposerModeView_Day_0_de.png b/screenshots/de/libraries.textcomposer_ComposerModeView_Day_0_de.png index 7ecf002112b..ed5241c920f 100644 --- a/screenshots/de/libraries.textcomposer_ComposerModeView_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_ComposerModeView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e7bdf3b06a82ef3ab52d8dfb804a5e7d1157e4db38f25311c9f4828525c6952 +oid sha256:3052037d003f8bdde4bf79b820d8a1970604b0b8b8a0e2f8d7fa761b29c7c0cb size 6305 diff --git a/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png b/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png index 630a153541f..10e166e98a4 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:42488fd254e1f3af4daa2dbc89d474f6fc33ff32e65fe3a5f1c225fcf440b4f8 -size 55344 +oid sha256:2099eef793b8e48ea898f477cbfb782a470079e1e169cb72fd99fb285cae4022 +size 55497 diff --git a/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png index a7adc543f64..a9633e8d1fc 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:e21e91e0d2fa375a1228f56aca68ec6db1209e5c49970d88d8e72a83362308e2 -size 61266 +oid sha256:bb24147e7a8a9e75c8a8dafd50d83530f2c1e942304f0daa3d6b8b3c28e8bd2a +size 61442 diff --git a/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png index aa645ae51bf..28f77d2dcee 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:077d64f7179ad1d32ff633c5e4074270095d9579508b9a396c05437774810a41 -size 48936 +oid sha256:df3992dca26add78c7d5c325c4dcb1dbbbc73ad89edcb187f823f4b28bc057a6 +size 48865 diff --git a/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png index 4217c75857d..86489db05df 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:51f950b5fbb386756f920bcffe055eac581edfa78f566ff857dacc41761a03df -size 61347 +oid sha256:992054813335b901b58b9708e4ff0ec5e15823b1a712ad810052701571550430 +size 59337 diff --git a/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png new file mode 100644 index 00000000000..ffc8d8791ba --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7ddd16c4d67a3aba5de38be25238c8269f71afe1cd7ebc16108226268f088e7 +size 67935 diff --git a/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png index 630a153541f..10e166e98a4 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:42488fd254e1f3af4daa2dbc89d474f6fc33ff32e65fe3a5f1c225fcf440b4f8 -size 55344 +oid sha256:2099eef793b8e48ea898f477cbfb782a470079e1e169cb72fd99fb285cae4022 +size 55497 diff --git a/screenshots/de/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_de.png new file mode 100644 index 00000000000..7860ec19884 --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90706d1d933bb5f730662d812eb846750267a1e2495df9a00aa8b099f407946a +size 66040 diff --git a/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png index 972c016e3e1..965bba9f222 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:d992143ff08f81f2b2bce2128e8c6ad7520ad98953a3114b44565ec2f43e2d95 -size 52946 +oid sha256:88b5cef701ed726b9570ef194fd07da2be4b09d3c02a9cecdd0a04894ea5c736 +size 53419 diff --git a/screenshots/de/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_de.png index 6cd878104eb..817e9368eee 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9465ae95a76d3c51c086ec9dc8b75e72005af324d6d358356ef361c48545bad -size 14760 +oid sha256:19eff09a3190f13352919e820f64a831544d3bcb5d45904bbb953be8d49f8e10 +size 14868 diff --git a/screenshots/de/libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_de.png index d5211b270de..e1c4bd661be 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:082070d50d9aaa75dc3a9bae767cdb3955e303583b442c9cb99496c0c095b64f -size 15926 +oid sha256:796d1da0f461fe6a9544b98acb73a408feddb754f0fd1f595e773a65591d59b0 +size 16341 diff --git a/screenshots/de/libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_de.png index c033287b5ea..aca05f1877c 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33fb10df2e336c5ed144f6a18cbcefac5a1478d87bc4c53b00ce45d495c3700f -size 19638 +oid sha256:7167d05769b82a6c42face1fd25ecf82a96770fb0279834ffa99bd01d7ed53d9 +size 19969 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png new file mode 100644 index 00000000000..1ef7e3004e8 --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a73103227c128c1f28b3b50cb1c14a9ae052b118249fa807db41e311e82bcf21 +size 74834 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png new file mode 100644 index 00000000000..e30ff355ac3 --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31112b9353107408794ced14e8079b9c5382e153df7efe5428b4dfae1e8ce4fe +size 61468 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png new file mode 100644 index 00000000000..aa5b22fcc9f --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da7799a387265659209bbbc28fdef696d603247de596084dc42c84cb1a1e950a +size 74248 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png new file mode 100644 index 00000000000..50632d441ea --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c82097113e6a51041aea18ebf5ce4d460920d42b1ec25f0ccf44c623a81bbe7 +size 82346 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png new file mode 100644 index 00000000000..4e541e3b78a --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c44eaf0d7532b9d1d0685c51e34ec7543600c745143f39fee8eaf16ac81dbaf +size 63947 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png new file mode 100644 index 00000000000..f4d891ead75 --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cee23d424d4272b9079ee26d49f4f6b2e5a820af8b6a5ed666228db1f389798a +size 62838 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png new file mode 100644 index 00000000000..5801097ed82 --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11e62a896997c3de594b077b02998359ff96b65b166fccf7bebf419449425e5a +size 68480 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png new file mode 100644 index 00000000000..979ba9c61c0 --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1339de24c9c196afc106c955650b13288b7afbebcfae65e68281c7c96c615abd +size 91982 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png new file mode 100644 index 00000000000..2de66310759 --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b2cbfb2eab3af42ef566bd9cdc21d437144b05d968b98d6d86b36def92acafa +size 62336 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png new file mode 100644 index 00000000000..e93966addff --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6050a0697ec0e343c1c3e3c118db5c7b0b70e6538f2a7bb903586bd7f6424ce7 +size 63467 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png new file mode 100644 index 00000000000..c4d965e54ca --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0042e41c23d05ccf736061546c8dc30f2a6e15f6f5f7ac82546d90e83c75d55 +size 71344 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png new file mode 100644 index 00000000000..39ec0b51324 --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbaade7bde192beee2f230097bf022f3f091fa64cde85ee8106f2e27f985f055 +size 61874 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png index 9081a2f732f..366f5c6f558 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:6b6804e4c9415ba75da5ca9b16be07c6b1eda1b6d84e2d563ef692cb93617d32 -size 75820 +oid sha256:a9c6ba3a6d39ab7928ce7ccf7e37a399412cd71a8ff790997806f860358c4d01 +size 75900 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png index 65b9db12be0..c2a79f66499 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:3faaf28767463aa8fe5961a61c6fdf992c7d618b19733bc91150471a88020cb6 -size 58864 +oid sha256:dde43a408286bd596221359e3a29ed82325326f9c5f9ea7e83636ec2a07855e4 +size 59435 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png index b6c1f2e50eb..3d0a1cb6c81 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:84100fcdc5b70f67f9b2165bdf8eb77cccc8f05cd61b0bdcd8ba9039d7949449 -size 73845 +oid sha256:8ccdf0f2a87017ff6fd061670450aa6e4fa33dfdef95e0100b94aaf304c8edc9 +size 74099 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png index a53d365ccd2..5bf71f1dea0 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:2d6aa5d6f29e38888aaaf9539f66bd3235c9ea8a9096e48a6538086057b0b473 -size 85309 +oid sha256:9050d07e311f03077cc9e943ca3dd50612b95804232bda285e076390c4da86c6 +size 85289 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png index 4b664b00557..2c3b4f6a5f9 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:9b853ffe20e9d4c8c10c86caec538eb302b496d7c18232ac0325a498085cc9e4 -size 62069 +oid sha256:7f9e6b5f62d9ca76b369a204040f583932bc827fb6b5f1c1ca0942c814d30f31 +size 62539 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png index 16864c1179d..048d4842d95 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:dba9bd70b11b1747e945aa12ccbb94b40c3025cf70efaf961eb44da4e75be8b8 -size 60340 +oid sha256:ea1f6d452f6fbb9ba45bbc3ef777266f99fd71e0f277c8c557adc8d9470adddf +size 61586 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png index 86b1424d2df..bcf5e5386cc 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:bc3d3b82f386467567eefa83522d21c6d93c0561f5e5cc6301cbacf521686802 -size 68386 +oid sha256:1053bc550660011eee4f313a845d596b5fec2e6a6c65580a7843f855998f2017 +size 68969 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png index 72f44ed719d..24809861703 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:afe16af06bfedd3b030f3a7d27e8d8f63e89068625c3530c71f50a627fcc6a07 -size 59291 +oid sha256:5c53a176cf0f300d40708e01069e27c66349d874c206bd7d079dafa3bcf71abe +size 104228 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png index d1114778105..7d10e145eca 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:93eeb7258381e4e5026226a6226da75975fd3db36d28c57359df0fc8f832bfc4 -size 60072 +oid sha256:c46f7a68998accd949e8ca2e33bd9f8e663de338ebd464e9178dab4c4dd8deec +size 60617 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png index 58bd7612b5d..0419ce4b2a0 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:635fe16b8cf1bf574c989fbf2b0cd302674b66c101ddeb2cde3f2dcb7426ea00 -size 62282 +oid sha256:1fcfe18b254baaff3c93481680979f944fbcbab9e40de45920e35a031e3d64b9 +size 62666 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png index d55b3812f39..ea8a69d22ad 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:c00782f67da13a617718e50dbb6b21e6e4ded8ee29ac1c6e2eb6ae8374050b78 -size 70410 +oid sha256:c4e5601722e9d3e983e168e7725d1bf0538692b89a22784257c6a6f451dad447 +size 71072 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png index 76a546a60c1..dc4559c47d0 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:1906f2a814ab6ddb0f6ca509f2eb28b45515285f6db2acb7822c2c72d49b5884 -size 59543 +oid sha256:01a0264c455d738cb46073213f043ab27762bec96d5839c42daaf12cfec9dd0f +size 60127 diff --git a/screenshots/de/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_de.png new file mode 100644 index 00000000000..d892e33f6dd --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f553fb21d87d1b6cb92d083e60e873d1aa56b85ff2a06874122c78bd72c250f0 +size 58371 diff --git a/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png index 491ce0d9f00..62761e404a9 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fe8f3ac71d021e8799b9536bcc81e8570d742d768f3b07fc87bc9f31f17f0e7 -size 46244 +oid sha256:48afdc8565914ae3782d3c90055bd7686e0e208638a2fb8b86bdaf70c5e48ea3 +size 46578 diff --git a/screenshots/de/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_de.png new file mode 100644 index 00000000000..20a33a20a34 --- /dev/null +++ b/screenshots/de/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:954c6df99d354ed3984f4a62f330f8389d07d15fc94190226a598a0e235e0ced +size 39572 diff --git a/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_0_de.png b/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_0_de.png new file mode 100644 index 00000000000..9abe947d273 --- /dev/null +++ b/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae227ffb1062126d376bd128ce8ad8c5619fbdb95674c1c1e36065ca1dd98a44 +size 13159 diff --git a/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_de.png b/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_de.png new file mode 100644 index 00000000000..26ef5f7f822 --- /dev/null +++ b/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4be3d70c900c8e425267ab125e2391257468767c7eca5e5d4df9b4815524681a +size 45220 diff --git a/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_2_de.png b/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_2_de.png new file mode 100644 index 00000000000..d784900e3a9 --- /dev/null +++ b/screenshots/de/libraries.troubleshoot.impl.history_PushHistoryView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a14221fec003c89240a14fb580878c8a826ff24deb1156d0342aceda91b78c0 +size 23391 diff --git a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_de.png b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_de.png index 237b140a6e4..7a6150671cb 100644 --- a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_de.png +++ b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:166a2c3756c1c71250704d488804399e44437f6b4dab00623e3cc1a0c0d1a171 -size 35565 +oid sha256:ca2ff10f7858de609d123f10d8e3b41da6c321c58159c6c20712fc5c27bf1c3b +size 33679 diff --git a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_de.png b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_de.png index 2d0e6fc008f..85376abe25b 100644 --- a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_de.png +++ b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d025ba2a655c388f908453584015f28b21224e28c2fad3c7779c04308df0846 -size 20398 +oid sha256:fe33cd547b9b3029f850985edf71a76fb93fb3a47f36778fcfb2948f2c09b6c9 +size 20391 diff --git a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_de.png b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_de.png index 42831b1f7e9..6fc011f7fd2 100644 --- a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_de.png +++ b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff28a823d4aa78927e4a1f3647a2b9efd92aa9525a1e8ec7785735d93ddaa412 -size 20811 +oid sha256:793dbbba2e946f34facc717cf42564efb6115f73b69c18b749e94769beac30dc +size 20805 diff --git a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_de.png b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_de.png index b0e663d7608..5aaef3937fc 100644 --- a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_de.png +++ b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b8a48453fdf419ec222fec41c3964f7b4976c6e82e2a1a9eacb340b2a9d196a -size 33037 +oid sha256:df0f6ea6c28da2bea1f1f0bf480e58f42c02e2b991c98a4343978417f3f70173 +size 33704 diff --git a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_de.png b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_de.png index 851cfc4f4aa..2f4733470fc 100644 --- a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_de.png +++ b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:810b6f6c8d2cf36da8d3adf74bf1c351c92a79c40a27059a4cc5c4cc4657f64a -size 27908 +oid sha256:f55a2d44c18f67b1330a18c7ec8e6049d2be810a2a5d2840137555455fcb02ee +size 25554 diff --git a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_de.png b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_de.png index eb4b67d27ab..d88792d37c4 100644 --- a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_de.png +++ b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fa3e1fa0be69164c385397a9217f7ca62f795623d763203b8a03cb0eb8415ce -size 43947 +oid sha256:3107a37af464ba5cda1c622a456365b199a33edaad1abd83a199d44aa4b34b0e +size 41654 diff --git a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_de.png b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_de.png index 8c27788cddc..806fa704670 100644 --- a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_de.png +++ b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8233cebe8131ae7839709f08786b0a261648f1c8fde9ff2e5a9c48ab157b79e8 -size 27454 +oid sha256:565ecdd6b591e0112e0141f737908398f155ba3a4831c9ccdb5a2d4e58bbdb26 +size 27448 diff --git a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_de.png b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_de.png index e8fc9966ffe..5eac10d4bad 100644 --- a/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_de.png +++ b/screenshots/de/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:248daef14353381e3c39926ae5041d31c5a3b94fc4beefead85a07f279ab4900 -size 26600 +oid sha256:e9f4eb92ea02e5f3f14c1b7f7ddaaea46f4b739735a290ff7ef812590d20f52b +size 27191 diff --git a/screenshots/html/data.js b/screenshots/html/data.js index 9fa9c532361..9006dd4020b 100644 --- a/screenshots/html/data.js +++ b/screenshots/html/data.js @@ -1,60 +1,75 @@ // Generated file, do not edit export const screenshots = [ ["en","en-dark","de",], -["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20119,], -["features.invite.impl.response_AcceptDeclineInviteView_Day_0_en","features.invite.impl.response_AcceptDeclineInviteView_Night_0_en",0,], -["features.invite.impl.response_AcceptDeclineInviteView_Day_1_en","features.invite.impl.response_AcceptDeclineInviteView_Night_1_en",20119,], -["features.invite.impl.response_AcceptDeclineInviteView_Day_2_en","features.invite.impl.response_AcceptDeclineInviteView_Night_2_en",20119,], -["features.invite.impl.response_AcceptDeclineInviteView_Day_3_en","features.invite.impl.response_AcceptDeclineInviteView_Night_3_en",20119,], -["features.invite.impl.response_AcceptDeclineInviteView_Day_4_en","features.invite.impl.response_AcceptDeclineInviteView_Night_4_en",20119,], -["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20119,], -["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20119,], -["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20119,], -["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20119,], -["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20119,], +["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20273,], +["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",20273,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20273,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20273,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20273,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20273,], +["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20273,], +["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20273,], +["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20273,], +["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20273,], +["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20273,], +["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20273,], ["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,], ["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",20119,], -["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20119,], -["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20119,], +["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20273,], +["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20273,], +["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20273,], ["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",20119,], -["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20119,], -["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20119,], -["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20119,], -["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20119,], -["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20119,], -["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20119,], -["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20119,], -["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20119,], -["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20119,], -["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20119,], -["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20119,], -["features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en",20119,], -["features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en",20119,], -["features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en",20119,], -["features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en",20119,], -["features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en",20119,], -["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20119,], -["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20119,], -["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20119,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20119,], -["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20119,], +["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20273,], +["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20273,], +["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20273,], +["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20273,], +["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20273,], +["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20273,], +["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20273,], +["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20273,], +["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20273,], +["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20273,], +["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20273,], +["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20273,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20273,], +["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20273,], +["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20273,], +["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20273,], +["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20273,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20273,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20273,], +["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20273,], ["libraries.designsystem.components_Announcement_Day_0_en","libraries.designsystem.components_Announcement_Night_0_en",0,], -["services.apperror.impl_AppErrorView_Day_0_en","services.apperror.impl_AppErrorView_Night_0_en",20119,], +["services.apperror.impl_AppErrorView_Day_0_en","services.apperror.impl_AppErrorView_Night_0_en",20273,], ["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",20119,], +["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20273,], ["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",20119,], +["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20273,], ["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",20119,], +["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20273,], ["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",20119,], +["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20273,], ["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,], @@ -64,18 +79,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_AttachmentsView_0_en","",20119,], -["features.messages.impl.attachments.preview_AttachmentsView_1_en","",20119,], -["features.messages.impl.attachments.preview_AttachmentsView_2_en","",20119,], -["features.messages.impl.attachments.preview_AttachmentsView_3_en","",20119,], -["features.messages.impl.attachments.preview_AttachmentsView_4_en","",20119,], -["features.messages.impl.attachments.preview_AttachmentsView_5_en","",20119,], +["features.messages.impl.attachments.preview_AttachmentsView_0_en","",20273,], +["features.messages.impl.attachments.preview_AttachmentsView_1_en","",0,], +["features.messages.impl.attachments.preview_AttachmentsView_2_en","",20273,], +["features.messages.impl.attachments.preview_AttachmentsView_3_en","",20273,], +["features.messages.impl.attachments.preview_AttachmentsView_4_en","",20273,], +["features.messages.impl.attachments.preview_AttachmentsView_5_en","",20273,], ["features.messages.impl.attachments.preview_AttachmentsView_6_en","",0,], -["features.messages.impl.attachments.preview_AttachmentsView_7_en","",20119,], +["features.messages.impl.attachments.preview_AttachmentsView_7_en","",20273,], ["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",20119,], +["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20273,], +["libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en","",0,], ["features.knockrequests.impl.banner_AvatarRowRtl_Day_0_en","features.knockrequests.impl.banner_AvatarRowRtl_Night_0_en",0,], ["features.knockrequests.impl.banner_AvatarRowRtl_Day_1_en","features.knockrequests.impl.banner_AvatarRowRtl_Night_1_en",0,], ["features.knockrequests.impl.banner_AvatarRowRtl_Day_2_en","features.knockrequests.impl.banner_AvatarRowRtl_Night_2_en",0,], @@ -171,19 +187,26 @@ export const screenshots = [ ["libraries.designsystem.components.avatar_Avatar_Avatars_84_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_85_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_86_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_87_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_88_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_89_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_8_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_90_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_91_en","",0,], +["libraries.designsystem.components.avatar_Avatar_Avatars_92_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_9_en","",0,], ["libraries.designsystem.components.button_BackButton_Buttons_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",20273,], ["libraries.designsystem.components_BigCheckmark_Day_0_en","libraries.designsystem.components_BigCheckmark_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",20119,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20119,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20119,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20119,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20119,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20119,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20119,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20273,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20273,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20273,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20273,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20273,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20273,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20273,], ["libraries.designsystem.components_BloomInitials_Day_0_en","libraries.designsystem.components_BloomInitials_Night_0_en",0,], ["libraries.designsystem.components_BloomInitials_Day_1_en","libraries.designsystem.components_BloomInitials_Night_1_en",0,], ["libraries.designsystem.components_BloomInitials_Day_2_en","libraries.designsystem.components_BloomInitials_Night_2_en",0,], @@ -194,146 +217,162 @@ export const screenshots = [ ["libraries.designsystem.components_BloomInitials_Day_7_en","libraries.designsystem.components_BloomInitials_Night_7_en",0,], ["libraries.designsystem.components_Bloom_Day_0_en","libraries.designsystem.components_Bloom_Night_0_en",0,], ["libraries.designsystem.theme.components_BottomSheetDragHandle_Day_0_en","libraries.designsystem.theme.components_BottomSheetDragHandle_Night_0_en",0,], -["features.rageshake.impl.bugreport_BugReportView_Day_0_en","features.rageshake.impl.bugreport_BugReportView_Night_0_en",20119,], -["features.rageshake.impl.bugreport_BugReportView_Day_1_en","features.rageshake.impl.bugreport_BugReportView_Night_1_en",20119,], -["features.rageshake.impl.bugreport_BugReportView_Day_2_en","features.rageshake.impl.bugreport_BugReportView_Night_2_en",20119,], -["features.rageshake.impl.bugreport_BugReportView_Day_3_en","features.rageshake.impl.bugreport_BugReportView_Night_3_en",20119,], -["features.rageshake.impl.bugreport_BugReportView_Day_4_en","features.rageshake.impl.bugreport_BugReportView_Night_4_en",20119,], +["features.rageshake.impl.bugreport_BugReportView_Day_0_en","features.rageshake.impl.bugreport_BugReportView_Night_0_en",20273,], +["features.rageshake.impl.bugreport_BugReportView_Day_1_en","features.rageshake.impl.bugreport_BugReportView_Night_1_en",20273,], +["features.rageshake.impl.bugreport_BugReportView_Day_2_en","features.rageshake.impl.bugreport_BugReportView_Night_2_en",20273,], +["features.rageshake.impl.bugreport_BugReportView_Day_3_en","features.rageshake.impl.bugreport_BugReportView_Night_3_en",20273,], +["features.rageshake.impl.bugreport_BugReportView_Day_4_en","features.rageshake.impl.bugreport_BugReportView_Night_4_en",20273,], ["libraries.designsystem.atomic.molecules_ButtonColumnMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ButtonColumnMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.molecules_ButtonRowMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ButtonRowMolecule_Night_0_en",0,], ["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",20119,], -["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20119,], +["features.messages.impl.timeline.components_CallMenuItem_Day_2_en","features.messages.impl.timeline.components_CallMenuItem_Night_2_en",20273,], +["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20273,], ["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",0,], -["features.call.impl.ui_CallScreenPipView_Day_0_en","features.call.impl.ui_CallScreenPipView_Night_0_en",0,], -["features.call.impl.ui_CallScreenPipView_Day_1_en","features.call.impl.ui_CallScreenPipView_Night_1_en",0,], +["features.messages.impl.timeline.components_CallMenuItem_Day_5_en","features.messages.impl.timeline.components_CallMenuItem_Night_5_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",20119,], -["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20119,], -["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20119,], -["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20119,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en",20119,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en",20119,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en",20119,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en",20119,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en",20119,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en",20119,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en",20119,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en",20119,], +["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20273,], +["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20273,], +["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20273,], +["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20273,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20273,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en",20273,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en",20273,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en",20273,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en",20273,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en",20273,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en",20273,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en",20273,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en",20273,], ["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",20119,], -["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20119,], +["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20273,], +["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20273,], +["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20273,], ["libraries.matrix.ui.components_CheckableResolvedUserRow_en","",0,], -["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20119,], +["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20273,], ["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",20273,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20273,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20273,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20273,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20273,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20273,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20273,], ["libraries.designsystem.theme.components_CircularProgressIndicator_Progress_Indicators_en","",0,], ["libraries.designsystem.components_ClickableLinkText_Text_en","",0,], ["libraries.designsystem.theme_ColorAliases_Day_0_en","libraries.designsystem.theme_ColorAliases_Night_0_en",0,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20119,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20119,], -["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20119,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20273,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20273,], +["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20273,], ["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,], -["libraries.textcomposer.components_ComposerOptionsButton_Day_0_en","libraries.textcomposer.components_ComposerOptionsButton_Night_0_en",0,], -["libraries.designsystem.components.avatar_CompositeAvatar_Avatars_en","",0,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20119,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20119,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20119,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20119,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20119,], -["features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20119,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20273,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20273,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20273,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20273,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20273,], +["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20273,], ["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_ConnectivityIndicatorView_Day_0_en","features.networkmonitor.api.ui_ConnectivityIndicatorView_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",20119,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20119,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20119,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20119,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20119,], -["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20119,], -["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20119,], -["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20119,], -["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20119,], -["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20119,], -["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20119,], -["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20119,], -["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20119,], -["features.createroom.impl.root_CreateRoomRootView_Day_0_en","features.createroom.impl.root_CreateRoomRootView_Night_0_en",20119,], -["features.createroom.impl.root_CreateRoomRootView_Day_1_en","features.createroom.impl.root_CreateRoomRootView_Night_1_en",20119,], -["features.createroom.impl.root_CreateRoomRootView_Day_2_en","features.createroom.impl.root_CreateRoomRootView_Night_2_en",20119,], -["features.createroom.impl.root_CreateRoomRootView_Day_3_en","features.createroom.impl.root_CreateRoomRootView_Night_3_en",20119,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20119,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20119,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20119,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20119,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20119,], +["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20273,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20273,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20273,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20273,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20273,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20273,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20273,], +["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20273,], +["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20273,], +["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20273,], +["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20273,], +["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20273,], +["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20273,], +["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20273,], +["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20273,], +["features.createroom.impl.root_CreateRoomRootView_Day_0_en","features.createroom.impl.root_CreateRoomRootView_Night_0_en",20273,], +["features.createroom.impl.root_CreateRoomRootView_Day_1_en","features.createroom.impl.root_CreateRoomRootView_Night_1_en",20273,], +["features.createroom.impl.root_CreateRoomRootView_Day_2_en","features.createroom.impl.root_CreateRoomRootView_Night_2_en",20273,], +["features.createroom.impl.root_CreateRoomRootView_Day_3_en","features.createroom.impl.root_CreateRoomRootView_Night_3_en",20273,], +["features.createroom.impl.root_CreateRoomRootView_Day_4_en","features.createroom.impl.root_CreateRoomRootView_Night_4_en",20273,], +["features.createroom.impl.root_CreateRoomRootView_Day_5_en","features.createroom.impl.root_CreateRoomRootView_Night_5_en",20273,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20273,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20273,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20273,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20273,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20273,], ["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","",20119,], -["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20119,], +["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20273,], +["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20273,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20273,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20273,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20273,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20273,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20273,], ["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",20119,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20119,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20119,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20273,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20273,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20273,], ["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",20119,], -["features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en",20119,], -["features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBar_Night_0_en",20119,], +["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20273,], +["features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en","features.home.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en",20273,], +["features.home.impl.components_DefaultRoomListTopBar_Day_0_en","features.home.impl.components_DefaultRoomListTopBar_Night_0_en",20273,], ["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",20119,], -["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20119,], -["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20119,], -["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20119,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20119,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20119,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20119,], +["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20273,], +["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20273,], +["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20273,], +["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20273,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20273,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20273,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20273,], ["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,], ["libraries.designsystem.theme.components_DialogWithTitleAndOkButton_Dialog_with_title_and_ok_button_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithTitleIconAndOkButton_Dialog_with_title,_icon_and_ok_button_Dialogs_en","",0,], -["libraries.textcomposer.components_DismissTextFormattingButton_Day_0_en","libraries.textcomposer.components_DismissTextFormattingButton_Night_0_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",20119,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20119,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20119,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20119,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20119,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_0_en",20122,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en",20122,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en",20122,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_3_en",20122,], -["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_4_en",20122,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20119,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20273,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20273,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20273,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20273,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20273,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_0_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_0_en",20273,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en",20273,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en",20273,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_3_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_3_en",20273,], +["features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_4_en","features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_4_en",20273,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20273,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20273,], ["libraries.matrix.ui.components_EditableAvatarView_Day_0_en","libraries.matrix.ui.components_EditableAvatarView_Night_0_en",0,], ["libraries.matrix.ui.components_EditableAvatarView_Day_1_en","libraries.matrix.ui.components_EditableAvatarView_Night_1_en",0,], ["libraries.matrix.ui.components_EditableAvatarView_Day_2_en","libraries.matrix.ui.components_EditableAvatarView_Night_2_en",0,], @@ -343,10 +382,11 @@ export const screenshots = [ ["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_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en",0,], -["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20119,], -["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20119,], -["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20119,], +["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20273,], +["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20273,], +["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20273,], ["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,], ["libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en",0,], @@ -363,28 +403,46 @@ export const screenshots = [ ["libraries.designsystem.theme.components_FloatingActionButton_Floating_Action_Buttons_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",20119,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20119,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20119,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20273,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20273,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20273,], +["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.messages.impl.forward_ForwardMessagesView_Day_0_en","features.messages.impl.forward_ForwardMessagesView_Night_0_en",0,], ["features.messages.impl.forward_ForwardMessagesView_Day_1_en","features.messages.impl.forward_ForwardMessagesView_Night_1_en",0,], ["features.messages.impl.forward_ForwardMessagesView_Day_2_en","features.messages.impl.forward_ForwardMessagesView_Night_2_en",0,], -["features.messages.impl.forward_ForwardMessagesView_Day_3_en","features.messages.impl.forward_ForwardMessagesView_Night_3_en",20119,], -["features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.roomlist.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20119,], +["features.messages.impl.forward_ForwardMessagesView_Day_3_en","features.messages.impl.forward_ForwardMessagesView_Night_3_en",20273,], +["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20273,], ["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_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20273,], +["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20273,], +["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",20273,], +["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20273,], +["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20273,], +["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20273,], +["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20273,], +["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",0,], +["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20273,], +["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20273,], +["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20273,], +["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20273,], +["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20273,], ["libraries.designsystem.theme.components_HorizontalDivider_Dividers_en","",0,], ["libraries.designsystem.ruler_HorizontalRuler_Day_0_en","libraries.designsystem.ruler_HorizontalRuler_Night_0_en",0,], ["libraries.designsystem.theme.components_IconButton_Buttons_en","",0,], +["libraries.designsystem.theme.components_IconColorButton_Day_0_en","libraries.designsystem.theme.components_IconColorButton_Night_0_en",0,], ["libraries.designsystem.theme.components_IconImageVector_Icons_en","",0,], ["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_IconsCompound_Day_0_en","libraries.designsystem.icons_IconsCompound_Night_0_en",0,], ["libraries.designsystem.icons_IconsCompound_Day_1_en","libraries.designsystem.icons_IconsCompound_Night_1_en",0,], ["libraries.designsystem.icons_IconsCompound_Day_2_en","libraries.designsystem.icons_IconsCompound_Night_2_en",0,], @@ -393,8 +451,8 @@ export const screenshots = [ ["libraries.designsystem.icons_IconsCompound_Day_5_en","libraries.designsystem.icons_IconsCompound_Night_5_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",20119,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20119,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20273,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20273,], ["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,], @@ -402,65 +460,85 @@ 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",20119,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20273,], ["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",20119,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20273,], ["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",20119,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20119,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20119,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20119,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20119,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20119,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20119,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20119,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20119,], +["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20273,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20273,], +["features.networkmonitor.api.ui_Indicator_Day_0_en","features.networkmonitor.api.ui_Indicator_Night_0_en",0,], ["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.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20119,], +["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20273,], +["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20273,], +["features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20273,], +["features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20273,], +["features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20273,], +["features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20273,], +["features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20273,], +["features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20273,], ["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",20119,], -["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20119,], -["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",0,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20119,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20119,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20119,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20119,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20119,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20119,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20119,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20119,], +["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20273,], +["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20273,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20273,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20273,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20273,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20273,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20273,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20273,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20273,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20273,], ["libraries.designsystem.components_LabelledCheckbox_Toggles_en","",0,], ["features.leaveroom.api_LeaveRoomView_Day_0_en","features.leaveroom.api_LeaveRoomView_Night_0_en",0,], -["features.leaveroom.api_LeaveRoomView_Day_1_en","features.leaveroom.api_LeaveRoomView_Night_1_en",20119,], -["features.leaveroom.api_LeaveRoomView_Day_2_en","features.leaveroom.api_LeaveRoomView_Night_2_en",20119,], -["features.leaveroom.api_LeaveRoomView_Day_3_en","features.leaveroom.api_LeaveRoomView_Night_3_en",20119,], -["features.leaveroom.api_LeaveRoomView_Day_4_en","features.leaveroom.api_LeaveRoomView_Night_4_en",20119,], -["features.leaveroom.api_LeaveRoomView_Day_5_en","features.leaveroom.api_LeaveRoomView_Night_5_en",20119,], -["features.leaveroom.api_LeaveRoomView_Day_6_en","features.leaveroom.api_LeaveRoomView_Night_6_en",20119,], +["features.leaveroom.api_LeaveRoomView_Day_1_en","features.leaveroom.api_LeaveRoomView_Night_1_en",20273,], +["features.leaveroom.api_LeaveRoomView_Day_2_en","features.leaveroom.api_LeaveRoomView_Night_2_en",20273,], +["features.leaveroom.api_LeaveRoomView_Day_3_en","features.leaveroom.api_LeaveRoomView_Night_3_en",20273,], +["features.leaveroom.api_LeaveRoomView_Day_4_en","features.leaveroom.api_LeaveRoomView_Night_4_en",20273,], +["features.leaveroom.api_LeaveRoomView_Day_5_en","features.leaveroom.api_LeaveRoomView_Night_5_en",20273,], +["features.leaveroom.api_LeaveRoomView_Day_6_en","features.leaveroom.api_LeaveRoomView_Night_6_en",20273,], ["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.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",20273,], ["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,], @@ -515,30 +593,33 @@ export const screenshots = [ ["libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en","",0,], ["libraries.textcomposer.components_LiveWaveformView_Day_0_en","libraries.textcomposer.components_LiveWaveformView_Night_0_en",0,], ["appnav.room.joined_LoadingRoomNodeView_Day_0_en","appnav.room.joined_LoadingRoomNodeView_Night_0_en",0,], -["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20119,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20119,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20119,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20119,], +["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20273,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20273,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20273,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20273,], ["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",20119,], -["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20119,], -["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20119,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20119,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20119,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20119,], -["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20119,], -["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20119,], -["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20119,], -["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20119,], -["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20119,], -["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20119,], -["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20119,], -["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20119,], -["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20119,], -["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20119,], +["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20273,], +["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20273,], +["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20273,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20273,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20273,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20273,], +["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20273,], +["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20273,], +["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20273,], +["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20273,], +["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20273,], +["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20273,], +["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20273,], +["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20273,], +["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20273,], +["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20273,], +["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20273,], +["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20273,], ["libraries.designsystem.components.button_MainActionButton_Buttons_en","",0,], -["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20119,], +["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20273,], ["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_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,], @@ -549,22 +630,22 @@ export const screenshots = [ ["libraries.matrix.ui.components_MatrixUserRow_Day_1_en","libraries.matrix.ui.components_MatrixUserRow_Night_1_en",0,], ["libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en","libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en",0,], ["libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en","libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en",0,], -["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20119,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20119,], +["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20273,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20273,], ["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",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20119,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20119,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20273,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20273,], ["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,], @@ -572,13 +653,14 @@ export const screenshots = [ ["libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en","libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_0_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_10_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20119,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20119,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20273,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20273,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20122,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20273,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_15_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_16_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_1_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20119,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20273,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_3_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_4_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_5_en","",0,], @@ -590,14 +672,8 @@ 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",20119,], +["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20273,], ["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_10_en","features.messages.impl.timeline.components_MessageEventBubble_Night_10_en",0,], -["features.messages.impl.timeline.components_MessageEventBubble_Day_11_en","features.messages.impl.timeline.components_MessageEventBubble_Night_11_en",0,], -["features.messages.impl.timeline.components_MessageEventBubble_Day_12_en","features.messages.impl.timeline.components_MessageEventBubble_Night_12_en",0,], -["features.messages.impl.timeline.components_MessageEventBubble_Day_13_en","features.messages.impl.timeline.components_MessageEventBubble_Night_13_en",0,], -["features.messages.impl.timeline.components_MessageEventBubble_Day_14_en","features.messages.impl.timeline.components_MessageEventBubble_Night_14_en",0,], -["features.messages.impl.timeline.components_MessageEventBubble_Day_15_en","features.messages.impl.timeline.components_MessageEventBubble_Night_15_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,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_3_en","features.messages.impl.timeline.components_MessageEventBubble_Night_3_en",0,], @@ -605,9 +681,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_MessageEventBubble_Day_8_en","features.messages.impl.timeline.components_MessageEventBubble_Night_8_en",0,], -["features.messages.impl.timeline.components_MessageEventBubble_Day_9_en","features.messages.impl.timeline.components_MessageEventBubble_Night_9_en",0,], -["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20119,], +["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20273,], ["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,], @@ -615,23 +689,26 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessagesReactionButton_Day_1_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_1_en",0,], ["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.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20119,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20119,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20119,], -["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20119,], -["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20119,], -["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",20119,], -["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20119,], -["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20119,], -["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20119,], -["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20119,], -["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20119,], -["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20119,], -["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20119,], -["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20119,], -["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20119,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20273,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20273,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20273,], +["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20273,], +["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20273,], +["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",20273,], +["features.messages.impl_MessagesView_Day_12_en","features.messages.impl_MessagesView_Night_12_en",20273,], +["features.messages.impl_MessagesView_Day_13_en","features.messages.impl_MessagesView_Night_13_en",20273,], +["features.messages.impl_MessagesView_Day_14_en","features.messages.impl_MessagesView_Night_14_en",20273,], +["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20273,], +["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20273,], +["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20273,], +["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20273,], +["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20273,], +["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20273,], +["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20273,], +["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20273,], +["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20273,], ["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",20119,], +["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20273,], ["libraries.designsystem.theme.components_ModalBottomSheetDark_Bottom_Sheets_en","",0,], ["libraries.designsystem.theme.components_ModalBottomSheetLight_Bottom_Sheets_en","",0,], ["appicon.element_MonochromeIcon_en","",0,], @@ -640,29 +717,44 @@ export const screenshots = [ ["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,], -["features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en","features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20119,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20119,], -["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20119,], -["libraries.oidc.impl.webview_OidcView_Day_0_en","libraries.oidc.impl.webview_OidcView_Night_0_en",0,], -["libraries.oidc.impl.webview_OidcView_Day_1_en","libraries.oidc.impl.webview_OidcView_Night_1_en",0,], +["libraries.designsystem.theme.components_NavigationBar_App_Bars_en","",0,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20273,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20273,], +["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20273,], ["libraries.designsystem.atomic.pages_OnBoardingPage_Day_0_en","libraries.designsystem.atomic.pages_OnBoardingPage_Night_0_en",0,], -["features.onboarding.impl_OnBoardingView_Day_0_en","features.onboarding.impl_OnBoardingView_Night_0_en",20119,], -["features.onboarding.impl_OnBoardingView_Day_1_en","features.onboarding.impl_OnBoardingView_Night_1_en",20119,], -["features.onboarding.impl_OnBoardingView_Day_2_en","features.onboarding.impl_OnBoardingView_Night_2_en",20119,], -["features.onboarding.impl_OnBoardingView_Day_3_en","features.onboarding.impl_OnBoardingView_Night_3_en",20119,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20273,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20273,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20273,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20273,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20273,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20273,], ["libraries.designsystem.background_OnboardingBackground_Day_0_en","libraries.designsystem.background_OnboardingBackground_Night_0_en",0,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20273,], +["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",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20273,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20273,], ["libraries.designsystem.theme.components_OutlinedButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMediumLowPadding_Buttons_en","",0,], @@ -674,66 +766,67 @@ export const screenshots = [ ["libraries.designsystem.components_PageTitleWithIconFull_Day_3_en","libraries.designsystem.components_PageTitleWithIconFull_Night_3_en",0,], ["libraries.designsystem.components_PageTitleWithIconFull_Day_4_en","libraries.designsystem.components_PageTitleWithIconFull_Night_4_en",0,], ["libraries.designsystem.components_PageTitleWithIconFull_Day_5_en","libraries.designsystem.components_PageTitleWithIconFull_Night_5_en",0,], +["libraries.designsystem.components_PageTitleWithIconFull_Day_6_en","libraries.designsystem.components_PageTitleWithIconFull_Night_6_en",0,], ["libraries.designsystem.components_PageTitleWithIconMinimal_Day_0_en","libraries.designsystem.components_PageTitleWithIconMinimal_Night_0_en",0,], -["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20119,], -["features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en",20119,], -["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20119,], -["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20119,], -["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20119,], -["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20119,], +["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20273,], +["features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en",20273,], +["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20273,], +["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20273,], +["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20273,], +["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20273,], ["features.lockscreen.impl.components_PinEntryTextField_Day_0_en","features.lockscreen.impl.components_PinEntryTextField_Night_0_en",0,], ["libraries.designsystem.components_PinIcon_Day_0_en","libraries.designsystem.components_PinIcon_Night_0_en",0,], ["features.lockscreen.impl.unlock.keypad_PinKeypad_Day_0_en","features.lockscreen.impl.unlock.keypad_PinKeypad_Night_0_en",0,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20119,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20119,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20273,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20273,], ["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",20119,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20119,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20119,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20119,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20119,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20119,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20119,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20119,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20119,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20119,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20119,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20119,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20119,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20119,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20273,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20273,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20273,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20273,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20273,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20273,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20273,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20273,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20273,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20273,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20273,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20273,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20273,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20273,], ["libraries.designsystem.atomic.atoms_PlaceholderAtom_Day_0_en","libraries.designsystem.atomic.atoms_PlaceholderAtom_Night_0_en",0,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20119,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20119,], -["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20119,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20119,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20119,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20273,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20273,], +["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20273,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20273,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20273,], ["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",20119,], -["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20119,], -["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20119,], -["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20119,], -["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20119,], -["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20119,], -["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20119,], -["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20119,], -["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20119,], -["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20119,], -["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20119,], +["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20273,], +["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20273,], +["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20273,], +["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20273,], +["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20273,], +["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20273,], +["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20273,], +["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20273,], +["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20273,], +["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20273,], +["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20273,], ["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,], @@ -747,312 +840,313 @@ 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,], -["libraries.designsystem.components.preferences_PreferenceTextDark_Preferences_en","",0,], -["libraries.designsystem.components.preferences_PreferenceTextLight_Preferences_en","",0,], -["libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeDark_Preferences_en","",0,], -["libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeLight_Preferences_en","",0,], -["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20119,], -["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20119,], -["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20119,], -["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20119,], +["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20273,], +["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20273,], +["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20273,], +["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20273,], ["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","",20119,], -["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20119,], -["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20119,], -["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20119,], -["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20119,], -["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20119,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20119,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20119,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20119,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20119,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20119,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20119,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20119,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20119,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20119,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20119,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20119,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20119,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20119,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20119,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20119,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20119,], +["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20273,], +["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20273,], +["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20273,], +["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20273,], +["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20273,], +["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20273,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20273,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20273,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20273,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20273,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20273,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20273,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20273,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20273,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20273,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20273,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20273,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20273,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20273,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20273,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20273,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20273,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20273,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20273,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20273,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20273,], ["libraries.designsystem.theme.components_RadioButton_Toggles_en","",0,], -["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20119,], -["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20119,], +["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20273,], +["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20273,], ["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",20119,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20119,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20119,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20119,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20119,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20119,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20119,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20273,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20273,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20273,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20273,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20273,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20273,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20273,], ["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",20119,], -["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20119,], -["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20119,], -["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20119,], -["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20119,], -["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20119,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20119,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20119,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20119,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20119,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20119,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20119,], +["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20273,], +["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20273,], +["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20273,], +["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20273,], +["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20273,], +["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20273,], +["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20273,], +["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20273,], +["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20273,], +["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20273,], +["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20273,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20273,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20273,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20273,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20273,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20273,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20273,], ["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",20119,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20119,], -["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20119,], -["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20119,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en",20119,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en",20119,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en",20119,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en",20119,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en",20119,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en",20119,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en",20119,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en",20119,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20273,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20273,], +["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20273,], +["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20273,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en",20273,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en",20273,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en",20273,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en",20273,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en",20273,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en",20273,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en",20273,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en",20273,], ["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",0,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20119,], -["features.roomdetails.impl_RoomDetailsDark_0_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_10_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_11_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_12_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_13_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_14_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_15_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_1_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_2_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_3_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_4_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_5_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_6_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_7_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_8_en","",20119,], -["features.roomdetails.impl_RoomDetailsDark_9_en","",20119,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_0_en",20119,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_1_en",20119,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_2_en",20119,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_3_en",20119,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_4_en",20119,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_5_en",20119,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_6_en",20119,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_7_en",20119,], -["features.roomdetails.impl_RoomDetails_0_en","",20119,], -["features.roomdetails.impl_RoomDetails_10_en","",20119,], -["features.roomdetails.impl_RoomDetails_11_en","",20119,], -["features.roomdetails.impl_RoomDetails_12_en","",20119,], -["features.roomdetails.impl_RoomDetails_13_en","",20119,], -["features.roomdetails.impl_RoomDetails_14_en","",20119,], -["features.roomdetails.impl_RoomDetails_15_en","",20119,], -["features.roomdetails.impl_RoomDetails_1_en","",20119,], -["features.roomdetails.impl_RoomDetails_2_en","",20119,], -["features.roomdetails.impl_RoomDetails_3_en","",20119,], -["features.roomdetails.impl_RoomDetails_4_en","",20119,], -["features.roomdetails.impl_RoomDetails_5_en","",20119,], -["features.roomdetails.impl_RoomDetails_6_en","",20119,], -["features.roomdetails.impl_RoomDetails_7_en","",20119,], -["features.roomdetails.impl_RoomDetails_8_en","",20119,], -["features.roomdetails.impl_RoomDetails_9_en","",20119,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20119,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20119,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20119,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20119,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20119,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20119,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20119,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en",20119,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en",20119,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en",20119,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en",20119,], -["features.roomlist.impl.components_RoomListContentView_Day_0_en","features.roomlist.impl.components_RoomListContentView_Night_0_en",20119,], -["features.roomlist.impl.components_RoomListContentView_Day_1_en","features.roomlist.impl.components_RoomListContentView_Night_1_en",20119,], -["features.roomlist.impl.components_RoomListContentView_Day_2_en","features.roomlist.impl.components_RoomListContentView_Night_2_en",0,], -["features.roomlist.impl.components_RoomListContentView_Day_3_en","features.roomlist.impl.components_RoomListContentView_Night_3_en",20119,], -["features.roomlist.impl.components_RoomListContentView_Day_4_en","features.roomlist.impl.components_RoomListContentView_Night_4_en",20119,], -["features.roomlist.impl.components_RoomListContentView_Day_5_en","features.roomlist.impl.components_RoomListContentView_Night_5_en",20119,], -["features.roomlist.impl.filters_RoomListFiltersView_Day_0_en","features.roomlist.impl.filters_RoomListFiltersView_Night_0_en",20119,], -["features.roomlist.impl.filters_RoomListFiltersView_Day_1_en","features.roomlist.impl.filters_RoomListFiltersView_Night_1_en",20119,], -["features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en",20119,], -["features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en",20119,], -["features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en",20119,], -["features.roomlist.impl.search_RoomListSearchContent_Day_0_en","features.roomlist.impl.search_RoomListSearchContent_Night_0_en",0,], -["features.roomlist.impl.search_RoomListSearchContent_Day_1_en","features.roomlist.impl.search_RoomListSearchContent_Night_1_en",20119,], -["features.roomlist.impl.search_RoomListSearchContent_Day_2_en","features.roomlist.impl.search_RoomListSearchContent_Night_2_en",20119,], -["features.roomlist.impl_RoomListView_Day_0_en","features.roomlist.impl_RoomListView_Night_0_en",20119,], -["features.roomlist.impl_RoomListView_Day_10_en","features.roomlist.impl_RoomListView_Night_10_en",20119,], -["features.roomlist.impl_RoomListView_Day_1_en","features.roomlist.impl_RoomListView_Night_1_en",20119,], -["features.roomlist.impl_RoomListView_Day_2_en","features.roomlist.impl_RoomListView_Night_2_en",20119,], -["features.roomlist.impl_RoomListView_Day_3_en","features.roomlist.impl_RoomListView_Night_3_en",20119,], -["features.roomlist.impl_RoomListView_Day_4_en","features.roomlist.impl_RoomListView_Night_4_en",20119,], -["features.roomlist.impl_RoomListView_Day_5_en","features.roomlist.impl_RoomListView_Night_5_en",20119,], -["features.roomlist.impl_RoomListView_Day_6_en","features.roomlist.impl_RoomListView_Night_6_en",20119,], -["features.roomlist.impl_RoomListView_Day_7_en","features.roomlist.impl_RoomListView_Night_7_en",20119,], -["features.roomlist.impl_RoomListView_Day_8_en","features.roomlist.impl_RoomListView_Night_8_en",0,], -["features.roomlist.impl_RoomListView_Day_9_en","features.roomlist.impl_RoomListView_Night_9_en",0,], -["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en",20119,], -["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en",20119,], -["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en",20119,], -["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20119,], -["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20119,], -["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20119,], -["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20119,], -["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20119,], -["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",0,], -["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20119,], -["features.roomdetails.impl.members_RoomMemberListView_Day_7_en","features.roomdetails.impl.members_RoomMemberListView_Night_7_en",20119,], -["features.roomdetails.impl.members_RoomMemberListView_Day_8_en","features.roomdetails.impl.members_RoomMemberListView_Night_8_en",20119,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20273,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20273,], +["features.roomdetails.impl_RoomDetailsDark_0_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_10_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_11_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_12_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_13_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_14_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_15_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_16_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_17_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_18_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_19_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_1_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_2_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_3_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_4_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_5_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_6_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_7_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_8_en","",20273,], +["features.roomdetails.impl_RoomDetailsDark_9_en","",20273,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_0_en",20273,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_1_en",20273,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_2_en",20273,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_3_en",20273,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_4_en",20273,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_5_en",20273,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_6_en",20273,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_7_en",20273,], +["features.roomdetails.impl_RoomDetails_0_en","",20273,], +["features.roomdetails.impl_RoomDetails_10_en","",20273,], +["features.roomdetails.impl_RoomDetails_11_en","",20273,], +["features.roomdetails.impl_RoomDetails_12_en","",20273,], +["features.roomdetails.impl_RoomDetails_13_en","",20273,], +["features.roomdetails.impl_RoomDetails_14_en","",20273,], +["features.roomdetails.impl_RoomDetails_15_en","",20273,], +["features.roomdetails.impl_RoomDetails_16_en","",20273,], +["features.roomdetails.impl_RoomDetails_17_en","",20273,], +["features.roomdetails.impl_RoomDetails_18_en","",20273,], +["features.roomdetails.impl_RoomDetails_19_en","",20273,], +["features.roomdetails.impl_RoomDetails_1_en","",20273,], +["features.roomdetails.impl_RoomDetails_2_en","",20273,], +["features.roomdetails.impl_RoomDetails_3_en","",20273,], +["features.roomdetails.impl_RoomDetails_4_en","",20273,], +["features.roomdetails.impl_RoomDetails_5_en","",20273,], +["features.roomdetails.impl_RoomDetails_6_en","",20273,], +["features.roomdetails.impl_RoomDetails_7_en","",20273,], +["features.roomdetails.impl_RoomDetails_8_en","",20273,], +["features.roomdetails.impl_RoomDetails_9_en","",20273,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20273,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20273,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20273,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20273,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20273,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20273,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20273,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en",20273,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en",20273,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en",20273,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en",20273,], +["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20273,], +["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20273,], +["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",20273,], +["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20273,], +["features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en",20273,], +["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20273,], +["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20273,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en",20273,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en",20273,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en",20273,], +["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",20273,], +["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en",20273,], +["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en",20273,], +["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en",20273,], +["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20273,], +["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20273,], +["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20273,], +["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20273,], +["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20273,], +["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20273,], +["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",0,], +["features.roomdetails.impl.members_RoomMemberListView_Day_7_en","features.roomdetails.impl.members_RoomMemberListView_Night_7_en",20273,], +["features.roomdetails.impl.members_RoomMemberListView_Day_8_en","features.roomdetails.impl.members_RoomMemberListView_Night_8_en",20273,], +["features.roomdetails.impl.members_RoomMemberListView_Day_9_en","features.roomdetails.impl.members_RoomMemberListView_Night_9_en",20273,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20273,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20273,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20273,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20273,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20273,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20273,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20273,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20273,], ["libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Day_0_en","libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Night_0_en",0,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en",20119,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en",20119,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en",20119,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_3_en",20119,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_4_en",20119,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en",0,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en",20119,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en",20119,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en",20119,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_9_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_9_en",0,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20119,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20119,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20119,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20119,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20119,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20119,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20119,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20119,], -["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20119,], -["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20119,], -["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20119,], -["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20119,], -["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20119,], -["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20119,], -["features.roomlist.impl.components_RoomSummaryPlaceholderRow_Day_0_en","features.roomlist.impl.components_RoomSummaryPlaceholderRow_Night_0_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_0_en","features.roomlist.impl.components_RoomSummaryRow_Night_0_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_10_en","features.roomlist.impl.components_RoomSummaryRow_Night_10_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_11_en","features.roomlist.impl.components_RoomSummaryRow_Night_11_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_12_en","features.roomlist.impl.components_RoomSummaryRow_Night_12_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_13_en","features.roomlist.impl.components_RoomSummaryRow_Night_13_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_14_en","features.roomlist.impl.components_RoomSummaryRow_Night_14_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_15_en","features.roomlist.impl.components_RoomSummaryRow_Night_15_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_16_en","features.roomlist.impl.components_RoomSummaryRow_Night_16_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_17_en","features.roomlist.impl.components_RoomSummaryRow_Night_17_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_18_en","features.roomlist.impl.components_RoomSummaryRow_Night_18_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_19_en","features.roomlist.impl.components_RoomSummaryRow_Night_19_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_1_en","features.roomlist.impl.components_RoomSummaryRow_Night_1_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_20_en","features.roomlist.impl.components_RoomSummaryRow_Night_20_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_21_en","features.roomlist.impl.components_RoomSummaryRow_Night_21_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_22_en","features.roomlist.impl.components_RoomSummaryRow_Night_22_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_23_en","features.roomlist.impl.components_RoomSummaryRow_Night_23_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_24_en","features.roomlist.impl.components_RoomSummaryRow_Night_24_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_25_en","features.roomlist.impl.components_RoomSummaryRow_Night_25_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_26_en","features.roomlist.impl.components_RoomSummaryRow_Night_26_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_27_en","features.roomlist.impl.components_RoomSummaryRow_Night_27_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_28_en","features.roomlist.impl.components_RoomSummaryRow_Night_28_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_29_en","features.roomlist.impl.components_RoomSummaryRow_Night_29_en",20119,], -["features.roomlist.impl.components_RoomSummaryRow_Day_2_en","features.roomlist.impl.components_RoomSummaryRow_Night_2_en",20119,], -["features.roomlist.impl.components_RoomSummaryRow_Day_30_en","features.roomlist.impl.components_RoomSummaryRow_Night_30_en",20119,], -["features.roomlist.impl.components_RoomSummaryRow_Day_31_en","features.roomlist.impl.components_RoomSummaryRow_Night_31_en",20119,], -["features.roomlist.impl.components_RoomSummaryRow_Day_32_en","features.roomlist.impl.components_RoomSummaryRow_Night_32_en",20119,], -["features.roomlist.impl.components_RoomSummaryRow_Day_33_en","features.roomlist.impl.components_RoomSummaryRow_Night_33_en",20119,], -["features.roomlist.impl.components_RoomSummaryRow_Day_3_en","features.roomlist.impl.components_RoomSummaryRow_Night_3_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_4_en","features.roomlist.impl.components_RoomSummaryRow_Night_4_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_5_en","features.roomlist.impl.components_RoomSummaryRow_Night_5_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_6_en","features.roomlist.impl.components_RoomSummaryRow_Night_6_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_7_en","features.roomlist.impl.components_RoomSummaryRow_Night_7_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_8_en","features.roomlist.impl.components_RoomSummaryRow_Night_8_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_9_en","features.roomlist.impl.components_RoomSummaryRow_Night_9_en",0,], -["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20119,], -["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20119,], -["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20119,], -["appicon.element_RoundIcon_en","",0,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20273,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20273,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20273,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20273,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20273,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20273,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20273,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20273,], +["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20273,], +["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20273,], +["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20273,], +["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20273,], +["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20273,], +["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20273,], +["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,], +["features.home.impl.components_RoomSummaryRow_Day_11_en","features.home.impl.components_RoomSummaryRow_Night_11_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_12_en","features.home.impl.components_RoomSummaryRow_Night_12_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_13_en","features.home.impl.components_RoomSummaryRow_Night_13_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_14_en","features.home.impl.components_RoomSummaryRow_Night_14_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_15_en","features.home.impl.components_RoomSummaryRow_Night_15_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_16_en","features.home.impl.components_RoomSummaryRow_Night_16_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_17_en","features.home.impl.components_RoomSummaryRow_Night_17_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_18_en","features.home.impl.components_RoomSummaryRow_Night_18_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_19_en","features.home.impl.components_RoomSummaryRow_Night_19_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_1_en","features.home.impl.components_RoomSummaryRow_Night_1_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_20_en","features.home.impl.components_RoomSummaryRow_Night_20_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_21_en","features.home.impl.components_RoomSummaryRow_Night_21_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_22_en","features.home.impl.components_RoomSummaryRow_Night_22_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_23_en","features.home.impl.components_RoomSummaryRow_Night_23_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_24_en","features.home.impl.components_RoomSummaryRow_Night_24_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_25_en","features.home.impl.components_RoomSummaryRow_Night_25_en",0,], +["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",20273,], +["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20273,], +["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20273,], +["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20273,], +["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20273,], +["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20273,], +["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20273,], +["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,], +["features.home.impl.components_RoomSummaryRow_Day_6_en","features.home.impl.components_RoomSummaryRow_Night_6_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_7_en","features.home.impl.components_RoomSummaryRow_Night_7_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_8_en","features.home.impl.components_RoomSummaryRow_Night_8_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_9_en","features.home.impl.components_RoomSummaryRow_Night_9_en",0,], +["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20273,], +["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20273,], +["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20273,], ["appicon.enterprise_RoundIcon_en","",0,], +["appicon.element_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",20119,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20119,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20119,], +["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20273,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20273,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20273,], ["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","",20119,], +["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20273,], ["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,], -["features.createroom.impl.components_SearchMultipleUsersResultItem_en","",20119,], -["features.createroom.impl.components_SearchSingleUserResultItem_en","",20119,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20119,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20119,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20119,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20119,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20119,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20119,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20119,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20119,], -["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20119,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20119,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_en","",20122,], -["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_en","",20122,], +["features.createroom.impl.components_SearchMultipleUsersResultItem_en","",20273,], +["features.createroom.impl.components_SearchSingleUserResultItem_en","",20273,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20273,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20273,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20273,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20273,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20273,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20273,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20273,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20273,], +["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20273,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_0_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_1_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_2_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_3_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_4_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_5_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_6_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_7_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewDark_8_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_0_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_1_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_2_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_3_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_4_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_5_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_6_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_7_en","",20273,], +["features.roomdetails.impl.securityandprivacy_SecurityAndPrivacyViewLight_8_en","",20273,], ["libraries.matrix.ui.components_SelectedRoom_Day_0_en","libraries.matrix.ui.components_SelectedRoom_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoom_Day_1_en","libraries.matrix.ui.components_SelectedRoom_Night_1_en",0,], ["libraries.matrix.ui.components_SelectedRoom_Day_2_en","libraries.matrix.ui.components_SelectedRoom_Night_2_en",0,], @@ -1060,11 +1154,11 @@ export const screenshots = [ ["libraries.matrix.ui.components_SelectedUser_Day_0_en","libraries.matrix.ui.components_SelectedUser_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedUsersRowList_Day_0_en","libraries.matrix.ui.components_SelectedUsersRowList_Night_0_en",0,], ["libraries.textcomposer.components_SendButton_Day_0_en","libraries.textcomposer.components_SendButton_Night_0_en",0,], -["features.location.impl.send_SendLocationView_Day_0_en","features.location.impl.send_SendLocationView_Night_0_en",20119,], -["features.location.impl.send_SendLocationView_Day_1_en","features.location.impl.send_SendLocationView_Night_1_en",20119,], -["features.location.impl.send_SendLocationView_Day_2_en","features.location.impl.send_SendLocationView_Night_2_en",20119,], -["features.location.impl.send_SendLocationView_Day_3_en","features.location.impl.send_SendLocationView_Night_3_en",20119,], -["features.location.impl.send_SendLocationView_Day_4_en","features.location.impl.send_SendLocationView_Night_4_en",20119,], +["features.location.impl.send_SendLocationView_Day_0_en","features.location.impl.send_SendLocationView_Night_0_en",20273,], +["features.location.impl.send_SendLocationView_Day_1_en","features.location.impl.send_SendLocationView_Night_1_en",20273,], +["features.location.impl.send_SendLocationView_Day_2_en","features.location.impl.send_SendLocationView_Night_2_en",20273,], +["features.location.impl.send_SendLocationView_Day_3_en","features.location.impl.send_SendLocationView_Night_3_en",20273,], +["features.location.impl.send_SendLocationView_Day_4_en","features.location.impl.send_SendLocationView_Night_4_en",20273,], ["libraries.matrix.ui.messages.sender_SenderName_Day_0_en","libraries.matrix.ui.messages.sender_SenderName_Night_0_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_1_en","libraries.matrix.ui.messages.sender_SenderName_Night_1_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_2_en","libraries.matrix.ui.messages.sender_SenderName_Night_2_en",0,], @@ -1074,27 +1168,27 @@ 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",20119,], -["features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20119,], -["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20119,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20119,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20119,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20119,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20119,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20119,], +["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20273,], +["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20273,], +["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20273,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20273,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20273,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20273,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20273,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20273,], ["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",20119,], -["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20119,], -["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20119,], -["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20119,], -["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20119,], -["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20119,], -["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20119,], -["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20119,], -["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20119,], -["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20119,], +["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20273,], +["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20273,], +["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20273,], +["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20273,], +["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20273,], +["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20273,], +["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20273,], +["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20273,], +["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20273,], +["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20273,], ["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,], ["libraries.designsystem.components.list_SingleSelectionListItemCustomFormattert_Single_selection_List_item_-_custom_formatter_List_items_en","",0,], @@ -1103,87 +1197,111 @@ 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",20119,], +["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20273,], ["libraries.designsystem.theme.components_SnackbarWithActionAndCloseButton_Snackbar_with_action_and_close_button_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLineAndCloseButton_Snackbar_with_action_and_close_button_on_new_line_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLine_Snackbar_with_action_on_new_line_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithAction_Snackbar_with_action_Snackbars_en","",0,], ["libraries.designsystem.theme.components_Snackbar_Snackbar_Snackbars_en","",0,], +["libraries.designsystem.components.avatar.internal_SpaceAvatar_Avatars_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierInsideSquare_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeHeight_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeWidth_en","",0,], -["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",0,], -["features.location.api.internal_StaticMapPlaceholder_Day_1_en","features.location.api.internal_StaticMapPlaceholder_Night_1_en",20119,], +["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20273,], ["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",20119,], +["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20273,], ["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",20119,], +["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20273,], +["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",20119,], -["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20119,], -["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20119,], -["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20119,], -["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20119,], -["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20119,], -["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20119,], -["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20119,], -["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20119,], -["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20119,], +["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20273,], +["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20273,], +["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20273,], +["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20273,], +["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20273,], +["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20273,], +["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20273,], +["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20273,], +["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20273,], +["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20273,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20273,], +["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20273,], +["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20273,], +["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20273,], +["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20273,], ["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",20273,], +["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20273,], ["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,], ["libraries.designsystem.theme.components_TextFieldsDark_TextFields_en","",0,], ["libraries.designsystem.theme.components_TextFieldsLight_TextFields_en","",0,], +["libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_0_en","libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_0_en",0,], +["libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_1_en","libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_1_en",0,], +["libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_2_en","libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_2_en",0,], +["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,], -["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20119,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20119,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20119,], +["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20273,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20273,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20273,], ["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",20119,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20119,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20273,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20273,], ["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,], ["features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en",0,], ["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",20119,], +["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20273,], ["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",20119,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20119,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20273,], ["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,], @@ -1191,17 +1309,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",20119,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20119,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20273,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20273,], ["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_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20119,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20119,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20273,], +["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20273,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20273,], ["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",20119,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20119,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20273,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20273,], ["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,], @@ -1210,40 +1329,40 @@ 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",20119,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20273,], ["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",20119,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20273,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en",0,], ["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","",20119,], +["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20273,], ["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",20119,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20119,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20273,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20273,], ["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",20119,], +["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20273,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20119,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20119,], -["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20119,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20273,], +["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20273,], ["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",20119,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20119,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20273,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20273,], ["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",20119,], +["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20273,], ["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,], @@ -1252,22 +1371,24 @@ 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",20119,], -["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20119,], +["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20273,], +["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20273,], ["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,], ["features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemTextViewWithLinkifiedUrlAndNestedParenthesis_en","",0,], +["features.messages.impl.timeline.components.event_TimelineItemTextViewWithLinkifiedUrl_en","",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_2_en",0,], ["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",20119,], -["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20119,], +["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20273,], +["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20273,], ["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,], @@ -1290,91 +1411,82 @@ 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",20119,], -["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20119,], +["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20273,], +["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20273,], ["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",0,], -["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20119,], -["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20119,], -["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20119,], -["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20119,], -["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20119,], -["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20119,], -["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20119,], -["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20119,], +["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20273,], +["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20273,], +["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20273,], +["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20273,], +["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20273,], +["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20273,], +["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20273,], +["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20273,], ["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",20119,], +["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20273,], ["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",20119,], +["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20273,], ["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",20119,], +["features.messages.impl.timeline_TimelineView_Day_8_en","features.messages.impl.timeline_TimelineView_Night_8_en",20273,], ["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_TopAppBar_App_Bars_en","",0,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20119,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20119,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20119,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20119,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20119,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20119,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20119,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20119,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20273,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20273,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20273,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20273,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20273,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20273,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20273,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20273,], ["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",20119,], -["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20119,], -["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20119,], -["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20119,], -["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20119,], -["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20119,], +["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20273,], +["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20273,], +["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20273,], +["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20273,], +["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20273,], +["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20273,], ["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","",20119,], +["libraries.matrix.ui.components_UnresolvedUserRow_en","",20273,], ["libraries.matrix.ui.components_UnsavedAvatar_Day_0_en","libraries.matrix.ui.components_UnsavedAvatar_Night_0_en",0,], -["libraries.designsystem.components.avatar_UserAvatarColors_Day_0_en","libraries.designsystem.components.avatar_UserAvatarColors_Night_0_en",0,], -["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20119,], -["features.createroom.impl.components_UserListView_Day_0_en","features.createroom.impl.components_UserListView_Night_0_en",20119,], -["features.createroom.impl.components_UserListView_Day_1_en","features.createroom.impl.components_UserListView_Night_1_en",20119,], -["features.createroom.impl.components_UserListView_Day_2_en","features.createroom.impl.components_UserListView_Night_2_en",20119,], +["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",20273,], +["features.createroom.impl.components_UserListView_Day_0_en","features.createroom.impl.components_UserListView_Night_0_en",20273,], +["features.createroom.impl.components_UserListView_Day_1_en","features.createroom.impl.components_UserListView_Night_1_en",20273,], +["features.createroom.impl.components_UserListView_Day_2_en","features.createroom.impl.components_UserListView_Night_2_en",20273,], ["features.createroom.impl.components_UserListView_Day_3_en","features.createroom.impl.components_UserListView_Night_3_en",0,], ["features.createroom.impl.components_UserListView_Day_4_en","features.createroom.impl.components_UserListView_Night_4_en",0,], ["features.createroom.impl.components_UserListView_Day_5_en","features.createroom.impl.components_UserListView_Night_5_en",0,], ["features.createroom.impl.components_UserListView_Day_6_en","features.createroom.impl.components_UserListView_Night_6_en",0,], -["features.createroom.impl.components_UserListView_Day_7_en","features.createroom.impl.components_UserListView_Night_7_en",20119,], +["features.createroom.impl.components_UserListView_Day_7_en","features.createroom.impl.components_UserListView_Night_7_en",20273,], ["features.createroom.impl.components_UserListView_Day_8_en","features.createroom.impl.components_UserListView_Night_8_en",0,], -["features.createroom.impl.components_UserListView_Day_9_en","features.createroom.impl.components_UserListView_Night_9_en",20119,], +["features.createroom.impl.components_UserListView_Day_9_en","features.createroom.impl.components_UserListView_Night_9_en",20273,], ["features.preferences.impl.user_UserPreferences_Day_0_en","features.preferences.impl.user_UserPreferences_Night_0_en",0,], ["features.preferences.impl.user_UserPreferences_Day_1_en","features.preferences.impl.user_UserPreferences_Night_1_en",0,], ["features.preferences.impl.user_UserPreferences_Day_2_en","features.preferences.impl.user_UserPreferences_Night_2_en",0,], -["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20119,], -["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20119,], -["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20119,], -["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20119,], -["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20119,], -["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20119,], -["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20119,], -["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20119,], -["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en",0,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_12_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_12_en",0,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en",20119,], -["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en",20119,], +["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20273,], +["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20273,], +["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20273,], +["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20273,], +["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20273,], +["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20273,], +["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20273,], +["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20273,], +["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20273,], +["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20273,], +["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20273,], +["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20273,], +["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.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",0,], +["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20273,], ["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,], @@ -1393,6 +1505,6 @@ export const screenshots = [ ["libraries.textcomposer.components_VoiceMessageRecording_Day_0_en","libraries.textcomposer.components_VoiceMessageRecording_Night_0_en",0,], ["libraries.textcomposer.components_VoiceMessage_Day_0_en","libraries.textcomposer.components_VoiceMessage_Night_0_en",0,], ["libraries.designsystem.components.media_WaveformPlaybackView_Day_0_en","libraries.designsystem.components.media_WaveformPlaybackView_Night_0_en",0,], -["features.ftue.impl.welcome_WelcomeView_Day_0_en","features.ftue.impl.welcome_WelcomeView_Night_0_en",20119,], +["features.ftue.impl.welcome_WelcomeView_Day_0_en","features.ftue.impl.welcome_WelcomeView_Night_0_en",20273,], ["libraries.designsystem.ruler_WithRulers_Day_0_en","libraries.designsystem.ruler_WithRulers_Night_0_en",0,], ]; 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 9ff4053b332..750a6d1d17d 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 @@ -19,9 +19,9 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker { fun getAvailableAnalyticsProviders(): Set /** - * Return a Flow of Boolean, true if the user has given their consent. + * A Flow of Boolean, true if the user has given their consent. */ - fun getUserConsent(): Flow + val userConsentFlow: Flow /** * Update the user consent value. @@ -29,9 +29,9 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker { suspend fun setUserConsent(userConsent: Boolean) /** - * Return a Flow of Boolean, true if the user has been asked for their consent. + * A Flow of Boolean, true if the user has been asked for their consent. */ - fun didAskUserConsent(): Flow + val didAskUserConsentFlow: Flow /** * Store the fact that the user has been asked for their consent. @@ -39,9 +39,9 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker { suspend fun setDidAskUserConsent() /** - * Return a Flow of String, used for analytics Id. + * A Flow of String, used for analytics Id. */ - fun getAnalyticsId(): Flow + val analyticsIdFlow: Flow /** * Update analyticsId from the AccountData. diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt index 6dafd7f334d..31d52d22492 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt @@ -14,6 +14,7 @@ import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.UserProperties import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.services.analytics.api.AnalyticsService @@ -34,6 +35,7 @@ class DefaultAnalyticsService @Inject constructor( private val analyticsProviders: Set<@JvmSuppressWildcards AnalyticsProvider>, private val analyticsStore: AnalyticsStore, // private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, + @AppCoroutineScope private val coroutineScope: CoroutineScope, private val sessionObserver: SessionObserver, ) : AnalyticsService, SessionListener { @@ -43,6 +45,10 @@ class DefaultAnalyticsService @Inject constructor( // Cache for the properties to send private var pendingUserProperties: UserProperties? = null + override val userConsentFlow: Flow = analyticsStore.userConsentFlow + override val didAskUserConsentFlow: Flow = analyticsStore.didAskUserConsentFlow + override val analyticsIdFlow: Flow = analyticsStore.analyticsIdFlow + init { observeUserConsent() observeSessions() @@ -52,19 +58,11 @@ class DefaultAnalyticsService @Inject constructor( return analyticsProviders } - override fun getUserConsent(): Flow { - return analyticsStore.userConsentFlow - } - override suspend fun setUserConsent(userConsent: Boolean) { Timber.tag(analyticsTag.value).d("setUserConsent($userConsent)") analyticsStore.setUserConsent(userConsent) } - override fun didAskUserConsent(): Flow { - return analyticsStore.didAskUserConsentFlow - } - override suspend fun setDidAskUserConsent() { Timber.tag(analyticsTag.value).d("setDidAskUserConsent()") analyticsStore.setDidAskUserConsent() @@ -74,10 +72,6 @@ class DefaultAnalyticsService @Inject constructor( analyticsStore.setDidAskUserConsent(false) } - override fun getAnalyticsId(): Flow { - return analyticsStore.analyticsIdFlow - } - override suspend fun setAnalyticsId(analyticsId: String) { Timber.tag(analyticsTag.value).d("setAnalyticsId($analyticsId)") analyticsStore.setAnalyticsId(analyticsId) @@ -93,7 +87,7 @@ class DefaultAnalyticsService @Inject constructor( } private fun observeUserConsent() { - getUserConsent() + userConsentFlow .onEach { consent -> Timber.tag(analyticsTag.value).d("User consent updated to $consent") userConsent.set(consent) diff --git a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt index 1b00b238eb1..e05a6e4208f 100644 --- a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt +++ b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt @@ -132,10 +132,10 @@ class DefaultAnalyticsServiceTest { analyticsStore = store, ) assertThat(store.userConsentFlow.first()).isFalse() - assertThat(sut.getUserConsent().first()).isFalse() + assertThat(sut.userConsentFlow.first()).isFalse() sut.setUserConsent(true) assertThat(store.userConsentFlow.first()).isTrue() - assertThat(sut.getUserConsent().first()).isTrue() + assertThat(sut.userConsentFlow.first()).isTrue() } @Test @@ -146,10 +146,10 @@ class DefaultAnalyticsServiceTest { analyticsStore = store, ) assertThat(store.analyticsIdFlow.first()).isEqualTo("") - assertThat(sut.getAnalyticsId().first()).isEqualTo("") + assertThat(sut.analyticsIdFlow.first()).isEqualTo("") sut.setAnalyticsId(AN_ID) assertThat(store.analyticsIdFlow.first()).isEqualTo(AN_ID) - assertThat(sut.getAnalyticsId().first()).isEqualTo(AN_ID) + assertThat(sut.analyticsIdFlow.first()).isEqualTo(AN_ID) } @Test @@ -160,10 +160,10 @@ class DefaultAnalyticsServiceTest { analyticsStore = store, ) assertThat(store.didAskUserConsentFlow.first()).isFalse() - assertThat(sut.didAskUserConsent().first()).isFalse() + assertThat(sut.didAskUserConsentFlow.first()).isFalse() sut.setDidAskUserConsent() assertThat(store.didAskUserConsentFlow.first()).isTrue() - assertThat(sut.didAskUserConsent().first()).isTrue() + assertThat(sut.didAskUserConsentFlow.first()).isTrue() } @Test diff --git a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/store/FakeAnalyticsStore.kt b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/store/FakeAnalyticsStore.kt index 68e0c0691a0..8cfa3649ec4 100644 --- a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/store/FakeAnalyticsStore.kt +++ b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/store/FakeAnalyticsStore.kt @@ -10,7 +10,7 @@ package io.element.android.services.analytics.impl.store import io.element.android.tests.testutils.lambda.lambdaError import kotlinx.coroutines.flow.MutableStateFlow -class FakeAnalyticsStore( +internal class FakeAnalyticsStore( defaultUserConsent: Boolean = false, defaultDidAskUserConsent: Boolean = false, defaultAnalyticsId: String = "", diff --git a/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopAnalyticsService.kt b/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopAnalyticsService.kt index eb05c5c151a..9af15543f0a 100644 --- a/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopAnalyticsService.kt +++ b/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopAnalyticsService.kt @@ -24,11 +24,11 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class NoopAnalyticsService @Inject constructor() : AnalyticsService { override fun getAvailableAnalyticsProviders(): Set = emptySet() - override fun getUserConsent(): Flow = flowOf(false) + override val userConsentFlow: Flow = flowOf(false) override suspend fun setUserConsent(userConsent: Boolean) = Unit - override fun didAskUserConsent(): Flow = flowOf(true) + override val didAskUserConsentFlow: Flow = flowOf(true) override suspend fun setDidAskUserConsent() = Unit - override fun getAnalyticsId(): Flow = flowOf("") + override val analyticsIdFlow: Flow = flowOf("") override suspend fun setAnalyticsId(analyticsId: String) = Unit override suspend fun reset() = Unit override fun capture(event: VectorAnalyticsEvent) = Unit diff --git a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt index d60cc8f633d..081f66d4e63 100644 --- a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt +++ b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt @@ -15,6 +15,7 @@ import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.AnalyticsProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow class FakeAnalyticsService( isEnabled: Boolean = false, @@ -22,7 +23,7 @@ class FakeAnalyticsService( private val resetLambda: () -> Unit = {}, ) : AnalyticsService { private val isEnabledFlow = MutableStateFlow(isEnabled) - private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) + override val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) val capturedEvents = mutableListOf() val screenEvents = mutableListOf() val trackedErrors = mutableListOf() @@ -30,19 +31,17 @@ class FakeAnalyticsService( override fun getAvailableAnalyticsProviders(): Set = emptySet() - override fun getUserConsent(): Flow = isEnabledFlow + override val userConsentFlow: Flow = isEnabledFlow.asStateFlow() override suspend fun setUserConsent(userConsent: Boolean) { isEnabledFlow.value = userConsent } - override fun didAskUserConsent(): Flow = didAskUserConsentFlow - override suspend fun setDidAskUserConsent() { didAskUserConsentFlow.value = true } - override fun getAnalyticsId(): Flow = MutableStateFlow("") + override val analyticsIdFlow: Flow = MutableStateFlow("") override suspend fun setAnalyticsId(analyticsId: String) { } diff --git a/services/analyticsproviders/posthog/build.gradle.kts b/services/analyticsproviders/posthog/build.gradle.kts index fb59fb842e3..01e6dbc8365 100644 --- a/services/analyticsproviders/posthog/build.gradle.kts +++ b/services/analyticsproviders/posthog/build.gradle.kts @@ -1,3 +1,5 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.setupAnvil /* @@ -12,6 +14,21 @@ plugins { android { namespace = "io.element.android.services.analyticsproviders.posthog" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigFieldStr( + name = "POSTHOG_HOST", + value = BuildTimeConfig.SERVICES_POSTHOG_HOST.takeIf { isEnterpriseBuild } ?: "" + ) + buildConfigFieldStr( + name = "POSTHOG_APIKEY", + value = BuildTimeConfig.SERVICES_POSTHOG_APIKEY.takeIf { isEnterpriseBuild } ?: "" + ) + } } setupAnvil() @@ -21,6 +38,7 @@ dependencies { implementation(libs.posthog) { exclude("com.android.support", "support-annotations") } + implementation(projects.features.enterprise.api) implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.services.analyticsproviders.api) diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt index 2e872367418..af450cfc842 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt @@ -11,7 +11,6 @@ import android.content.Context import com.posthog.PostHogInterface import com.posthog.android.PostHogAndroid import com.posthog.android.PostHogAndroidConfig -import io.element.android.libraries.core.extensions.isElement import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.ApplicationContext import javax.inject.Inject @@ -22,8 +21,7 @@ class PostHogFactory @Inject constructor( private val posthogEndpointConfigProvider: PosthogEndpointConfigProvider, ) { fun createPosthog(): PostHogInterface? { - if (!buildMeta.isElement()) return null - val endpoint = posthogEndpointConfigProvider.provide() + val endpoint = posthogEndpointConfigProvider.provide() ?: return null return PostHogAndroid.with( context, PostHogAndroidConfig( diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt index c699950f7ee..6d14ab6b14e 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt @@ -10,4 +10,6 @@ package io.element.android.services.analyticsproviders.posthog data class PosthogEndpointConfig( val host: String, val apiKey: String, -) +) { + val isValid = host.isNotBlank() && apiKey.isNotBlank() +} diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt index 2a4b80a5420..da35d661e51 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt @@ -7,24 +7,40 @@ package io.element.android.services.analyticsproviders.posthog +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.libraries.core.extensions.isElement import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType import javax.inject.Inject class PosthogEndpointConfigProvider @Inject constructor( private val buildMeta: BuildMeta, + private val enterpriseService: EnterpriseService, ) { - fun provide(): PosthogEndpointConfig { - return when (buildMeta.buildType) { - BuildType.RELEASE -> PosthogEndpointConfig( - host = "https://posthog.element.io", - apiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", - ) - BuildType.NIGHTLY, - BuildType.DEBUG -> PosthogEndpointConfig( - host = "https://posthog.element.dev", - apiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN", - ) + fun provide(): PosthogEndpointConfig? { + return if (enterpriseService.isEnterpriseBuild) { + PosthogEndpointConfig( + host = BuildConfig.POSTHOG_HOST, + apiKey = BuildConfig.POSTHOG_APIKEY, + ).takeIf { + // Note that if the config is invalid, this module will not be included in the build. + // So the configuration should be always valid. + it.isValid + } + } else if (buildMeta.isElement()) { + when (buildMeta.buildType) { + BuildType.RELEASE -> PosthogEndpointConfig( + host = "https://posthog.element.io", + apiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", + ) + BuildType.NIGHTLY, + BuildType.DEBUG -> PosthogEndpointConfig( + host = "https://posthog.element.dev", + apiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN", + ) + } + } else { + null } } } diff --git a/services/analyticsproviders/sentry/build.gradle.kts b/services/analyticsproviders/sentry/build.gradle.kts index e144cb67ea6..1b1c3517128 100644 --- a/services/analyticsproviders/sentry/build.gradle.kts +++ b/services/analyticsproviders/sentry/build.gradle.kts @@ -1,3 +1,5 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.readLocalProperty import extension.setupAnvil @@ -19,13 +21,15 @@ android { } defaultConfig { - buildConfigField( - type = "String", + buildConfigFieldStr( name = "SENTRY_DSN", - value = (System.getenv("ELEMENT_ANDROID_SENTRY_DSN") - ?: readLocalProperty("services.analyticsproviders.sentry.dsn") + value = if (isEnterpriseBuild) { + BuildTimeConfig.SERVICES_SENTRY_DSN + } else { + System.getenv("ELEMENT_ANDROID_SENTRY_DSN") + ?: readLocalProperty("services.analyticsproviders.sentry.dsn") + } ?: "" - ).let { "\"$it\"" } ) } } diff --git a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt index ede40e3051e..0effa8dc414 100644 --- a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt +++ b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt @@ -27,8 +27,6 @@ import timber.log.Timber import javax.inject.Inject import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.iterator -import kotlin.collections.orEmpty @ContributesMultibinding(AppScope::class) class SentryAnalyticsProvider @Inject constructor( @@ -41,9 +39,7 @@ class SentryAnalyticsProvider @Inject constructor( Timber.tag(analyticsTag.value).d("Initializing Sentry") if (Sentry.isEnabled()) return - val dsn = if (SentryConfig.DSN.isNotBlank()) { - SentryConfig.DSN - } else { + val dsn = SentryConfig.DSN.ifBlank { Timber.w("No Sentry DSN provided, Sentry will not be initialized") return } diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/ActiveRoomsHolder.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/ActiveRoomsHolder.kt new file mode 100644 index 00000000000..c45fb0a2808 --- /dev/null +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/ActiveRoomsHolder.kt @@ -0,0 +1,68 @@ +/* + * 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.services.appnavstate.api + +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +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 java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +/** + * Holds the active rooms for a given session so they can be reused instead of instantiating new ones. + */ +@SingleIn(AppScope::class) +class ActiveRoomsHolder @Inject constructor() { + private val rooms = ConcurrentHashMap>() + + /** + * Adds a new held room for the given sessionId. + */ + fun addRoom(room: JoinedRoom) { + val roomsForSessionId = rooms.getOrPut(key = room.sessionId, defaultValue = { mutableSetOf() }) + if (roomsForSessionId.none { it.roomId == room.roomId }) { + // We don't want to add the same room multiple times + roomsForSessionId.add(room) + } + } + + /** + * Returns the last room added for the given [sessionId] or null if no room was added. + */ + fun getActiveRoom(sessionId: SessionId): JoinedRoom? { + return rooms[sessionId]?.lastOrNull() + } + + /** + * Returns an active room associated to the given [sessionId], with the given [roomId], or null if none match. + */ + fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom? { + return rooms[sessionId]?.find { it.roomId == roomId } + } + + /** + * Removes any room matching the provided [sessionId] and [roomId]. + */ + fun removeRoom(sessionId: SessionId, roomId: RoomId) { + val roomsForSessionId = rooms[sessionId] ?: return + roomsForSessionId.removeIf { it.roomId == roomId } + } + + /** + * Clears all the rooms for the given sessionId. + */ + fun clear(sessionId: SessionId) { + val activeRooms = rooms.remove(sessionId) ?: return + for (room in activeRooms) { + // Destroy the room to reset the live timelines + room.destroy() + } + } +} 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 97909a07cab..6b96476376a 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 @@ -18,8 +18,38 @@ interface AppForegroundStateService { */ val isInForeground: StateFlow + /** + * Updates to whether the app is active because an incoming ringing call is happening will be emitted here. + */ + val hasRingingCall: StateFlow + + /** + * Updates to whether the app is in an active call or not will be emitted here. + */ + val isInCall: StateFlow + + /** + * Updates to whether the app is syncing a notification event or not will be emitted here. + */ + val isSyncingNotificationEvent: StateFlow + /** * Start observing the foreground state. */ - fun start() + fun startObservingForeground() + + /** + * Update the in-call state. + */ + fun updateIsInCallState(isInCall: Boolean) + + /** + * Update the 'has ringing call' state. + */ + fun updateHasRingingCall(hasRingingCall: Boolean) + + /** + * Update the active state for the syncing notification event flow. + */ + fun updateIsSyncingNotificationEvent(isSyncingNotificationEvent: 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 1f9d7d79ad4..9bd5004f133 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 @@ -12,19 +12,32 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.ProcessLifecycleOwner import io.element.android.services.appnavstate.api.AppForegroundStateService import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow class DefaultAppForegroundStateService : AppForegroundStateService { - private val state = MutableStateFlow(false) - override val isInForeground: StateFlow = state + override val isInForeground = MutableStateFlow(false) + override val isInCall = MutableStateFlow(false) + override val isSyncingNotificationEvent = MutableStateFlow(false) + override val hasRingingCall = MutableStateFlow(false) private val appLifecycle: Lifecycle by lazy { ProcessLifecycleOwner.get().lifecycle } - override fun start() { + override fun startObservingForeground() { appLifecycle.addObserver(lifecycleObserver) } - private val lifecycleObserver = LifecycleEventObserver { _, _ -> state.value = getCurrentState() } + override fun updateIsInCallState(isInCall: Boolean) { + this.isInCall.value = isInCall + } + + override fun updateHasRingingCall(hasRingingCall: Boolean) { + this.hasRingingCall.value = hasRingingCall + } + + override fun updateIsSyncingNotificationEvent(isSyncingNotificationEvent: Boolean) { + this.isSyncingNotificationEvent.value = isSyncingNotificationEvent + } + + private val lifecycleObserver = LifecycleEventObserver { _, _ -> isInForeground.value = getCurrentState() } private fun getCurrentState(): Boolean = appLifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) } diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt index d6a1bd735bf..204ab66495b 100644 --- a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SpaceId @@ -36,7 +37,8 @@ private val loggerTag = LoggerTag("Navigation") @SingleIn(AppScope::class) class DefaultAppNavigationStateService @Inject constructor( private val appForegroundStateService: AppForegroundStateService, - private val coroutineScope: CoroutineScope, + @AppCoroutineScope + coroutineScope: CoroutineScope, ) : AppNavigationStateService { private val state = MutableStateFlow( AppNavigationState( @@ -48,7 +50,7 @@ class DefaultAppNavigationStateService @Inject constructor( init { coroutineScope.launch { - appForegroundStateService.start() + appForegroundStateService.startObservingForeground() appForegroundStateService.isInForeground.collect { isInForeground -> state.getAndUpdate { it.copy(isInForeground = isInForeground) } } diff --git a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt index 6ab78562268..c6ddff060ef 100644 --- a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt +++ b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt @@ -23,8 +23,8 @@ import io.element.android.services.appnavstate.test.A_SESSION_OWNER import io.element.android.services.appnavstate.test.A_SPACE_OWNER import io.element.android.services.appnavstate.test.A_THREAD_OWNER import io.element.android.services.appnavstate.test.FakeAppForegroundStateService -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -52,7 +52,7 @@ class DefaultNavigationStateServiceTest { @Test fun testNavigation() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) @@ -75,14 +75,14 @@ class DefaultNavigationStateServiceTest { @Test fun testFailure() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) assertThat(service.appNavigationState.value.navigationState).isEqualTo(NavigationState.Root) } @Test fun testOnNavigateToThread() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() // From root (no effect) service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -111,7 +111,7 @@ class DefaultNavigationStateServiceTest { @Test fun testOnNavigateToRoom() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() // From root (no effect) service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -140,7 +140,7 @@ class DefaultNavigationStateServiceTest { @Test fun testOnNavigateToSpace() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() // From root (no effect) service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -169,7 +169,7 @@ class DefaultNavigationStateServiceTest { @Test fun testOnNavigateToSession() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() // From root service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) @@ -198,7 +198,7 @@ class DefaultNavigationStateServiceTest { @Test fun testOnLeavingThread() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() // From root (no effect) service.onLeavingThread(A_THREAD_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -226,7 +226,7 @@ class DefaultNavigationStateServiceTest { @Test fun testOnLeavingRoom() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() // From root (no effect) service.onLeavingRoom(A_ROOM_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -254,7 +254,7 @@ class DefaultNavigationStateServiceTest { @Test fun testOnLeavingSpace() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() // From root (no effect) service.onLeavingSpace(A_SPACE_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -282,7 +282,7 @@ class DefaultNavigationStateServiceTest { @Test fun testOnLeavingSession() = runTest { - val service = createStateService(backgroundScope) + val service = createStateService() // From root service.onLeavingSession(A_SESSION_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -332,7 +332,8 @@ class DefaultNavigationStateServiceTest { onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID) } - private fun createStateService( - coroutineScope: CoroutineScope - ) = DefaultAppNavigationStateService(FakeAppForegroundStateService(), coroutineScope) + private fun TestScope.createStateService() = DefaultAppNavigationStateService( + appForegroundStateService = FakeAppForegroundStateService(), + coroutineScope = backgroundScope, + ) } 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 627a355f0a3..b8bb7b8a20e 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 @@ -9,19 +9,35 @@ package io.element.android.services.appnavstate.test import io.element.android.services.appnavstate.api.AppForegroundStateService import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow class FakeAppForegroundStateService( - initialValue: Boolean = true, + initialForegroundValue: Boolean = true, + initialIsInCallValue: Boolean = false, + initialIsSyncingNotificationEventValue: Boolean = false, + initialHasRingingCall: Boolean = false, ) : AppForegroundStateService { - private val state = MutableStateFlow(initialValue) - override val isInForeground: StateFlow = state + override val isInForeground = MutableStateFlow(initialForegroundValue) + override val isInCall = MutableStateFlow(initialIsInCallValue) + override val isSyncingNotificationEvent = MutableStateFlow(initialIsSyncingNotificationEventValue) + override val hasRingingCall = MutableStateFlow(initialHasRingingCall) - override fun start() { + override fun startObservingForeground() { // No-op } fun givenIsInForeground(isInForeground: Boolean) { - state.value = isInForeground + this.isInForeground.value = isInForeground + } + + override fun updateIsInCallState(isInCall: Boolean) { + this.isInCall.value = isInCall + } + + override fun updateIsSyncingNotificationEvent(isSyncingNotificationEvent: Boolean) { + this.isSyncingNotificationEvent.value = isSyncingNotificationEvent + } + + override fun updateHasRingingCall(hasRingingCall: Boolean) { + this.hasRingingCall.value = hasRingingCall } } diff --git a/services/toolbox/test/build.gradle.kts b/services/toolbox/test/build.gradle.kts index 9f3f9bfee3f..8d2f2b8a2d8 100644 --- a/services/toolbox/test/build.gradle.kts +++ b/services/toolbox/test/build.gradle.kts @@ -14,4 +14,5 @@ android { dependencies { api(projects.services.toolbox.api) + implementation(projects.tests.testutils) } diff --git a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/intent/FakeExternalIntentLauncher.kt b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/intent/FakeExternalIntentLauncher.kt index 721a6ebe1bf..a8e9589b6d8 100644 --- a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/intent/FakeExternalIntentLauncher.kt +++ b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/intent/FakeExternalIntentLauncher.kt @@ -9,9 +9,10 @@ package io.element.android.services.toolbox.test.intent import android.content.Intent import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher +import io.element.android.tests.testutils.lambda.lambdaError class FakeExternalIntentLauncher( - var launchLambda: (Intent) -> Unit = {}, + var launchLambda: (Intent) -> Unit = { lambdaError() }, ) : ExternalIntentLauncher { override fun launch(intent: Intent) { launchLambda(intent) 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 a9f0d3358a1..e6b33d37858 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 @@ -12,15 +12,19 @@ import io.element.android.services.toolbox.api.strings.StringProvider class FakeStringProvider( private val defaultResult: String = "A string" ) : StringProvider { + var lastResIdParam: Int? = null override fun getString(resId: Int): String { + lastResIdParam = resId return defaultResult } override fun getString(resId: Int, vararg formatArgs: Any?): String { + lastResIdParam = resId return defaultResult + formatArgs.joinToString() } override fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any?): String { + lastResIdParam = resId return defaultResult + " ($quantity) " + formatArgs.joinToString() } } diff --git a/settings.gradle.kts b/settings.gradle.kts index b1475bb6368..f523b72fecb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,13 +18,6 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { - maven { - url = URI("https://jitpack.io") - content { - includeModule("com.github.sergio-sastre.ComposablePreviewScanner", "android") - includeModule("com.github.sergio-sastre.ComposablePreviewScanner", "core") - } - } // Snapshot versions maven { url = URI("https://s01.oss.sonatype.org/content/repositories/snapshots") @@ -46,7 +39,6 @@ dependencyResolutionManagement { maven { url = URI("https://www.jitpack.io") content { - includeModule("com.github.UnifiedPush", "android-connector") includeModule("com.github.matrix-org", "matrix-analytics-events") } } @@ -64,6 +56,7 @@ include(":appnav") include(":appconfig") include(":appicon:element") include(":appicon:enterprise") +include(":tests:detekt-rules") include(":tests:konsist") include(":tests:uitests") include(":tests:testutils") diff --git a/tests/detekt-rules/.gitignore b/tests/detekt-rules/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/tests/detekt-rules/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/tests/detekt-rules/build.gradle.kts b/tests/detekt-rules/build.gradle.kts new file mode 100644 index 00000000000..7fbcdb3a8af --- /dev/null +++ b/tests/detekt-rules/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} +java { + sourceCompatibility = Versions.javaVersion + targetCompatibility = Versions.javaVersion +} + +kotlin { + jvmToolchain { + languageVersion = Versions.javaLanguageVersion + } +} + +dependencies { + compileOnly(libs.test.detekt.api) + testImplementation(libs.test.detekt.test) + + testImplementation(libs.test.truth) +} diff --git a/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt new file mode 100644 index 00000000000..8e0ed3ee502 --- /dev/null +++ b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt @@ -0,0 +1,23 @@ +/* + * 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.detektrules + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.RuleSet +import io.gitlab.arturbosch.detekt.api.RuleSetProvider + +class ElementRuleSetProvider : RuleSetProvider { + override val ruleSetId: String = "ElementXRules" + + override fun instance(config: Config): RuleSet = RuleSet( + id = ruleSetId, + rules = listOf( + RunCatchingRule(config), + ) + ) +} diff --git a/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/RunCatchingRule.kt b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/RunCatchingRule.kt new file mode 100644 index 00000000000..094de51be36 --- /dev/null +++ b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/RunCatchingRule.kt @@ -0,0 +1,43 @@ +/* + * 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.detektrules + +import io.gitlab.arturbosch.detekt.api.CodeSmell +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.Debt +import io.gitlab.arturbosch.detekt.api.Entity +import io.gitlab.arturbosch.detekt.api.Issue +import io.gitlab.arturbosch.detekt.api.Rule +import io.gitlab.arturbosch.detekt.api.Severity +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.psiUtil.getCallNameExpression + +class RunCatchingRule(config: Config) : Rule(config) { + override val issue: Issue = Issue( + id = "RunCatchingNotAllowed", + severity = Severity.Style, + description = "Avoid using `runCatching`, use `runCatchingExceptions` or `tryOrNull` instead. " + + "Avoid `mapCatching`, use `mapCatchingExceptions` instead.", + debt = Debt.FIVE_MINS, + ) + + override fun visitCallExpression(expression: KtCallExpression) { + super.visitCallExpression(expression) + + val callNameExpression = expression.getCallNameExpression() ?: return + val hasRunCatchingCall = callNameExpression.text == "runCatching" + val hasMapCatchingCall = callNameExpression.text == "mapCatching" + if (hasRunCatchingCall || hasMapCatchingCall) { + report(CodeSmell( + issue = issue, + entity = Entity.from(expression), + message = "Use `runCatchingExceptions` or `tryOrNull` instead of `runCatching`. Avoid `mapCatching`, use `mapCatchingExceptions` instead." + )) + } + } +} diff --git a/tests/detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider b/tests/detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider new file mode 100644 index 00000000000..454797e7a0c --- /dev/null +++ b/tests/detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider @@ -0,0 +1 @@ +io.element.android.detektrules.ElementRuleSetProvider diff --git a/tests/detekt-rules/src/test/kotlin/io/element/android/detektrules/RunCatchingRuleTest.kt b/tests/detekt-rules/src/test/kotlin/io/element/android/detektrules/RunCatchingRuleTest.kt new file mode 100644 index 00000000000..c11d4d88680 --- /dev/null +++ b/tests/detekt-rules/src/test/kotlin/io/element/android/detektrules/RunCatchingRuleTest.kt @@ -0,0 +1,33 @@ +/* + * 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.detektrules + +import com.google.common.truth.Truth.assertThat +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.test.compileAndLint +import org.junit.Test + +class RunCatchingRuleTest { + private val subject = RunCatchingRule(Config.empty) + + @Test + fun `test RunCatchingRule`() { + val findings = subject.compileAndLint(code) + assertThat(findings).hasSize(3) + } + + private val code = """ + object Foo { + fun bar() { + runCatching {} + kotlin.runCatching {} + Result.success(true).mapCatching { false } + } + } + """.trimIndent() +} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt index b5e75b904da..1bcf7bbfcab 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt @@ -15,6 +15,7 @@ import com.lemonappdev.konsist.api.ext.list.withAllParentsOf import com.lemonappdev.konsist.api.ext.list.withAnnotationNamed import com.lemonappdev.konsist.api.ext.list.withNameContaining import com.lemonappdev.konsist.api.ext.list.withNameEndingWith +import com.lemonappdev.konsist.api.ext.list.withPackage import com.lemonappdev.konsist.api.ext.list.withoutName import com.lemonappdev.konsist.api.ext.list.withoutNameStartingWith import com.lemonappdev.konsist.api.verify.assertEmpty @@ -51,6 +52,7 @@ class KonsistClassNameTest { .withoutName( "AspectRatioProvider", "OverlapRatioProvider", + "TextFileContentProvider", ) .also { // Check that classes are actually found @@ -88,9 +90,9 @@ class KonsistClassNameTest { ) .assertTrue { val interfaceName = it.name - .replace("FakeRust", "") + .replace("FakeFfi", "") .replace("Fake", "") - val result = (it.name.startsWith("Fake") || it.name.startsWith("FakeRust")) && + val result = it.name.startsWith("Fake") && it.parents().any { parent -> val parentName = parent.name.replace(".", "") parentName == interfaceName @@ -105,6 +107,17 @@ class KonsistClassNameTest { assertThat(failingCases).isEqualTo(failingCasesList.size) } + @Test + fun `All Classes that override a class from the Ffi layer must have 'FakeFfi' prefix`() { + Konsist.scopeFromTest() + .classes() + .withPackage("io.element.android.libraries.matrix.impl.fixtures.fakes") + .assertTrue { klass -> + val parentName = klass.parents().firstOrNull()?.name.orEmpty() + klass.name == "FakeFfi$parentName" + } + } + @Test fun `Class implementing interface should have name not end with 'Impl' but start with 'Default'`() { Konsist.scopeFromProject() diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistFlowTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistFlowTest.kt new file mode 100644 index 00000000000..b9cca40740a --- /dev/null +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistFlowTest.kt @@ -0,0 +1,37 @@ +/* + * 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.tests.konsist + +import androidx.compose.runtime.Composable +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.withAnnotationOf +import com.lemonappdev.konsist.api.verify.assertFalse +import org.junit.Test + +class KonsistFlowTest { + @Test + fun `flow must be remembered when it is collected as state`() { + // Match + // ```).collectAsState``` + // and + // ```) + // .collectAsState``` + val regex = "(.*)\\)(\n\\s*)*\\.collectAsState".toRegex() + + Konsist + .scopeFromProject() + .functions() + .withAnnotationOf(Composable::class) + .assertFalse( + additionalMessage = "Please check that the flow is remembered when it is collected as state." + + " Only val flows can be not remembered.", + ) { function -> + regex.matches(function.text) + } + } +} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistImportTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistImportTest.kt index cb996db3b94..c921f3fd60e 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistImportTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistImportTest.kt @@ -24,4 +24,30 @@ class KonsistImportTest { it.name == "org.jetbrains.annotations.VisibleForTesting" } } + + @Test + fun `OutlinedTextField should not be used`() { + Konsist + .scopeFromProject() + .imports + .assertFalse( + additionalMessage = "Please use 'io.element.android.libraries.designsystem.theme.components.TextField' instead of " + + "'androidx.compose.material3.OutlinedTextField.", + ) { + it.name == "androidx.compose.material3.OutlinedTextField" + } + } + + @Test + fun `material3 TopAppBar should not be used`() { + Konsist + .scopeFromProject() + .imports + .assertFalse( + additionalMessage = "Please use 'io.element.android.libraries.designsystem.theme.components.TopAppBar' instead of " + + "'androidx.compose.material3.TopAppBar.", + ) { + it.name == "androidx.compose.material3.TopAppBar" + } + } } 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 24b13feb629..9f20f0fcac2 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 @@ -7,6 +7,7 @@ package io.element.android.tests.konsist +import com.google.common.truth.Truth.assertThat import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.verify.assertTrue import org.junit.Test @@ -43,10 +44,13 @@ class KonsistLicenseTest { .scopeFromProject() .files .filter { - it.path.contains("/enterprise/features").not() && + it.moduleName.startsWith("enterprise").not() && it.nameWithExtension != "locales.kt" && it.name.startsWith("Template ").not() } + .also { + assertThat(it).isNotEmpty() + } .assertTrue { publicLicense.containsMatchIn(it.text) } @@ -58,7 +62,7 @@ class KonsistLicenseTest { .scopeFromProject() .files .filter { - it.path.contains("/enterprise/features") + it.moduleName.startsWith("enterprise") } .assertTrue { enterpriseLicense.containsMatchIn(it.text) 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 e1ebdba139d..3bf65574ed9 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 @@ -66,13 +66,16 @@ class KonsistPreviewTest { "CallScreenPipViewPreview", "ColorAliasesPreview", "DefaultRoomListTopBarWithIndicatorPreview", + "FocusedEventPreview", "GradientFloatingActionButtonCircleShapePreview", + "HeaderFooterPageScrollablePreview", "IconsCompoundPreview", "IconsOtherPreview", "MarkdownTextComposerEditPreview", "MatrixBadgeAtomPositivePreview", "MatrixBadgeAtomNeutralPreview", "MatrixBadgeAtomNegativePreview", + "MatrixBadgeAtomInfoPreview", "MentionSpanPreview", "MessageComposerViewVoicePreview", "MessagesReactionButtonAddPreview", @@ -104,14 +107,21 @@ class KonsistPreviewTest { "TextComposerAddCaptionPreview", "TextComposerCaptionPreview", "TextComposerEditPreview", + "TextComposerEditNotEncryptedPreview", "TextComposerEditCaptionPreview", "TextComposerFormattingPreview", + "TextComposerFormattingNotEncryptedPreview", "TextComposerLinkDialogCreateLinkPreview", "TextComposerLinkDialogCreateLinkWithoutTextPreview", "TextComposerLinkDialogEditLinkPreview", "TextComposerReplyPreview", + "TextComposerReplyNotEncryptedPreview", "TextComposerSimplePreview", + "TextComposerSimpleNotEncryptedPreview", "TextComposerVoicePreview", + "TextComposerVoiceNotEncryptedPreview", + "TextFieldDialogWithBorderPreview", + "TextFieldDialogWithErrorPreview", "TimelineImageWithCaptionRowPreview", "TimelineItemEventRowForDirectRoomPreview", "TimelineItemEventRowShieldPreview", @@ -128,6 +138,7 @@ class KonsistPreviewTest { "TimelineVideoWithCaptionRowPreview", "TimelineViewMessageShieldPreview", "UserAvatarColorsPreview", + "UserProfileHeaderSectionWithVerificationViolationPreview", "VoiceItemViewPlayPreview", ) .assertTrue( diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index a7b81808f9c..8a7730095a3 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.services.toolbox.api) implementation(libs.test.turbine) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt index a794a2851b4..15d09e17a96 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt @@ -84,6 +84,27 @@ class EnsureCalledOnceWithTwoParams( } } +class EnsureCalledOnceWithTwoParamsAndResult( + private val expectedParam1: T, + private val expectedParam2: U, + private val result: R, +) : (T, U) -> R { + private var counter = 0 + override fun invoke(p1: T, p2: U): R { + if (p1 != expectedParam1 || p2 != expectedParam2) { + throw AssertionError("Expected to be called with $expectedParam1 and $expectedParam2, but was called with $p1 and $p2") + } + counter++ + return result + } + + fun assertSuccess() { + if (counter != 1) { + throw AssertionError("Expected to be called once, but was called $counter times") + } + } +} + /** * Shortcut for [ ensureCalledOnceWithParam] with Unit result. */ diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt index 28c57e62f66..fe45364f49c 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt @@ -32,3 +32,15 @@ class EnsureNeverCalledWithTwoParams : (T, U) -> Unit { lambdaError("Should not be called and is called with $p1 and $p2") } } + +class EnsureNeverCalledWithTwoParamsAndResult : (T, U) -> R { + override fun invoke(p1: T, p2: U): R { + lambdaError("Should not be called and is called with $p1 and $p2") + } +} + +class EnsureNeverCalledWithThreeParams : (T, U, V) -> Unit { + override fun invoke(p1: T, p2: U, p3: V) { + lambdaError("Should not be called and is called with $p1, $p2 and $p3") + } +} 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 0723d85d650..620b5def406 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 @@ -9,7 +9,9 @@ package io.element.android.tests.testutils import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import org.junit.Assert.assertFalse import org.junit.rules.TestRule import kotlin.coroutines.CoroutineContext @@ -49,7 +51,16 @@ object RobolectricDispatcherCleaner { } } -fun AndroidComposeTestRule.setSafeContent(content: @Composable () -> Unit) { - RobolectricDispatcherCleaner.clearAndroidUiDispatcher() - setContent(content) +fun AndroidComposeTestRule.setSafeContent( + clearAndroidUiDispatcher: Boolean = false, + content: @Composable () -> Unit, +) { + if (clearAndroidUiDispatcher) { + RobolectricDispatcherCleaner.clearAndroidUiDispatcher() + } + setContent { + CompositionLocalProvider(LocalUiTestMode provides true) { + content() + } + } } 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 440af516767..55371a960d0 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 @@ -16,6 +16,7 @@ import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import io.element.android.libraries.ui.strings.CommonStrings import org.junit.rules.TestRule @@ -54,3 +55,8 @@ fun AndroidComposeTestRule.pressBackKey() { fun SemanticsNodeInteractionsProvider.pressTag(tag: String) { onNode(hasTestTag(tag)).performClick() } + +fun AndroidComposeTestRule.assertNoNodeWithText(@StringRes res: Int) { + val text = activity.getString(res) + onNodeWithText(text).assertDoesNotExist() +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestComposable.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestComposable.kt new file mode 100644 index 00000000000..673fb70bd59 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestComposable.kt @@ -0,0 +1,27 @@ +/* + * 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.tests.testutils + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.test.core.app.ApplicationProvider + +@Composable +fun withConfigurationAndContext(content: @Composable () -> Any?): Any? { + var result: Any? = null + CompositionLocalProvider( + LocalConfiguration provides Configuration(), + LocalContext provides ApplicationProvider.getApplicationContext(), + ) { + result = content() + } + return result +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WithFakeLifecycleOwner.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WithFakeLifecycleOwner.kt index caafe14dc21..16d4cfb2fda 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WithFakeLifecycleOwner.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/WithFakeLifecycleOwner.kt @@ -9,31 +9,64 @@ package io.element.android.tests.testutils import android.annotation.SuppressLint import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.Stable +import androidx.compose.runtime.currentComposer import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.compose.LocalLifecycleOwner +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import io.element.android.libraries.architecture.Presenter +/** + * Composable that provides a fake [LifecycleOwner] to the composition. + * + * **WARNING: DO NOT USE OUTSIDE TESTS.** + */ +@OptIn(InternalComposeApi::class) @Stable @Composable -fun withFakeLifecycleOwner(lifecycleOwner: FakeLifecycleOwner = FakeLifecycleOwner(), block: @Composable () -> T): T { - var state: T? by remember { mutableStateOf(null) } - CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) { - state = block() - } - return state!! +fun withFakeLifecycleOwner( + lifecycleOwner: FakeLifecycleOwner = FakeLifecycleOwner(), + block: @Composable () -> T +): T { + currentComposer.startProvider(LocalLifecycleOwner provides lifecycleOwner) + val state = block() + currentComposer.endProvider() + return state +} + +/** + * Test a [Presenter] with a fake [LifecycleOwner]. + * + * **WARNING: DO NOT USE OUTSIDE TESTS.** + */ +suspend fun Presenter.testWithLifecycleOwner( + lifecycleOwner: FakeLifecycleOwner = FakeLifecycleOwner(), + block: suspend TurbineTestContext.() -> Unit +) { + moleculeFlow(RecompositionMode.Immediate) { + val ret = withFakeLifecycleOwner(lifecycleOwner) { + present() + } + ret + }.test(validate = block) } @SuppressLint("VisibleForTests") -class FakeLifecycleOwner : LifecycleOwner { +class FakeLifecycleOwner(initialState: Lifecycle.State? = null) : LifecycleOwner { override val lifecycle: Lifecycle = LifecycleRegistry.createUnsafe(this) + init { + initialState?.let { givenState(it) } + } + fun givenState(state: Lifecycle.State) { (lifecycle as LifecycleRegistry).currentState = state } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt index 1371384b518..0966dd1cb73 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt @@ -83,6 +83,13 @@ inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2, T3, T4, T5, T6, T7, T8) -> R +): LambdaEightParamsRecorder { + return LambdaEightParamsRecorder(ensureNeverCalled, block) +} + inline fun lambdaAnyRecorder( ensureNeverCalled: Boolean = false, noinline block: (List) -> R diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt index 8a692f67db8..2b16b099bf0 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt @@ -24,6 +24,18 @@ fun value(expectedValue: T) = object : ParameterMatcher { override fun toString(): String = "value($expectedValue)" } +/** + * A matcher that matches a value based on a condition. + * Can be used to assert that a lambda has been called with a value that satisfies a specific condition. + */ +fun matching(check: (T) -> Boolean) = object : ParameterMatcher { + override fun match(param: Any?): Boolean { + @Suppress("UNCHECKED_CAST") + return (param as? T)?.let { check(it) } ?: false + } + override fun toString(): String = "matching(condition)" +} + /** * A matcher that matches any value. * Can be used when we don't care about the value of a parameter. diff --git a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt index 28b66b70dd5..02ffa418a37 100644 --- a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt +++ b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt @@ -11,7 +11,6 @@ import android.content.res.Configuration import android.os.LocaleList import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration @@ -55,7 +54,7 @@ object ScreenshotTest { ElementTheme { Box( modifier = Modifier - .background(MaterialTheme.colorScheme.background) + .background(ElementTheme.colors.bgCanvasDefault) ) { preview() } diff --git a/tests/uitests/src/test/snapshots/images/appicon.element_Icon_en.png b/tests/uitests/src/test/snapshots/images/appicon.element_Icon_en.png index 97ef3ac6518..243b33fecd8 100644 --- a/tests/uitests/src/test/snapshots/images/appicon.element_Icon_en.png +++ b/tests/uitests/src/test/snapshots/images/appicon.element_Icon_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f43597d36fd123dc4ec478ed8ddc3160d383881623d613b8ab8045f042b80cb -size 44846 +oid sha256:96db51e34f64dc2fc5987b1b0ccfa94e866facb37821278cbd4d56247b2f63d2 +size 13785 diff --git a/tests/uitests/src/test/snapshots/images/appicon.element_RoundIcon_en.png b/tests/uitests/src/test/snapshots/images/appicon.element_RoundIcon_en.png index 5811dfb6077..70625c9bbcd 100644 --- a/tests/uitests/src/test/snapshots/images/appicon.element_RoundIcon_en.png +++ b/tests/uitests/src/test/snapshots/images/appicon.element_RoundIcon_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74af6c3ed723f027bb38b2fd7c4b0441ff26149abb587058bd2bb5be9adbe63b -size 40692 +oid sha256:8526150ec60c72842c4354e5c1d5f240813af6110e9f32f515e14954136ba514 +size 16123 diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_3_en.png index 3c4c448d475..d6d6a5a13cb 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0078ea63dae1b2a879c2b367f096307a7f20c72b3e400284c9bb4fd948a8cbff -size 25116 +oid sha256:4a4b04cdfbed4c67792daea3b5d31cec0f763b2cdf4ecee962ce5652ef01af57 +size 24338 diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_3_en.png index a8bce592f9b..4c07cc2a0d9 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29b448b13c0bd0afb0e7d085f5fc6d137b63dbeae75e1286cd236a8d4bea67b9 -size 23178 +oid sha256:6927236633af4aff987dcb40d16ee7ca3187a7b1841aa82147a41210c65e71e4 +size 22444 diff --git a/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Day_0_en.png index f5946367a6f..3136e8b347e 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:846bef7dc54b4d051bc0cd6ea90d66ba406a11d7dda36455dbdf3ee74aab2aae -size 8379 +oid sha256:30efa4ef62ce38f3599c1a6f2a99c9a36e5704a70c99d83670b5f2169a8648f3 +size 8378 diff --git a/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Day_1_en.png index ff61016e0fd..6359c66fe21 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6baed152f58ed5335d559b7e6bc78867147ddd0b8df63e1881875caf0c10f470 -size 10486 +oid sha256:e3c97dbafc4e1e74be95014f9e4f4e10f9b6516d98e89a149791d61c2e1c5ddf +size 10484 diff --git a/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Night_0_en.png index caf3fff19fe..3e42a561069 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9d7f9f3ba4857862530f0f278186dfbaa347d589abca7e2baf5142b2740ede4 -size 7999 +oid sha256:a1a5ca1ae30e3b1c4c2b242b3527a3c09b09acb415ad11333ad4b1f07182cbed +size 8005 diff --git a/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Night_1_en.png index c0b845825a2..5596ef7587d 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.room.joined_LoadingRoomNodeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:813cedb8cb525406a5d475a8b431bc94888f5952009e28f71f983a03198227db -size 10028 +oid sha256:eae95926adb295182d13dbbebcf1650ab1bf816a9d5edeb908cbd8683fec0c66 +size 10031 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en.png new file mode 100644 index 00000000000..76a832569b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6241486ded79ef2c079dcc45a0fe97bf01db66c05712ec7f74db035d76a91b5 +size 18115 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en.png new file mode 100644 index 00000000000..af54ab708ad --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ca4b3953ec0c460052305d9cfa5c0b1db2a97e8d9a27b483e8b5d15d873d32c +size 17500 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png index c9efd9275ae..65e4ae3fcbb 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4944bbdf4621ecfef659746bb7f3fa16d2a107269cbb60d057bf91c9360a1894 -size 82388 +oid sha256:236ed0a010eb4ad4ca28b474a269fe919dc2a329132c42f02092a31bdf7d84a1 +size 83154 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png new file mode 100644 index 00000000000..7e1db079dcb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a70a38c06be96a7d9acee023b3d26fb03b374a981185e5c2a2d217fdbed1fb +size 81825 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png index 972e86b2987..fcec6752c89 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c76c6454e90e4e23ad7ea92d930eeb47c7a68f81dcc7035b358203cb47639f13 -size 75518 +oid sha256:6b29e98607174bd4f8c64e582c8e012a95565a97601ade537375cf0515585592 +size 76228 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png new file mode 100644 index 00000000000..778259739b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f10da703d90ac9bc99c7145eea93598e209f1c674fc918ce69df8875bddbf736 +size 74353 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_0_en.png deleted file mode 100644 index 42d7e86fd0c..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b61b391f6380189603939badc2ae5912f5cb072200a001ff946af2d52e81b95a -size 12734 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_1_en.png deleted file mode 100644 index 8d0995fd9fb..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7696eb0f5bf8698d001ea44be6eb2005f414057f9a65703e6d987e8eb75f7f94 -size 9441 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_0_en.png deleted file mode 100644 index ba938a8d881..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c3d14805d85b33ce1f0fdd26eec433db1973924542cd85a3bb0e4514cdcd857d -size 12392 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_1_en.png deleted file mode 100644 index f8879f9ecb8..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:146fc2ce4d344c0ea947fe0370e6b8d94c2e724c69c01c2cc3476a756e1f09e4 -size 9315 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_0_en.png index 664608b02b7..8d0995fd9fb 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cc555b1614f3d9e5181769ec203da84f0fb01bbec9b7edf1ad53944158aea7e -size 12824 +oid sha256:7696eb0f5bf8698d001ea44be6eb2005f414057f9a65703e6d987e8eb75f7f94 +size 9441 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_1_en.png index 6c16c57faf9..43d80ad9792 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5480b1c7a3244e0c702f6623f736bfc76560843a8eba8c118a1560954414b4f4 -size 14560 +oid sha256:5d22e3789c7515c5fcb0ee0c72a7c238947f526b6959e1590a29008c8cfa9919 +size 11737 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_2_en.png index 90be70d43cd..23a7250bebc 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b42b1ad21ab76ffb7b555809e5d5d4495a6b0faceba181735369e0635ed0a56 -size 13847 +oid sha256:b4d802d233fdb12dbbb8136d8b127b10898ba7c5963ebbe2f8ce4d015016b984 +size 10892 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 5d5b02403fe..a5eb06f3297 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:73bbf4bfe808722a4d2444ab78bd1f6ab6978d0e427dfaed0d0b957d153a00e5 -size 19294 +oid sha256:6d885da406fe9866a5390a65bb6f8dda3e2242bdec4ac59e0ec654a16838bbf6 +size 16309 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_0_en.png index b9701048780..f8879f9ecb8 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:037a5e384eef63ade207a6a938409897f11efc83f1cbb1d382474bf21dbb54c2 -size 12524 +oid sha256:146fc2ce4d344c0ea947fe0370e6b8d94c2e724c69c01c2cc3476a756e1f09e4 +size 9315 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_1_en.png index 516828ebf99..0f1627f1889 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:548c1d91bfee3a9fe22e881d61c469369885f51f80e267ce9e16c6f4501948fe -size 13372 +oid sha256:8f2d4611b2fc7156c855b13d8b390bc9be80b877407f521a8d196434e97650fe +size 10519 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_2_en.png index 5393549b18c..40c8b11139a 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cd1c4e6317f6372c3c341f7e681d82533038a15c2287f584603330b3cf62a40 -size 12316 +oid sha256:b58875c87cfdf54fcfd6990cc4c7637d74d85faed1c69bc4e5c671afb827ea4d +size 9483 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 23fac4cf25c..c36c1d92f39 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:6de376c3fa6098ba16522181a7b7835660c57c77bfd2c7d8ca75d372c4144676 -size 17437 +oid sha256:928fecaa9000e8c0309d4bbbd7742921ec83497cb65868ffda1a2faf0cba9b57 +size 14599 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_IncomingCallScreen_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_IncomingCallScreen_Day_0_en.png index f89ae4e977f..a28414c9d51 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_IncomingCallScreen_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_IncomingCallScreen_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:686ef0bf0a5813e09f1afd7824d5aa909c914bebe553d03955a869673e8f9e16 -size 66270 +oid sha256:2f07a472c171966ad4dd9e8401a634b9c55a303e1ca4ab08cb44c32e17b1a128 +size 66278 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_IncomingCallScreen_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_IncomingCallScreen_Night_0_en.png index 29b78b707e4..f1a359975fa 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_IncomingCallScreen_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_IncomingCallScreen_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8f9f67b6bd11de3983dfdd76c3a72ceaf8d920daebb5e4e749bb74ab5d95dfa -size 58396 +oid sha256:f5a4097408c093e0919e54bdacfe29f3901fe2db7b0984b72c2003499a6dba11 +size 58400 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en.png new file mode 100644 index 00000000000..d7e0c73390a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c2a698cd7518bc96296bcd4f9b0b51b7a09094a178bca84b853efc03b58c228 +size 25365 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en.png new file mode 100644 index 00000000000..d5d6f55e1d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3302dd631da6a38d569425e4a827051c00d20ab5755c7787739bf7ff37441dfd +size 23622 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_0_en.png index a11e325c3a3..5acd82d3abc 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f15537d0a4cbfb012996f5043f43dc5ea5e769b2404968ac4ea60eda8a522f85 -size 13813 +oid sha256:126046f78f6f5064583c825c581b5ee1bf5173f6f454f6fbdb0bad60a739d76d +size 13626 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_1_en.png index 3fcc111b622..0586942e3eb 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c1a4c2d228441f59a2a13064fe2937778a9f3262f6fd843ad03cf09d89e5442 -size 24715 +oid sha256:986a8d145cf65cd2da4f6e670e7aed7e28f3942d9a141eddde5a040f8d035f66 +size 24569 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_2_en.png index 62e19018c44..ebfe52e698c 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:480dcf4628c371f8df8b6a26f73f0a5ed45e9ca89baf9f4ab52bd896ed1cc4d7 -size 77593 +oid sha256:5834fcf87293fd1c8887fceebba0b0e77be27af06843d5dea62a35e4becd2aca +size 77581 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_3_en.png index b77917a3f0c..e4ba410a1ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:118369cac5ffffbda03faa41c4a3fb26e4014757e86fa9b31871e18dfb312b3e -size 42073 +oid sha256:158eab26fc54518630163066c1a2c4b2321e4f5d5ef78204779ce03c7ca8a3e5 +size 41910 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_0_en.png index 135d85a9242..8fee36c1faa 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f3fe0798aae566b768bbe7090526afdb448a6f73966afa8d9c7ccda0e82f9b7 -size 12995 +oid sha256:c9ac77fcbaf4934ab4514622713e048f270d0775699b34e6292350e069ff798e +size 12867 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_1_en.png index de6b1b1a8ae..1e532ffaf14 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f93dfabe9b0e3737c870e710dc10f619959977fb74873bdbb2d9646ef08c593 -size 24914 +oid sha256:e5805f31aef36412e39b5961e41c2b63ddc855a49de8a2ca810662887c15b2b9 +size 24816 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_2_en.png index 26be59c3da9..06be476ce18 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96333cfda74bc44b7f9f95eab383ab26857b07d68ed30255ccb1ef3ae1558a4c -size 78110 +oid sha256:ccae537ee569b5c4450b1e238aaaf4570818d050a228347eb6b09ea10997135c +size 78100 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_3_en.png index 2f0cd1e72fa..ff1b4a807a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.addpeople_AddPeopleView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a5c2e713849c912004275c4c61256ffdf99388bff9da97b40732090295104e3 -size 42081 +oid sha256:19bf6aca68eba224fd702d6aa327a34e3319389fc4c518dd9ee735f481899576 +size 41977 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_SearchMultipleUsersResultItem_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_SearchMultipleUsersResultItem_en.png index e916bb17054..5a710e5bff9 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_SearchMultipleUsersResultItem_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_SearchMultipleUsersResultItem_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d783fa26d1b4a891a000f638a3d6b7e3df08390b9f800e3e4d123e76cfc274fd -size 82488 +oid sha256:75eff760370d8cbbd0d2795a049f06e5f9be75030ef064f3a0e6548750e0166e +size 82468 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_SearchSingleUserResultItem_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_SearchSingleUserResultItem_en.png index 7f83990532d..4dd03182f83 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_SearchSingleUserResultItem_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_SearchSingleUserResultItem_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aabc27a2531ecd79a01de70cbb149ff4c77fb9d37f3cefd5727a083c3edf455 -size 42712 +oid sha256:82d60ec6eaddaa3a9cc4d9d23bf5fe2e6debbf0b441ecafa377e27770d26ecdb +size 42697 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_0_en.png index 09850aa0dc0..852c3b3aee3 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25c7ea080ca7863a986f2fe7836c47647588a1b0b6a13878eae39e44f1f20e19 -size 9279 +oid sha256:516998b2a77a13b3a131f05d4484c2482c527d6e4992a7ebfd1581c492bb5377 +size 9072 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_1_en.png index 866076f13a3..40276b1eb2b 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9099e2ab73d1aae490189f4682dd5c215e47085affe750890d0d84f203c403ab -size 20579 +oid sha256:203174a9d2e58f3937f3d7b75c2afcb735c2172e06f9c92c64016bcaa430c050 +size 20380 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_2_en.png index 9cbd2e472bb..92d9db2f11a 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adb68fcd10b66e37f774560e59991bf6488043d4e1b66e6b1f19a648c90a6333 -size 7572 +oid sha256:d175ae95fed4ac24e9b506678c4ee1e235c3c8405915498f7f97db0764e5470c +size 7571 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_3_en.png index c10cc18a378..fdb10220a2c 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72dbe69b1952d3c2e00509bb1a5e0493baa30c0ef4e444f652f3fd7f81a8eccd +oid sha256:0db80941c8c981d3f66bc6cd9364f0f8fd5b1e7033f81da46cdffe478f97c748 size 6528 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_4_en.png index c10cc18a378..fdb10220a2c 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72dbe69b1952d3c2e00509bb1a5e0493baa30c0ef4e444f652f3fd7f81a8eccd +oid sha256:0db80941c8c981d3f66bc6cd9364f0f8fd5b1e7033f81da46cdffe478f97c748 size 6528 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_5_en.png index d52aba896f2..c562f2cabd9 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ae3884c6bfded545fda8dfadfc94cb4c17bf2df9556c4d33a931c3509b8a885 -size 38429 +oid sha256:f49822ac3f9a328dcbc9747e49ab3b3af74de42f81f833640232199d85d8d961 +size 38428 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_6_en.png index cba838d82c3..e0413b796a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:daba14ee51067d64e9098815b1d187a89b405b0f11b22f7fa888106cfb388fa9 +oid sha256:0c2187b3eb917901c6d9fac36ae44c28c7c3946a055cbf156aab3063c8981e4e size 52055 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_7_en.png index 86a85de950c..3c8281a03c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:878d81472018964e1217c36708e31469a62486e1a913da26fc5a209731c4f9a1 -size 11014 +oid sha256:57ee28833b587ec86f93c685d716f0cce94b60e0752de3fc1a1a3d7ac05cbdb6 +size 11013 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_8_en.png index c10cc18a378..fdb10220a2c 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72dbe69b1952d3c2e00509bb1a5e0493baa30c0ef4e444f652f3fd7f81a8eccd +oid sha256:0db80941c8c981d3f66bc6cd9364f0f8fd5b1e7033f81da46cdffe478f97c748 size 6528 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_9_en.png index 7dcfba5ddc7..0038c4a3daa 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2699ae8a9f69d336f20d2376012526a36394495027d3d277061c5d55359ba587 -size 37761 +oid sha256:d672f247a616419bac169eb8874b1dbc819e0d31ec5458c6f0e77bdc5076c5dc +size 37586 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_0_en.png index 76c02112ca0..74118fd4351 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e691ee4e2bf2b251d3261f0aab8b8b5c1e79a60453ea2299f71cb6f68fa963b -size 8687 +oid sha256:27c89db806974c4ce4a88ca18fdfe17c5dcad0b6b6e24e7cf24c92782f00d49a +size 8534 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_1_en.png index 83d7b3c603e..7808de2c54e 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:759c6edb5f0742fbbb3ed48ac2d88b40e3d5e0c24a396b2250c56392653e6b63 -size 21018 +oid sha256:19c4c14441df64f5ee30607e03d3d7c3d3495c528a07bbc29673e75feada16d7 +size 20895 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_2_en.png index f0f400e37ae..0fbe95ff702 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8638e7c07500bddc9c67480685d2548f0b50772977fee7565fb92ae72eaefcb -size 7504 +oid sha256:d174f1f8e77c9cd02506b487e410a7bcbf4d571f048f739d4f10582c72e682d0 +size 7501 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_3_en.png index acb40617137..228b02c0a9c 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:000dbaa58af2f75293be0a3fed993447275582abaa33088b3f9c533ac4e0fec6 -size 6298 +oid sha256:000c36468af4793e4956a4a2a2be1867afefa13140219e2dd8b17b1f7670b6de +size 6299 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_4_en.png index acb40617137..228b02c0a9c 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:000dbaa58af2f75293be0a3fed993447275582abaa33088b3f9c533ac4e0fec6 -size 6298 +oid sha256:000c36468af4793e4956a4a2a2be1867afefa13140219e2dd8b17b1f7670b6de +size 6299 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_5_en.png index 6e3f7c983bf..5ed5ad8a292 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:375315a6a917f013079bfaa0d96bb3b90f72d485c3c4ed7b30c3f86dd9277793 -size 39184 +oid sha256:ed09d7879d10d22bc90bf0757bc649436f90428347cc8df2780c05e4c1c95fe5 +size 39183 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_6_en.png index 2fa7ccc26b1..dd905d53cec 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:618164f424e1b2d334051d7d55dac014d77ea00ac8f15ae0f62fdd0fa77705fe +oid sha256:8cec0dc2a2b0870c7ee43a0329f9e8b67b42e7e4f26f349543e8c5d5841830fd size 53161 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_7_en.png index 605d9d0907a..39750c36e61 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b81f55a04ecf3d4c3f266d5e0b67413be226933816195307e830d081a2bbc286 +oid sha256:5694d2ee8d939aa3cc5107b7601ff251481fc8226981208b603507d407dcb12a size 10637 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_8_en.png index acb40617137..228b02c0a9c 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:000dbaa58af2f75293be0a3fed993447275582abaa33088b3f9c533ac4e0fec6 -size 6298 +oid sha256:000c36468af4793e4956a4a2a2be1867afefa13140219e2dd8b17b1f7670b6de +size 6299 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_9_en.png index 4811773d175..ddaf40c3db9 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.components_UserListView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd6eac503164cafe15fe7d7d668fb0ab82eb81324b2bf1de3b1230e945083830 -size 37892 +oid sha256:d2882db3b3a2fa6298636f5b2970e13d61b2348fcf76deb106c93c393dd24733 +size 37776 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en.png index 249dd286497..a2876bd426c 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72855bb6717291b00777e21711c0a5c8a7049e7b09cac7684dee9ae28e5be9f8 -size 29925 +oid sha256:58a0ee838378b313643ce3ebf3ea5c12565ff3da5e4fb1edb16a795f1c1a645e +size 29964 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en.png index 1adbbfa52ae..5c3b1570b9c 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fa0cc7a8cfa1e1726d0ec75186cfd99fd67e9d5b168b0c6be669e355af2a1d6 -size 43007 +oid sha256:583665542f91d2a2d4788ff5475e063ac6168a077c75fb675fa8e4f2ae6e9aad +size 43055 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png index edb45bb3046..5b83f62f828 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79c69f5ca1c04d4e6544decea4150eb5f43bdde58070d1dfe57d0a4c94022dd5 -size 58850 +oid sha256:22928ada21a391607fbcf349a5e086e98a2acd8bd6b48ed33cc118b8b0fd0747 +size 58898 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png index 9c82b817856..f41961cb401 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d99e10bf3d0322ed25d883aa80376b0f1bd92d8174a6623e099272b4002fbfc4 -size 56880 +oid sha256:9333e27748a0699fdec91fcd805373b20776237d0a84c1d9b84b07d24cdc3394 +size 56956 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png index 3aa9167cfe4..c3a2fdcdae2 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6f28ae60d74769a903228a58ed97f5753ec6e9a054b8e94d39d6c83ecb363d5 -size 58260 +oid sha256:51ddfe120825a6c3d1bd64d4e5711e6499e8c8b99d2e341352669b1c10d21458 +size 58336 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png index 5382ae80f44..d95dd3dd575 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e987390f164dcacfafd455a3ccd3f4ae9af7d8e057d91772670d28c6f6e6310 -size 56056 +oid sha256:19aebb87ad9b9cc7808d63fe2c540428ebde6efef52e871320abd6afe4be0367 +size 56105 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en.png index d3c00e7c2f9..688c007cff6 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b420340d9471294fce2458149f14c55df979060b248dba7cdd8bacfe71a39da -size 30808 +oid sha256:67d05d7bdd0420627263d8bce97c871bcd44904a1652d6f475e9585c61883846 +size 30875 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en.png index 234cf0b7052..c30864d32b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e71ef5a6fa821961c2a78885bab91eeeb783bac1fdaa495f32fd4173ecc615 -size 43621 +oid sha256:69c5790e896024624698b386b00803eb2d21627cf47024661dcf5cea81e0b6ac +size 43681 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png index 80d7d0808c4..5ae160d9546 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cca307360497a2a298bb9968678c35afb44cdc68e2dd46a350868833bf770464 -size 59968 +oid sha256:5e06262926945b00d5625d85a662c110b1e6ce99689a89230e4c85fddf4009ba +size 60029 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png index 0f5a5043b97..83ef94e4427 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9426c57dc59d8341087b3800a40847e63b8e6ee3c4e65cfe1712b3f5fa884618 -size 58696 +oid sha256:153dea4f37cf4ff00f61df87a71229704daa4df69d34fc6072ddfcc13ff58606 +size 58777 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png index a6393e62c04..45156ac5b36 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6527ca9d39895038bf0fec9cb9c1426f95db9f4b9460eff699c34bb59d11aaf8 -size 60122 +oid sha256:eedf66a56831cd3eb577abf89c20062caf109abb8c202d7bc925e7d2fa8c2a58 +size 60193 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png index 9215e348fd7..b8f81ad8bd5 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5d4014d2701847fadd4e163c26e7ba220577c4b1e10ebd35c95b1c6aa81507b -size 57814 +oid sha256:dd57076708a9a9100b0e59df7bae44513fe811ec4091cb9a7b9c5b35d3f2874b +size 57870 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en.png new file mode 100644 index 00000000000..b46d76faa65 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddac666d01929c8df834cff1e932eba12dbf377c7954cd4fdf2fa13bbb1e77be +size 16758 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en.png new file mode 100644 index 00000000000..e73def12a8e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae779c152a099185e14a4301ff47825cfd809a81e651e36cb3261fb3371f61e7 +size 17424 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en.png new file mode 100644 index 00000000000..4c1e8b33151 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32e129c360deb12a615861c12ee2505c6e6250cbee681d4d60e58d3b0dcb492b +size 17022 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en.png new file mode 100644 index 00000000000..bca0bfcc786 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f39dc9170e9e85d1f56b8b7f1b80fbbb7d5acaa762a585a6c52b494c8b25b285 +size 20884 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en.png new file mode 100644 index 00000000000..9285a3f6a56 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3169e88f2369e5f4d6cc8d5055af99e589c3a91bcd166acc246614a1206afb7b +size 20196 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en.png new file mode 100644 index 00000000000..62a6ca09adb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff518448cacd57889acee5c330f251fa20ff7b9261d4538f89818acc217b1c6a +size 20186 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en.png new file mode 100644 index 00000000000..e5781c467d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4285614e42cec849331dfac0a8d67786cc95f768815d5818a1c06fb97f14530 +size 15599 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en.png new file mode 100644 index 00000000000..6e2fccda25a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9610111f2ad914faf1f890faa1b593ccf04ec42248f04586f403a0f8bfbd5991 +size 16196 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en.png new file mode 100644 index 00000000000..995457b04ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afd1edb1c5bd6e04ffcfe583697d24be3a18e778da9e4f991f90f3bc2da78e33 +size 15637 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en.png new file mode 100644 index 00000000000..c59ae8781a1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca43cfd340541ee672c5b79727f4033fa21a4e5351dbef55cf48999946d3a2c6 +size 19474 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en.png new file mode 100644 index 00000000000..1183fdc5ebb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60a4b7d344da0151ed6b4153ec527d063233b7a6824264afd73c73af9d5a6586 +size 18710 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en.png new file mode 100644 index 00000000000..41a619f1078 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f592e046b694f20261ac2b154b50f766ecf3228dd69525eceae35fde9e7055c +size 18566 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_0_en.png index 68f1966bade..9142f465a13 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f63a4208682c52a2d141be96c11568c9be922d68e5361bd40105aa93d5e1b0b3 -size 21599 +oid sha256:f7e9fe54b399dde0a7831ff37309c8a9d6acb5a369d54e9583892b4e2dc59c02 +size 25779 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_1_en.png index 169ee5ef027..ea896a093dc 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0838f9b076ed75cb55ea1119aa097401314bf175ead2c34ee84d7cefb65f38c6 -size 19851 +oid sha256:208e9d9f42a929c0bdb280c4442faf0211f43d3d350c48246b20ca3256a11690 +size 19846 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_2_en.png index 64cd77c9dd4..9e06a542c9f 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcd340b12fb26fed072d336f68ce6fde749025b0043866be0de0ef507dab49c5 -size 26671 +oid sha256:f9988f9337ef201957b5eaac5277cb3d1243a2396c57bd3df725b692693fa482 +size 26666 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_3_en.png index 02d2735fb6f..dff5583e535 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e8fbff0db3fb8dc996226fcb1b515638be54f7e6fec6de303e86f954037c328 -size 48411 +oid sha256:bfde1df500000d69e1014077b973cfe7c4a5130419184d19d794bd7a975e67bb +size 53080 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_4_en.png new file mode 100644 index 00000000000..53da24ced24 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0460a8404ecb789ae10223a4d948e7f93567f041342a2398b354aca39143276c +size 41781 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_5_en.png new file mode 100644 index 00000000000..bae4ff708c8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1d8378280d5b8c6487fe62d0a893cc225458d7763495b949d4e44bc14d37d37 +size 29356 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_0_en.png index 8e80f790c9e..4f37bb59b52 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4de7af3399a3942ec597b5389414e868b43e54b42c6bac50dbfd4672eb54eb9b -size 20532 +oid sha256:e1f3d513a10202d05a3cbba270e1a5d63bbaceb74bf968d21aef01fb92c9e425 +size 24730 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_1_en.png index 055b332106a..ac30088346a 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b96b798e72a139710e318a2eb5203078654a1b39126bc07eb46bc0cf5d65114 -size 18783 +oid sha256:fa26d53e0373f886180c38c7398597737dd808ef98e658a5f98e0fc74f150f36 +size 18764 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_2_en.png index 8ea80e71558..45af15438b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a1ce3733e36875756923872eb2f862c9248ad25af5189771ec29ccf3f8d15cd -size 25121 +oid sha256:462c1055336d5cfe8342d87335a3ec89f57c5d1efb4e81da8b928a390acc1a59 +size 25104 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_3_en.png index 3cd0ea6005f..87d38f9bda2 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0c79e01df12f6784e85fea66c27e2b454b34742db9c666d7c7e3f14d27229e2 -size 48207 +oid sha256:8deed7312d246c817c33b4c0034f139faa788e43dbfb5152fe613dc07320b2fd +size 52655 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_4_en.png new file mode 100644 index 00000000000..358116454e3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e08c4b737016219338da25ff1088b433c67af0ea7d74b451a99f496458a38b17 +size 40239 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_5_en.png new file mode 100644 index 00000000000..bc7f31376da --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.root_CreateRoomRootView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0efaecc5af149f55ea90fde1b454427ac38ad909358b1098a5beda9489691bef +size 28105 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Day_0_en.png index ec56a647c0e..94e57d26f77 100644 --- a/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b5294950885cb417983db822b7615dfd0c4988c0eef7eceb466b751c0834020 -size 68101 +oid sha256:e6aa9ee5920f74d5a5fef6e2a0c81ef8e0480a428161a33283c55dfeb17d39ef +size 68963 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Night_0_en.png index 3f29a09d026..f68b07b3fad 100644 --- a/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6ea578f8aa88e306f50407c48cc08243e147635bd5e94f13c4f054ff02ed380 -size 59199 +oid sha256:f36c7ded0fb9e543dd367bc7ae676463d36cd3aa449af8eeffe98966a96dd847 +size 59960 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en.png new file mode 100644 index 00000000000..2dd4cdac47c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:470cb7c851af9d91cc44d52d502b50764136b079b600ca58ae2512e7ff12e34b +size 29534 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en.png new file mode 100644 index 00000000000..08cde67f549 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49db4684bd8f16361848130650ce1ee0fbc49fff5452fab177e5920ab857c1a9 +size 24663 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en.png new file mode 100644 index 00000000000..7e53aff6619 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1504a5137b90bf6c03ae8d3a90946ae1a9b528013f9c3898a0e91e4d073d159 +size 34347 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en.png new file mode 100644 index 00000000000..6736d8bf0a2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd973e0f9c5940e3f216f27af682fffd156535b6d759e4cec01c3979eb35f1c9 +size 29624 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en.png new file mode 100644 index 00000000000..c6692886f47 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dba1ef09211361216ee035c64ee50d62ab1df5191d365ef26f5a4f741477867 +size 28635 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en.png new file mode 100644 index 00000000000..cdb70c0358b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba7502251d19d1fe54fa631b7c71afcdfc3c864cbab04d741a0c4a8d691b2e7f +size 24048 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en.png new file mode 100644 index 00000000000..7a116903690 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14770fdc3e68b250b5f7dad6bb6fb1bf387c66774287276ab4b505c144007b33 +size 33299 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en.png new file mode 100644 index 00000000000..86c1142a7a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0466b1f78eda1575072f9be443fcb5741ff9952fd215b9aa9b2281de7109da70 +size 28783 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.welcome_WelcomeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.welcome_WelcomeView_Day_0_en.png index 316f03246a3..6860df86549 100644 --- a/tests/uitests/src/test/snapshots/images/features.ftue.impl.welcome_WelcomeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.welcome_WelcomeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b80198aeffe7bfec1e5747edf031af3bb892797950a98bdbe4063841fcf69da6 -size 269244 +oid sha256:72a54bb0c6c72e992468d81df80d0d1d274bb8b94f6883df1d2891483a855c56 +size 269259 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.welcome_WelcomeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.welcome_WelcomeView_Night_0_en.png index 6271012c12c..c2a050dcd8f 100644 --- a/tests/uitests/src/test/snapshots/images/features.ftue.impl.welcome_WelcomeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.welcome_WelcomeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5188a7358c698d1a67ee483a5e9277cf1158dbeb98b94933ed71f779918cf1ab -size 345464 +oid sha256:2ea10f84e31b3591596dbfb4b3930fb108ce5d7704bdda108a8dac9b78ccbca3 +size 345504 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_BatteryOptimizationBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_BatteryOptimizationBanner_Day_0_en.png new file mode 100644 index 00000000000..6b81678de07 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_BatteryOptimizationBanner_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef31a93af4f24ebada1e1436812c01d949e75d09f1259fabbe0d7a2a7eae3400 +size 26278 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_BatteryOptimizationBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_BatteryOptimizationBanner_Night_0_en.png new file mode 100644 index 00000000000..64406048487 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_BatteryOptimizationBanner_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bf4b769592e06e707a727bae111a6ebf27ced4c272c4aaace3368049873182b +size 25324 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en.png new file mode 100644 index 00000000000..a35b0147a08 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62a7e346397466a3a98bdd2e3dfcad43d65471017d40aa719c00ac2032ff9509 +size 41231 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en.png new file mode 100644 index 00000000000..398a201bdc7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abe3d11ea9f0088529da6651526650a1bd2fe34335da31f043cfc8b1fb945597 +size 49428 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBar_Day_0_en.png new file mode 100644 index 00000000000..f3954c778c8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBar_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d079c9142b483482b12e2e55253bb02cf9ff1f37b95d455b1887d54dbfbbb9b0 +size 40941 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBar_Night_0_en.png new file mode 100644 index 00000000000..3bcc64f9476 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_DefaultRoomListTopBar_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09c215a680812b95b07501aba343331932c53e8988d13b43e8f3296d84664f63 +size 49089 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_FullScreenIntentPermissionBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_FullScreenIntentPermissionBanner_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_1_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_2_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_3_en.png new file mode 100644 index 00000000000..45eac04de3f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cb7674ebc4568690f73f34f0db69a0a796ed743b4ccab4d44ddf72bb73a97ab +size 18054 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_4_en.png new file mode 100644 index 00000000000..44466ff3f52 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4f4a4b6f6f577d8e6c7f71dc73615d806ec605579bd4dc845aef0e9dbd38b0c +size 47073 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_1_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_2_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_3_en.png new file mode 100644 index 00000000000..4a05033e4f5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df52d536c1cccf61d5aef17fb4781ebef1482138a633480713df3324b1449d34 +size 17684 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_4_en.png new file mode 100644 index 00000000000..291da5b229e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1ebc9ff9d939f9ac1dadb7c2dc187df3de32d0537cb989729c0facddb99f2ca +size 45552 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryPlaceholderRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryPlaceholderRow_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryPlaceholderRow_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryPlaceholderRow_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryPlaceholderRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryPlaceholderRow_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryPlaceholderRow_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryPlaceholderRow_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_10_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_10_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_10_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_11_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_11_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_11_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_12_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_12_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_12_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_13_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_13_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_13_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_14_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_14_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_14_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_15_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_15_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_15_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_16_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_16_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_16_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_17_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_17_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_17_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_18_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_18_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_18_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_18_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_19_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_19_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_19_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_19_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_1_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_20_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_20_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_20_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_20_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_21_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_21_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_21_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_21_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_22_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_22_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_22_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_22_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_23_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_23_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_23_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_23_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_24_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_24_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_24_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_24_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_25_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_25_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_25_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_25_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_26_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_26_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_26_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_26_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_27_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_27_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_27_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_27_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_28_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_28_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_28_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_28_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_29_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_29_en.png new file mode 100644 index 00000000000..9c35c6a4d6f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_29_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8f5508fc0509afed78f4259037c1f886b9c872d581756db947a11e688248c5a +size 20623 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_2_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_30_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_30_en.png new file mode 100644 index 00000000000..c5f164e6cd4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_30_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a92aa815f9c74893ea1b9b98912f0eefd86159af900ece588a3eb426264d076a +size 15473 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_31_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_31_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_31_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_31_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_32_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_32_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_32_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_32_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_33_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_33_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_33_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_33_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_34_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_34_en.png new file mode 100644 index 00000000000..dd6fcb47f4b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_34_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26ec46ce726cb5a36accb280cc019a1a93d065bc6f6c74027d42efb4f7709f6a +size 14566 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_3_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_3_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_4_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_4_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_5_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_5_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_6_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_6_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_6_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_7_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_7_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_7_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_8_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_8_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_8_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_9_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_9_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_9_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_10_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_10_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_10_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_11_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_11_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_11_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_12_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_12_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_12_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_13_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_13_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_13_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_14_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_14_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_14_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_15_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_15_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_15_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_16_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_16_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_16_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_17_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_17_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_17_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_18_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_18_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_18_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_18_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_19_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_19_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_19_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_19_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_1_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_20_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_20_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_20_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_20_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_21_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_21_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_21_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_21_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_22_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_22_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_22_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_22_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_23_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_23_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_23_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_23_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_24_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_24_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_24_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_24_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_25_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_25_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_25_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_25_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_26_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_26_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_26_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_26_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_27_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_27_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_27_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_27_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_28_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_28_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_28_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_28_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_29_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_29_en.png new file mode 100644 index 00000000000..8369441179e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_29_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:480a8c2cb0c3ef6d30125cae576d9737b69b3ee55b8d0cb761562098c784ca83 +size 20476 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_2_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_30_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_30_en.png new file mode 100644 index 00000000000..c4379b0b60c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_30_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8287ed2eda7e0d486f6122c5bd61517f7f6ccc5ea21413f2eea085683d15ba68 +size 15370 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_31_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_31_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_31_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_31_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_32_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_32_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_32_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_32_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_33_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_33_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_33_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_33_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_34_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_34_en.png new file mode 100644 index 00000000000..b66b1007fa9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_34_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc7cf18c82801dd7aea6383a0f17fdbba11e92f9e4cead38ac609d7a2238837f +size 14203 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_3_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_3_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_4_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_4_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_5_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_5_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_6_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_6_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_6_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_7_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_7_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_7_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_8_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_8_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_8_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_9_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_9_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_9_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Day_0_en.png new file mode 100644 index 00000000000..a261ac0f14c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:327d939634c2a2f19de767687b82bc24db3b421dcf75b5cc8560eb36bf2e693d +size 14362 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Day_1_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Night_0_en.png new file mode 100644 index 00000000000..cc3a974e2cb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14d141f1501984a71449239951e63c34fff21c1a4187e5044b846efd6b817052 +size 14050 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Night_1_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.filters_RoomListFiltersView_Night_1_en.png 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 00000000000..33e41771478 --- /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:da5423fd025457b1465b9c3073a8ffa29e24f7eec379bbdb43473b2ab4e57257 +size 25968 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 00000000000..3e218b0b78c --- /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:5830c192e065e7e59c7995d1079fd2abd08d3aa5cc41fea41505daf6c64e17a8 +size 25245 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 00000000000..41b808d546f --- /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:49e9723d3b41096d7d3ddd29601e1bf510d2a1f92e1ef0d5ada2e0d84ceb19bc +size 22189 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 00000000000..939137422fb --- /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:174037d1706a96148acba807bcb89b3a4e840a3ceb5f6c56550e4f0296f42741 +size 22452 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 00000000000..c30a6637e60 --- /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:93a9294ae555735ef7fa238f277ec694d8cde7207a270c417714a514eed0e7ba +size 24028 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 00000000000..c2aa4ab638f --- /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:6435bc1f5e506257f3a0e6cfd4fc5e500c143d7f396a44e5f74df63ca37e230d +size 21459 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 00000000000..c42cf1e3455 --- /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:40269776883b9c524236936dcc2bb4110a96d381efb68c46a32ff7739220996d +size 21738 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 00000000000..800611b7216 --- /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:f7638ef3bdead48705bac34df1450614d696a9e7ccbfdb70ed68b10bb0fbabf3 +size 23231 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Day_2_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Night_2_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png new file mode 100644 index 00000000000..b3dfe3db1d0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7b0dc90002ec59917ed0c64dae785088117c5aca61961c3d71dc7c05d2fa72b +size 82238 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png new file mode 100644 index 00000000000..2bc640fe3a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:660e96d8c627d8f24bbfbc150a3e2b7e74f6819e7fbaa581c5872f544ed9685e +size 46034 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png new file mode 100644 index 00000000000..6e0088ee148 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f65131302fab3b0ba710528947376b57230a74731aa6549473998c30f4e07f6e +size 40681 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_12_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_9_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_12_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png new file mode 100644 index 00000000000..ca0f363c491 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20e6d5a1c7218304ede990c1906ec628593acbe57a6496b98a2c1fe9c87d1ae3 +size 105294 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png new file mode 100644 index 00000000000..b7e12785912 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6041a7693307a30183606e7f8f2b605ba4c9bfb8c7ad5d8122837749813d6e2c +size 99928 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png new file mode 100644 index 00000000000..ca51b8eef3f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:765619612593a9821c31afb6268259890d9374099f5e26ae9897aca09abf06e9 +size 82801 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png new file mode 100644 index 00000000000..b3dfe3db1d0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7b0dc90002ec59917ed0c64dae785088117c5aca61961c3d71dc7c05d2fa72b +size 82238 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png new file mode 100644 index 00000000000..f6988f9732d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51d89e7c0e8c205c230351c4a00e0d560f3f54c6b423ae3b2b801fd6d00343bf +size 83509 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png new file mode 100644 index 00000000000..ec5767287f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7383bea26cbd09cff981e6b4202f7e9bca788094ea482241b45eaefcbf16127 +size 34927 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png new file mode 100644 index 00000000000..b3dfe3db1d0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7b0dc90002ec59917ed0c64dae785088117c5aca61961c3d71dc7c05d2fa72b +size 82238 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png new file mode 100644 index 00000000000..20806bc42ca --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4385b380efbae924d4a8fece278a61003d40acbd795fef1aa2eab4c5fadbf367 +size 60691 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png new file mode 100644 index 00000000000..1b34b511fbf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d64fa49cfca399eb073a8efac881aebbfed38726116d3113f9a562245ac9a41 +size 60495 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png new file mode 100644 index 00000000000..13db73a34eb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:020d732b7b3a4f43918bee7b9424ffd03fe20074c5823341811a1c700551b2f2 +size 58734 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png new file mode 100644 index 00000000000..114ba675cef --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63f8198b96e49f9a29dad22ef2249503d1528aec874599d9daab3f8634d662d7 +size 99704 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png new file mode 100644 index 00000000000..92d58a36cd8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca1e0f43d447faf770ee7aa4f443f3a310f520c9437cbd5baca9ac7ce36d2aae +size 88613 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png new file mode 100644 index 00000000000..81c7db17ae0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6be30df0d8f5ba0a57c720bcc16eefc3a5b7dacfc8115a8d47a42edab3e63e82 +size 53250 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png new file mode 100644 index 00000000000..66309e8b713 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c6dc3c0c72d1ea8a980f393d7cfe2c9e0392016c8fc25b901817dd077d8526c +size 47265 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_12_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_9_en.png rename to tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_12_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png new file mode 100644 index 00000000000..19375d3ec57 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c93289bc30c3b590360f916a55a86a5cc98028bd2201cb410f329de85ed615c +size 111059 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png new file mode 100644 index 00000000000..6cafa75e882 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7563629f9df4012d7cc35b2805e1ac381ccdb171935b35cad0d1ea60186e3e7d +size 105496 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png new file mode 100644 index 00000000000..f7edb970451 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7f9c804e0f2b75c84a99081d33d12c46ef2aed62aaf044ec1f957f728f586b1 +size 88564 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png new file mode 100644 index 00000000000..92d58a36cd8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca1e0f43d447faf770ee7aa4f443f3a310f520c9437cbd5baca9ac7ce36d2aae +size 88613 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png new file mode 100644 index 00000000000..1c65bb2ce73 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3962f724784add8dc33c7327dd866cab76eff0d53abb781b94e974ae68212e0b +size 90485 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png new file mode 100644 index 00000000000..9b2a3650ed3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4850c3002637166016ac75129c0fd4081c540848969b40eb84c4f60a4366c111 +size 43857 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png new file mode 100644 index 00000000000..92d58a36cd8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca1e0f43d447faf770ee7aa4f443f3a310f520c9437cbd5baca9ac7ce36d2aae +size 88613 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png new file mode 100644 index 00000000000..611da1630c8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acde7691ec0ebe690bcb29478963107e2dc0fe6fa54892b3600f50e0cf1db2a3 +size 69463 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png new file mode 100644 index 00000000000..642eca994d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38e55635243d77c084053c4feea03dcedabd549aa556f4a622ec2d629bb4f426 +size 69240 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png new file mode 100644 index 00000000000..0604b0bc731 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fc45bd4279456b75cadcb8381451f9e6a0e1fadaf4a8f22274c94aa89c7c348 +size 67496 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png new file mode 100644 index 00000000000..48835abda62 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d9f42109a12e450a13d1166f7d7790913e1dd847cd44f00a2c881d825cad9b4 +size 105496 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_0_en.png 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 new file mode 100644 index 00000000000..934dc43d9d0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db72f73fd89896d575cea546e3cf382c7be3736af6c3db74fc2301434939d99e +size 21960 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 new file mode 100644 index 00000000000..b23b2c98d77 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:828b5234e2bfb40965b7b17362402b8e77332d11f0338443866153d6a9ab5437 +size 25963 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en.png new file mode 100644 index 00000000000..6f39152c181 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db20190581472583a2f493275d0f7dc46ed4a2128cef475b98730e3695b918c7 +size 19149 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en.png new file mode 100644 index 00000000000..a196f7839f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:784fd291db5fc825b8c1f0bac5ed3066e3aaa67fd6cbc35fd65b0df92f7cb052 +size 20504 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en.png new file mode 100644 index 00000000000..6f39152c181 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db20190581472583a2f493275d0f7dc46ed4a2128cef475b98730e3695b918c7 +size 19149 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_0_en.png 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 new file mode 100644 index 00000000000..c299337e546 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52ee0fa663944b2972f16675c337f52e9bbb3912aea1df86200965fae2cd9849 +size 20040 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 new file mode 100644 index 00000000000..64da9e64bdc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a51609d679d9ce799a98b7215e99cfe7553c1a154358f6f9ff7d5e0048e031a +size 24105 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en.png new file mode 100644 index 00000000000..67ef6464723 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4557838eceb90ec501062367413359550ccc927cd4e0c50c9a01c2d9eb0938f +size 17146 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en.png new file mode 100644 index 00000000000..a205f24caf4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e620cc1fe0e21a92da88948bc8da2db49c3bcaea0480f8abaa61e22a0882a56e +size 18341 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en.png new file mode 100644 index 00000000000..67ef6464723 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4557838eceb90ec501062367413359550ccc927cd4e0c50c9a01c2d9eb0938f +size 17146 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en.png new file mode 100644 index 00000000000..9b7e2b3ce92 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d7f72a828564c9e798c7b207befbf585753951b04a1c32debc2e10b25bced6c +size 33179 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en.png new file mode 100644 index 00000000000..92a82fefb1b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f8c6170e259cd1cc8579d750ca4dfb68d40e1d2994757c7e3d57a0b1931a593 +size 38072 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en.png new file mode 100644 index 00000000000..23f6835b3be --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dce84fcbb387139cbcbd5907bafb4954b6b8d52209c45e251eeb715973232fa +size 33366 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en.png new file mode 100644 index 00000000000..d23ab2365ed --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4777b45b61243ce502af6fc69d9b262bede2362c9f556bbbfd66d2391d4980d9 +size 30172 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en.png new file mode 100644 index 00000000000..0a543ce4411 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a1fe6b47d0651ffbd7587fc56f1be0fc8771dc31648050eb98d097198ae4d53 +size 34583 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en.png new file mode 100644 index 00000000000..386d1ee7209 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e9c0bdaeabc62c5a66649bcd37f207dccca268ea8bd2d118dc151d6a2d55601 +size 31817 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en.png new file mode 100644 index 00000000000..ba9f9bdf0dc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c69cd79f91cbba2a72682d229fbbdaed566302067ce6c6910a613e15cc67767 +size 36455 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en.png new file mode 100644 index 00000000000..3350b2f936f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:056cedc5a79d692c1cd81eb163e22a809ea52053cef85b8a6ab08cfabc316474 +size 31965 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en.png new file mode 100644 index 00000000000..5cf3f2d3578 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6f494a918082962ff121a809a54a5cd00911af35c85c0a2f4a49cc8475ba210 +size 28300 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en.png new file mode 100644 index 00000000000..14687f05316 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86cf5163cdf20bde56e6a0d858d69ead4fc4275c38c595c0debbd743ea0e9770 +size 32107 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_1_en.png deleted file mode 100644 index 001133641ef..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:293b11b5ffbef9253c51922e84c707d4404eb6945d6f99d64ff9abf7c1f632e8 -size 22186 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_2_en.png deleted file mode 100644 index 0af5940238b..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e36bd9c0358f68a2b6a833677775b8db9b93c4d1851e9adf8221fb8b5a005cd4 -size 22767 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png deleted file mode 100644 index bb2c035f902..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f5e0ac8b492e4152a0eb8ef9cb0b6456b6cd9dad9733a23c99ad53def2d15ff -size 9962 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_4_en.png deleted file mode 100644 index bb2c035f902..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f5e0ac8b492e4152a0eb8ef9cb0b6456b6cd9dad9733a23c99ad53def2d15ff -size 9962 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_1_en.png deleted file mode 100644 index 76355d9a9d7..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:84adf676f352a072e79269d53b5b572aa3ea12978d0c6076bd442d83db6aeeb3 -size 20110 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_2_en.png deleted file mode 100644 index 2978d98e7e0..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e9a7acf590703274148b0887d0bf71934ad9ba0bfce6c86c8675f2277c49a0bc -size 20851 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png deleted file mode 100644 index c35c13d07f4..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9e05f1b1cfc75e0698e82c76985255d43838fc9e07dbfda39a5f7b27dcd7dcc8 -size 8596 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_4_en.png deleted file mode 100644 index c35c13d07f4..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9e05f1b1cfc75e0698e82c76985255d43838fc9e07dbfda39a5f7b27dcd7dcc8 -size 8596 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_0_en.png index 9aab291de01..b0358452a9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f27cb5420d3537479d18e03cb7060ee14b1a3fee46fa12a3e1473b358e833d13 -size 92644 +oid sha256:11c79a9b9c4deaa671d8e8b8d9ce4ab0a681edbe53f906110bcf163e22336841 +size 10172 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_10_en.png index 6d0ff2554ec..21150eb1544 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9d0338485aabe5296754868ce2fcd5097d914eea28298ec11c16f7161ae2341 -size 111617 +oid sha256:9163c7aaae605af5470506d99eed7ecd19374fa7e719f8b0318e55f1319702a2 +size 43406 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_11_en.png index deabc9b36a5..1b43985f3f0 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45040e44ee78a5103ebf1a3963c24f1a201485affa3f24d8f707e4f99be6cafd -size 112406 +oid sha256:634df4b9fab9797663e11293e5894de9854e918184d227e73e47be047bcb9623 +size 50335 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_12_en.png index 15fef4a0842..210a02addf5 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6eee1185065c82f90cf53d7b9fd62e6de72d907606099b0536b6305ea9537f2 -size 117250 +oid sha256:a971216d0cfc58f65b3589479420c02f2a04010ac27d79ea2434075dcd6b4f7c +size 51283 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_13_en.png new file mode 100644 index 00000000000..6f8e9c095b8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc93b58449602518d7fd6e14334bba0793a5692f478367e825b75c6ca9de685a +size 29451 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_14_en.png new file mode 100644 index 00000000000..9e8e6676441 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:355d17b9e2bab7feb55b7c958f54e1c5e90f2c4ac58575c2868c5e15414eedce +size 29097 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_15_en.png new file mode 100644 index 00000000000..b34742fa47e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_15_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5bb0724cbb42ffe66a8c54e4301eccfac8e4f2f68621697c6859aa6a230405d +size 36376 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_16_en.png new file mode 100644 index 00000000000..6b0197002fe --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_16_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dcb5fb864d2692e62de1161e3420a3c9e9d7a338110b5e3b7791753e7995b7a +size 45863 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_1_en.png index 0c9e5f8beb0..91315f00563 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6319cbc723b9f3cf0b89c4ec5dc6098a7b6a2a2c053ee5bf5afc21830b8d3f6 -size 110855 +oid sha256:2ee424d3f53556823f1bc19a1bf0d742918dff295962f4dda371459c8607fc15 +size 34990 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_2_en.png index 5e59459e907..0e31405365b 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57fcd25c0dab243b8ce273d38d0c4f36bb3415ac4488d1c6f4189c368899ca76 -size 98786 +oid sha256:6f19a41fa98afa02f2d913c02cfd8cca6e53d6af164ab1c81d98cc75ae877bbc +size 36305 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_3_en.png index a87a8a56f66..33f9384163f 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:420a7c957ef0909cc2bf9cd9461af98cdff20fff21dab5df9e94165034ed0dda -size 110641 +oid sha256:0e527cea5e937b1ab89fcbc0388ab5d81008ceef8e85b25af227f4c8465df268 +size 31504 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_4_en.png index 3fcad9605e8..9eb729467d3 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3463e70cd08f4d884f8bfafe7d66c5b75ac1fc05bf70cf1478a9f11def071069 -size 111151 +oid sha256:8d3b3f549530f475fd984f839c7b28a7963ad7c8903b5f308d3925a60132991b +size 39468 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_5_en.png index da1fd14667f..51e9f08da54 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70c33e148a040ec24287f9ca48353a76e8167015f24d8249bff405e9cc9f16ff -size 118640 +oid sha256:2604ac285ee6334a46fe348313fe2abe655860bd9b54f237fc92d0f99e38a7be +size 29162 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png index aaab740c4f2..c92e767822d 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b3e666dc4693d7c096dd5c0168770f72be41c41797a93e7c728a20b587ddaba -size 92658 +oid sha256:2770e628229279569dd66e62395cce39e763ffdf64ddd9901e72afa3c7055c07 +size 29911 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_7_en.png index 28403e285ee..1353e970a46 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9af5a8c8a529eb5f8ea40e9c8028ce2c0ca4b352ef80b2778fa5122cc6bfa937 -size 96826 +oid sha256:8286e517a48da892830571538a9047d9d91daae01400ce0ff19673a932debf96 +size 37727 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_8_en.png index 8cb6932f435..fdc89929c12 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:084e303ed61e399ddbefda7e54cadb69545eb0db9493185aa48a3db7ba69fb04 -size 112515 +oid sha256:18076ab6a11a67d3a89f0cbdccab96a69f40acd3c473fad28681a5d1901799bc +size 26607 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_9_en.png index d28af79e36a..fdc856fe155 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:916516ede645eaff617b40e6cf50acf2ba1e583827d778d22e9a012fe367fe7a -size 106146 +oid sha256:7f4cf59462d6f5ed3de8b398b6ecdb4a6c4842d7d76637a4c97079051412365b +size 33567 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_0_en.png index 4ffef60edd3..49c7233279e 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61f2ac89dfbf76937344609afe536c1c9ea6af1134ebe41cb3b3608ffe51b0d0 -size 77863 +oid sha256:bd88a9505b7c9c1a76caf35e918f362ab9a96c64f719b9811977fe3031dab557 +size 9984 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_10_en.png index 17884e72828..b2f18605433 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88584b1d4a4876e68e2972f9bbd03156127ee9b552de1ec20a9057d8f6cb1828 -size 101296 +oid sha256:e01e9453b8c72e9a1a55bbb4f14e88836f93d1d9b28af10557aec81eae847d74 +size 43374 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_11_en.png index 1f4cc5b6168..95227bd8198 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cafbd46dbff04191aeabb2ea403d99728972c1854116022e545b4ca6be7bc675 -size 102209 +oid sha256:90b00f22d1d2d1b22f6260c95ea3fe39851682d258df7012b89c68db2af01601 +size 50133 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_12_en.png index 29dcd629aa0..4d7d868c646 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07458d183bae7ecfb7ee61bb6f2e0abb9a4399dd373068002fec3722f575e754 -size 102438 +oid sha256:18d2e25e32b0dc63b72a2c8c32213d2c7d9d499bf568b88addf24709e433ce34 +size 50997 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_13_en.png new file mode 100644 index 00000000000..1e702f7ec92 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cbdba568da3e23a836524fbde2d431268d6ce27408a3490fa3bf1407a2862e0 +size 28729 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_14_en.png new file mode 100644 index 00000000000..2f01ebb7a34 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91fc0e147f5f0d7bde4d33082ae84e962784a55f3f63af9de7fb6ba0ecb8c340 +size 28860 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_15_en.png new file mode 100644 index 00000000000..c83a9db1b0c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_15_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66785e81ea5d103a8ea1aca54e537850d67becb84f3b4cdb463189b7b21afa68 +size 35453 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_16_en.png new file mode 100644 index 00000000000..b220116942d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_16_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bed222380fcd708584b52aa098622487025598c870ac3ed68694dcf7e900038 +size 45755 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_1_en.png index 9c14a8b24af..c9777afe5d5 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59666c1ca888ed089d7cf08fd75cc2f3e993b59c255317ad1af652aadfac8b4e -size 97026 +oid sha256:445c343404fba0ed533ffe07a138ddf74686f67e3e163626963be4e5db17fd7f +size 34479 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_2_en.png index a72228043e8..846b7643c0a 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e9618ede3ef8f718c54b3912fbbdadf69182417c4f7cc714a8301a84deaca2f -size 84239 +oid sha256:2e2489ed1cbbd10709456b0a910b8bbb6607732bbf40c4ea30114306875fe999 +size 36282 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_3_en.png index b1d43106ec8..e02296766b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55dd6915907ebe7c0d3799a54491640582f3fc08c7d8163fb77cfd7820b95ad3 -size 96938 +oid sha256:5b7b89d817b0d1847828b214a1340fc041e85a44d43fc89dae64e6c56324ac41 +size 31806 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_4_en.png index 9d57ee0ac22..a7e826f26c6 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1e7887e1120c852e5a9086dba35fd8e52485d0e285c2e3724687ffa71babd45 -size 96405 +oid sha256:913a9b7a9abe56116062436d405c97105d87d07ccbb012e65b5a6365974a6d66 +size 38439 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_5_en.png index e0062f755bb..8c700b0e526 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3d75eca5904d605b91becebca60199f7e60e6f2bec6b9a945ce8d130cfd7e47 -size 104802 +oid sha256:1ce6e3a17d5b092b4f292659cea45834207b4956e273b7cecc69c7a07daa8d51 +size 27255 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png index c382416a8b0..1f66f16ca98 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e97d78225215b0b5e13f369485e7503d020baa7d157cb1c6826d18759d580c6e -size 78221 +oid sha256:25fc6358031c6f3b10401b2e8e5227ae0c63b36bd77e0d2f115eb19988f8cfbd +size 29242 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_7_en.png index 65edf4ebd39..86195807d19 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95f818f0426868414b0ef5cb525254ad67811ec777c31a435072fb2236914f22 -size 82448 +oid sha256:58178d173a6575ea5bdfce718f5d1686767f7078e19df2c18b55dca7ff6edb2f +size 36944 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_8_en.png index d979aa4f0d7..fe70c0a848b 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:050785374d848e773941d858848e34f5d1753fcdada4dcef548a98eb5e78c31a -size 99409 +oid sha256:25e8c52cd120b0fde6c15f7a31e35f6952f9e1fd5689b741fe029a91fef2cf33 +size 24775 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_9_en.png index b1833d04599..cd0035f6a13 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9f9d59037ba084e60e6324e066736bc08c0b52319664d67eb84779df6806ce4 -size 91377 +oid sha256:7ed31ce5baa82c06abb3e414876c0a9b70c98f2223d0c432d31ad3f01bdb53ca +size 33343 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_1_en.png index 2548547d60b..fc732f0feab 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a67bb9ba78ea1d8802ce503e7d6a16be2ff46415df0e435efd7a36f3ee711b0d -size 26453 +oid sha256:4b588d1d5a02e4ccfd2e5d44d3c38723612e5fe4a051c5c582bc81bd250850df +size 26452 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_3_en.png index 3c0c73be86f..acf052a4dd1 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe9aa023cb136da6bcb54fb4c371c16c9526d81bbda718ffa61a6a7e758fd64a +oid sha256:e533e9b4a2e10fed24d8379b2f2d96e5a2cc460d9059989f8ad29d535684343f size 42170 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_1_en.png index 613fccf45cb..9b2f4804f05 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be2ae50ce1b0e23fec7bcbbe9ca0f0381af4d0f29f1e7498be837d9d53b75542 -size 26003 +oid sha256:fca69fe3a6bcaae65f686e3c26e2b6935de8e33c9080b0e9f3e58ce3448c0573 +size 26004 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png index 45b83f26b42..50603c16d88 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc8f93a9c4ec2437ab05f1f1065d5518719942d8d55550b9911384442346c463 -size 41086 +oid sha256:084ae1a4a101781ef76bf1a3f68bfb1e8c80ca6ab1de773eaddedb92a12265a5 +size 41087 diff --git a/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Day_6_en.png index 49004b08fa1..3ca3f74e6d3 100644 --- a/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9772de041aa3819dc1f322000fc49a0f2c48c5b978181406da9e64b6ac083f08 -size 32821 +oid sha256:a723573878b837d6774d6fb5bdd20d661468c991e1f34054b6551e5c969e5b11 +size 28385 diff --git a/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Night_6_en.png index c88ef3b46a8..243070e00fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.leaveroom.api_LeaveRoomView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7bed5155e5d41abe69b8a24905a5aa2b4935f56c7b04040206dfdecd59951b0 -size 30817 +oid sha256:1288017d71303ce29628cae7837968b4ec12576fff096ea4c4f9b19bbaf5a2ec +size 26547 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Day_0_en.png index 05ae7c88958..4114b76ed16 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55663ac9ab6472fe7f302d7f74af536080764a3e581ba50bb8198b8970b35e2c -size 19164 +oid sha256:577085f16207b31d905cbe94bb60c61c0c7b6be02f01cf6883f98a00eb69fceb +size 18768 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Night_0_en.png index a77bb5c4527..96789215737 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32c9a5768233fd07c69612dd839f64a2674b9f6905dc432400b93ab6c8259a34 -size 18933 +oid sha256:25be51215130a793208edc2e01d0c15d3151bf9d0543e7d64607d1f829e8fa33 +size 18559 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_0_en.png index 4f59a86c874..4bd8480af0c 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4c96790d61f5ae03c3b32d5ea85d2b1efddc84f248f2373290f28c15ced4dc0 -size 11443 +oid sha256:44501f02d607565bfcf84d83d53288efb93939e6325eeec1901481941074ab56 +size 10861 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_1_en.png index 3e28ef04c8f..49ded6b6ea5 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c19cd3e9a9e76ad26789ca882e19574ddd8b979f46e2da970b1a31fd6793b81 -size 10659 +oid sha256:4127991f99717671dca28e736e992d1344d9f138bd4a7600cc6d352d7ede3b7b +size 10079 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png index a967566289d..4711d360099 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f4a7102b45fc1acd7c8cba59282548ae36bf8a3e65acc12c4041990f0cc61c0 -size 30165 +oid sha256:42ade89c88841182bc0c4295d289eac4dde26bdf763a216944bdf6dc8894a4e7 +size 29491 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_3_en.png index 683799c4949..b5ff98014dd 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b16d4f9226d4df9efa9b57a0f6573e9fd6b0301f2e4cde2794de2863cf31ac5 -size 31373 +oid sha256:91a75224ef7854250a2d5b6d525a1481b6c9a7bc6fd6adeec047ff909d759095 +size 30751 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_0_en.png index 3f468fa931b..0e19cf631c0 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da6d82da577060eafd43bd4c13867cac99143fb8bc877f7149db4c3f4aa6e81a -size 11088 +oid sha256:e8d582ca5d78964c333899eaded2df2febcb238e1e096163ecc55b7ea36523f7 +size 10645 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_1_en.png index 4481510fcb0..7fc56dec827 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cade141584d56fef5ebe38657c3e953914a4e7c7c7167b114327fcae913e50e7 -size 10324 +oid sha256:c5ac6eb568306add765a487eb9156132eba4195847256c9d5b9f73007a3e6d5a +size 9892 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png index ad9bc11f602..2ae6d2f3c64 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5ccee59f5f0fbc79d252886b647003ce9b3e3b7cb8e54f5c154c37221e30c14 -size 29382 +oid sha256:cc6f5fd2548221be498d6788d394bb901467f687c692d73b425ed3f5ad2cb129 +size 28843 diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_3_en.png index a47858aeb1b..1028071e942 100644 --- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7afe56fbb521c23252ca5b375a594824d75bf80d9571f830acc14b0f9230d944 -size 30549 +oid sha256:646f948869e9a6cad317255c42317aba153d1e50fc3cfca0286525ee048ebb1d +size 30046 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 91297a8544f..5affd0ac127 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:aad48b326c07ed1eda341817b9a30cc74ef6cc83dd80406dd26aed858112eda8 -size 252914 +oid sha256:7275edb15579beb09be957ca2fb42ac95fc3131a9349446920db9797d79efca5 +size 439078 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Day_1_en.png deleted file mode 100644 index d46b4da2f56..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a22cacdc5b1c38a3b28e98b4345fa55d7b598fc393d8f31863d5830de9a8497 -size 255334 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 7e5ffb533d2..3eec5f639e0 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:e63a6eaedca373d7154c66d7f9fc5f52e4999dbfaafe00bb06cf94a86e2fb762 -size 105538 +oid sha256:50de6c03736fc71fc3029dadf353c212bfc504a643d6216594f5417357daf16c +size 173445 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Night_1_en.png deleted file mode 100644 index c918bcc2df4..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:96456bdfe75dbfdefb7a500435901fba66a55581373f5091fb2a8556190ea029 -size 106910 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_0_en.png index 7ac77ce99b5..db910993cfe 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:522b94ccb2ff37b449c554890fd21be992d444afd5e5cc95fe11b45bf5401ca1 -size 20166 +oid sha256:e2d9c34ddfcb745a18a26dce9303c1208329f081f4ad1c67fcd394541cdfaca3 +size 20167 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_1_en.png index aa9cafdf6ad..d8b285e71de 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15978b36ce15235dee3fe52da0693797bb206d641043f405b94e9f96c48a49a4 -size 35752 +oid sha256:1f42226557021bcf58eb0049b43427d44067a534d00c9a4a92b4a468e0580997 +size 35756 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_2_en.png index 43781cbc301..8d3f26311ce 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7a39ae309c00c4b1ee4f2777f9a9032e8138414cfc6a8b8c79e18004979d0ad -size 34048 +oid sha256:40b31eaaca9bf16a5ec5564369f0aa787e3c9e946debcce73175a7a9758528ea +size 34050 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_3_en.png index 7ac77ce99b5..db910993cfe 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:522b94ccb2ff37b449c554890fd21be992d444afd5e5cc95fe11b45bf5401ca1 -size 20166 +oid sha256:e2d9c34ddfcb745a18a26dce9303c1208329f081f4ad1c67fcd394541cdfaca3 +size 20167 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_4_en.png index 2c1cf66fdef..3f1ed0ff94d 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f6f1cb0d707a79ddddf0b1c5089eeb0e35216560541b85817c902d0b172415a +oid sha256:c97c5feaf589f42a5b183eacdbde8d130cd4bae93daab467d60a1a9c4025386e size 20252 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_0_en.png index c75dd536e93..aa7d556646d 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2024df21d3b248a8edd6737fcbadf707f7ffdee6ecfe89a4c9eed27aa1b41d4 +oid sha256:892239fde8809ae0e1f8b59f36c934cc43c94d197662364d890585b4f60ef1ad size 19518 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_1_en.png index 8e59598a5ec..b63dc915288 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3b07595b65ddeff6fadaf1468f9fc5526ba7827926e97ec54edf4f59161aa19 -size 33613 +oid sha256:d5b5e4ee6eda67992beacf2f2d497a46a372c95695939488e56ad3a2f5bc6ee0 +size 33614 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_2_en.png index d7b0f6bfd6f..e780203e40c 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9c8f907a1aeaf5300a6b9d3aac5abe1043ea5e2f47b876bddf3582b6b077475 -size 32218 +oid sha256:60f3d5d49b0b6152b2a68aded2bd9634d7b2c9318ca42b0d1c7edfded2c582f4 +size 32219 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_3_en.png index c75dd536e93..aa7d556646d 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2024df21d3b248a8edd6737fcbadf707f7ffdee6ecfe89a4c9eed27aa1b41d4 +oid sha256:892239fde8809ae0e1f8b59f36c934cc43c94d197662364d890585b4f60ef1ad size 19518 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_4_en.png index 9d624f724e0..a4a62ba2e72 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.send_SendLocationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6490d5ffc0c412a3fc4738b3eaf058d42621b929fd1ccbd9323923c6b2d818a -size 19753 +oid sha256:f5371c68c8f37abcc750807d24172cec499032ba01d3de60b32f5eafd81ff534 +size 19760 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_0_en.png index aafa465d895..d484968f398 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f5f9afdf10404a81be8c8c52b6010edd97d99693491d11f0fe0bcc4fe16c7ae -size 12045 +oid sha256:53da1803ba3f47dc835a82a52487ca2e7cec285c4651837430720a3c00274655 +size 12129 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 bc9adb2c36a..7c2023b830c 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:950f93d7323a7b07fc522bce7cb85f4977434792669a35dbdd0d23f4d21f1534 -size 30416 +oid sha256:88ba6efbc5a752de4c2468e8a957b24bd79ec8594e35bacc5f27a7645a432421 +size 30503 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 006b22ff208..374728ca6a9 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:aefb84fb5115bd11701034d3975ae889c347a9bad047cad8b613337be366f74d -size 28782 +oid sha256:41820d7e434e0a5af29dcfc3fcfe118a9d504c5ba1dca32026bd04b5fcda6ffe +size 28863 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 aafa465d895..d484968f398 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:2f5f9afdf10404a81be8c8c52b6010edd97d99693491d11f0fe0bcc4fe16c7ae -size 12045 +oid sha256:53da1803ba3f47dc835a82a52487ca2e7cec285c4651837430720a3c00274655 +size 12129 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 465b12d2eb8..65bf4223a31 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:68b91dd3884b78567f2d07a0c95eed16dfba6ed995fb1b32af0d2249f16a1eb7 -size 12243 +oid sha256:7ba3ec0aa2268d2512a95fa9008baa5ca567a29d05376bf91262ff08c0686539 +size 12296 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 a6271b10b99..1deb0e5928a 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:3e22f506097d964b21a66508219644f8f4b989ec7bf8ae22aa4f5c44ec337818 -size 15815 +oid sha256:ff515e0da1a57182966e9d46f3a6532c0c84a3f9b50ec31b0489b78ec77ca853 +size 15894 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_6_en.png index 0096c63e183..2af1ea15867 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2867b178672ad79489cbc97977aeffc5ff1575bbf41f42d5b9183abd871df737 -size 24412 +oid sha256:1146c7ceb204d2e45652cb17918196bd29ad75f62d97a58533a2eda076eb999f +size 24489 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_7_en.png index 40f21a974ab..e9ec889f769 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:937198fe6c7e90e5b55c449eca3d020358c02bc658ae9541702ea4b41e589448 -size 26772 +oid sha256:c5ba5f2f842d786ec30b98651d4adcc8fc5ed24a3754352490a095a78d40dd1e +size 26846 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_0_en.png index 69c6ba35d84..8112eb063d7 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7468007bd3ae791b1d440040ee4479cc9af4377275c115f8f8d78b74eaf62d4 -size 11524 +oid sha256:3d0368a621d6da4d6989f269cefdc5305f474c5275b20cd2b6df6ad6322c9d23 +size 11589 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 0dd28ad2cdc..abe7c4fa4cb 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:9d7551e2a174aa2d9cd8713c2dd04fec4866a119692e7baca3cea5722873af6d -size 28518 +oid sha256:f320685885792f6f677a97618655565df41b6f574760dc9029af099311ef23b1 +size 28594 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 2f0992bb1c7..53f34af2b42 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:10d381a2457cebe7dc77b6d63b1d01f96f5554511fbc76d84034128563d5b7cd -size 27130 +oid sha256:d0ffa8f0d5cd2185ec4ad2a52992ebceb4e5d066f79228a94e530da17cf20ea8 +size 27206 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 69c6ba35d84..8112eb063d7 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:e7468007bd3ae791b1d440040ee4479cc9af4377275c115f8f8d78b74eaf62d4 -size 11524 +oid sha256:3d0368a621d6da4d6989f269cefdc5305f474c5275b20cd2b6df6ad6322c9d23 +size 11589 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 33b3884a548..b11d08401e4 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:1c3e3e4eba5dc82e22908640e400c57c0a2d01b8adb007ee2aaee4fec175ee1f -size 11706 +oid sha256:8b79aae2fb29e493a66c7c62631d846fff152aa83b41bf8c83cf45f9e74b4af3 +size 11760 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 173c8005536..7e344d02181 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:ae22df52afd433d81c2e146cef6b455630de4e1d76eb2b30628ec9b8f87a2522 -size 15011 +oid sha256:7b65884ce154dd3d08589d4a035c710c42610f7b7c3f6d9b830e05e72d166d7c +size 15073 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_6_en.png index fe303acdf13..dc6ce7bf775 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6663d8bafc3176cfe83d486821ec91ab3204043ee468e689e159ff0af9e9fa5d -size 23411 +oid sha256:bdcf502d7406cc8b92f8600e046691e0b76134f3193dbb16d7e3af06e3ed2e19 +size 23477 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_7_en.png index 257576e140f..98793436430 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e763e8bc81c459b36caeb1b8a74ba262109e3c0ee1bbcf8436d944a1737c8601 -size 25920 +oid sha256:a7f86a135e2778eb533f42a228e993203c67106ab62800df03c9e2f0a01eb078 +size 25991 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en.png index 1f9643d227d..9000d2b4ed8 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68f71efc622decbb6e97f9e2f72e5a0be0a8ef555d26fe608d0400773ccaf3cf -size 29666 +oid sha256:cbf674d0dc11fdbabdce010624c1bc2ec8c61df4f886aa2cb69472d57c410c8b +size 29553 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en.png index 117da20335d..138ea3df7b4 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d920c3ed177616383d64dc1518d5bdc8784a95967f06d7381234d73c0d54d77a -size 29282 +oid sha256:ad606f70fbe7492f8e5850857acebcddb744effcff020a59b95ea6bae6b930ba +size 29181 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en.png index 409412027d6..a06cffdc7c0 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1665314b2c4865355fc3ff926d07e5fa4cd963cc4ef0adb95776d60031ebaa14 -size 29158 +oid sha256:37294aaf51ffe1989c5c3b6bbdde6511d3fae68099ec1601d85dba9b33465f02 +size 29053 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en.png index 8c157977da8..b42341f2360 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d7d944d8404f9f8dde94309d387e4c6d96dd85d3363fe576626b6c771c4e5af -size 25304 +oid sha256:12935728e54933b0fb2c8bb07430f711e5d097096f175c76b82700b14cb3ccf7 +size 25202 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en.png index aa145aa406d..b83636a0522 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f63978bd65a91be9a3c448dc10411a813e6853860460852c74877580e6bb09dc -size 30026 +oid sha256:706b8cb5ef0be1a5c7d4c8b6b78389c19a6ae778d969373efe27c4e66c790b1e +size 29925 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en.png index 1ff77934ca0..72658168b81 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb8020cfd58453ca01acb493d95fb3395d482f590f050bb18ce625220a358c83 -size 29135 +oid sha256:485af9ee0ac675c3d2fc5b6e0b64f6f80fa0daa3a54808b92dbef2f3aa1f3ee5 +size 29029 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en.png index 21f1fcc63fb..ed5b8702f27 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59980a74f85d06b67bd743214472c079ec770f3764c552c17b83a2bcb8fa7ac3 -size 28792 +oid sha256:f75a8548e2b2385d96049021652a2c0d7f062ed88158f0d8ad35543d4345bb8f +size 28688 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en.png index e2cab434c29..b3ddb9cd870 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4668416249c9969e61387674f2a5e5cf4a67907707cb87a956411931fe2292cf -size 28633 +oid sha256:cddeeac1e247ea2c3a53d7a63665d3b4eaff1f6d76b871cb09ade0319034a1da +size 28512 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en.png index b536178092c..56a6b69a256 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d79a5b879d4f992b563f2f1cc38baf73a618f645a6987603051b93331db3029 -size 23343 +oid sha256:e44870aa5ba012e2863b2f696b43a653755d6d7a8e7b5a6181757c2ee3ea4bf9 +size 23222 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en.png index 6a944e9f186..62a5c8e451e 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56db6c793f9e4c381ba20a09ed587402c948afa1265e15ae8a7916ec60078b8e -size 27964 +oid sha256:ed50349ba3d7de68ef1a4068cb0c064d087c58ffce4efc5a56a4552647801c98 +size 27842 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en.png index 9b96187e60a..df6048a0b07 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4132ddd836c382f198a5dcfc6c33e232ff55fcad78408b30f03e94cb1977044a -size 20530 +oid sha256:5123604b77467975e45599a7bb35c5b8cbb7311cdb22eb62f910675a96ab79e1 +size 20412 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en.png index 23f5acf95be..d298a1071cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb66db4ab6ad3ceecfea21041626e63420d6d761988cb06922b4d010378d22d8 -size 20129 +oid sha256:836f3390fac4cae03b65d36c278e9340b8c322cc1b947a367cbc574c06a00d2a +size 20007 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en.png index 0daf504fcbb..09f2639d6a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f06e45ae17e3bb230d58bb17d77bbf3d24569cae70c9d7198c04fa64c45ffee -size 21761 +oid sha256:6c265c895d130c2e0054f4e37c052d004bab05d21410df7327ae4acbd20d1c19 +size 21648 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en.png index a3192672d00..cce4311535e 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d44c556a135def1f159c98e8a8b3584213a07d36a47199dc6be9f6f4d0a1c5cb -size 33890 +oid sha256:51a1bb5d86fb72fd3ce72a2dbfb78667b261f2529d8fa9ae470fd7af4fa018fa +size 33779 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en.png index 9a1b6c989c8..e99d44237d6 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a37cf55bb68e4f354a9ec9c1b9aa70fd6bdc5300120928f65b1e698b165d8949 -size 18111 +oid sha256:35e5daef23743321724044d7b3245c97c0d4f76f888bcdf4ec75beeca1ef9a7f +size 17990 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en.png index 8a39b4bbca8..abf69d91913 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03a59450ac7c0bf5ac9b732a7708bf91a9edec0ffa2c2d93bd1783277a816f67 -size 31271 +oid sha256:3e14c15a6bf12aa86589b234716781ab4d4be7ff7f0ddd73d9b820a44993a918 +size 31153 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en.png index 96b31631754..492d1a3fd9c 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b6bfb112e2ecc8e1c552b1525347bccfbb762efb02e57a37e4fdf08310f541a -size 22170 +oid sha256:dbbd954d62ae018def93901dfc727631d80b9640b4238d20ff7fc892996a6c51 +size 22058 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en.png index 253066f199b..0c7f97710d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c218d7d90923ab55b641a1d8f4a63a55e36b3c6fe11bcc353baa2036b03f3f6c -size 23284 +oid sha256:66cfc5eab07a4ddd86bcb205c81ae29a5c39b08b99e51edbf2f615e630081402 +size 23176 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en.png index 620594372e2..fe3b8210da3 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a05b1194954c357a486f85d093504c56e9ab47bedd233f977bfb01e315f184e -size 20174 +oid sha256:213e47db7a62eed761521d97aeb06ead72f4c455018814d3e20b8f6d34e2ca66 +size 20053 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en.png index 94f0b13aeb2..44d9e83ce75 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cfddab6c314d679ec1af2c24a1585774273ce74b427858054014b4f65c85501 -size 19857 +oid sha256:d37fbea44ae5b73b5dc5247668906fabec147ef5c3576131a76bf1455ceddcb5 +size 19740 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en.png index 8658847accb..7f66fe7a1a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe6540c62d496f99d147f5e35e0f435d072e9f30fd8717561d28da4feb97d846 -size 21280 +oid sha256:d376716f2748e32b7a01aa0e543de6e3dbff021daa3132c55d377e74a9595dc5 +size 21167 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en.png index fb91898079e..f665c95cacb 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f720c7a8a6af9164893590ec1eff48033b24acfeecb32630b7bad629bb8abaa -size 31504 +oid sha256:25bf87088b34a4dcaecc9411adc8f1d8603a91918ef51d6c5ca8bfb5b066623c +size 31382 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en.png index 7100b9be844..734cf6ee4e1 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86536abf20dfeb05a7d025fc2ab6a67c1a40fae8d10ce3917343c9a6c7af3670 -size 17856 +oid sha256:84b0bfb6760849cf258706433836246b187245ed6948b331617883a7526e7ae5 +size 17739 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en.png index c70a356bdcb..3e61ab8b1cd 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a922b3290e7254b5d13d42c05079064eeec6b18137f17302c127ea86f1fe6861 -size 29167 +oid sha256:d2f22da18c137496f819e2f927c4bc132bdb92a717e843e5482a5e360038ef38 +size 29059 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en.png index c8a152e1909..993eb9102da 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d4133cd4230d392c41bd7a6463c85b478859ac92e8ad20464c5d1a9b8ddbd69 -size 20756 +oid sha256:08f4ef4a66021c266d2bc10d9c83e9856264729962bcd13943195e780c195686 +size 20640 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en.png index f06cd05b598..4da62f2d296 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:244dfe6d42bc44ee36ba4d354c3157b9bd84248e193a78250078399a5730b361 -size 21162 +oid sha256:47b6d86b5435bf9b14185ff6781db2eb0e010671a0b94d587a188d10fede3717 +size 21042 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_0_en.png index f937c15276b..877aa525c6c 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c093352f7b29704afa50fd52741e12390d3e1d73ac65fc2c9e3581262ba409fb -size 36236 +oid sha256:819fb8665b558fd5ce7da1304a105701153725b73d831e61a1542354e6d1f9cc +size 36108 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_1_en.png index d96e86ee38a..49105c975a3 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62ff72098bf6cf4e3a48e1d79d986c2239edd17cfbe81e6a378b0383fcb09eeb -size 36641 +oid sha256:434120a02d5dfd32e93d6e6705a595ad9b2bc8a46243b96e9ac7aeef876382aa +size 36515 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_2_en.png index 24631cdd16b..4a89b9fa82e 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cba4d246b9b5fe25e28c8e5761e400fe2b55cab7d63e7ed7b7180a7bbb948109 -size 37422 +oid sha256:98aba20c980c9a8663a77f53478b13e76cc4eb0c3ad0dba8588ef2f22a6fec64 +size 37299 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_3_en.png index 85120433f53..5f8403dfa06 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fce2f4f2a24faf87037442368209053c0dd12c2baa47474e56fd083e6513f27d -size 40809 +oid sha256:849cc5595d75d4683b99df5577ec006a20e56e9612e8a54a59f8e4052706b9c1 +size 40700 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_4_en.png index 2c9edfff65f..3a55fcafcac 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:033b5688dc7f52fc371d06efb556af1a771dd7c517a6c8d3cf45be81c2e2a81b -size 33909 +oid sha256:04a30051f1e8d3c00817cc118691313d4328a41d2902f77b3b0589f8592a54d6 +size 33781 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_5_en.png index 15ed126ace7..321f10ce647 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9273976f5646d761f5841114492fcb8db82104505a5da6b88fe8b6e6c475f0b5 -size 38207 +oid sha256:b1676d8f973c2cb9c5ea733da6745bba95a098b3a4c1f319b67e7a62319ee8e8 +size 38087 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_6_en.png index bb0fbac04c8..1d0e1a48736 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eafe36f938290e0249c3385db3ffac3a8855c9253183a5c799e1378e0fe78442 -size 30291 +oid sha256:599902a48cc3dd35dab65bd0cc6dd0fe936a7faa6d70abb5dac966645db0e786 +size 30180 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png index 587a2de4105..00b5b79b18e 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:651bc225128bf611c46b03b32fcb606cdcc3a5eb84fd1ba329806342930d4a12 -size 30469 +oid sha256:321c485e440a4f96ba3e6633201a42e2fa11ebbf63393aefac5b04300dec84bf +size 30360 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_0_en.png index 6fa043b4086..26fe1147b75 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb496814ffeadda18deb9d2484218970be7cfef3cd1af733a536912bb2452b41 -size 35061 +oid sha256:c87a47d282d99d445b3d4915a9d12aa76f8f71df291a06e69f82137446ea35b2 +size 34949 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_1_en.png index d720de42ee1..0158d38e636 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:589f2eddcdfa6e44e59e2dfb84d2fb151066c412f91c2be2ea55431f3ab79ef3 -size 35443 +oid sha256:106c37dd124a0c47a14bca7ce4727b0a69775d650996265b6da6c3ea7a47fbdb +size 35336 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_2_en.png index 823b11b5da3..23df7e68d20 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a873f196f0af07ab4f3f8573b46b33c36317e003810bb13c5d89f5864c18c8ea -size 36107 +oid sha256:971438a5e41f48ed79c49d2e550561b91994374361b6ea44191b4d6f7ed6da04 +size 36001 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_3_en.png index b4ccfda8175..65e36ca793f 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:066e0c158a8b2dbbf055d53b32aab0800a7e650b813c6dbf5d4a71a7d6b2be7b -size 37725 +oid sha256:479f91cb4e0ce4296ca78893778645158133fb6ff6bb2cc4ddc96e5bdc1c0551 +size 37619 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_4_en.png index 2bbe7bced45..dd57107832a 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c935fd954a2ee2260b2f536b3be867fe344caac4b3e231962dba2aba33fb003e -size 32832 +oid sha256:8ec4693fb4a28e2609ffa9422fb94c171dcd3d032d89cf0dee27823396bca17d +size 32723 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_5_en.png index f25019f5a51..8f560f43426 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95f50bfbc143f22cd33cf6477c1186f52bfe9fbb6bd497178efcad67ab11e637 -size 35301 +oid sha256:e0ae6263c675dbae60fa122e3140f9bee334c89459c46581ff4e8b5f9a92497a +size 35199 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_6_en.png index 2a9231be669..1f64d63b3dd 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09cf2fb5e3f1392aa79fe93d7ac72f62512ee41b2f6669ee7d9140f445560070 -size 27702 +oid sha256:22e1901bdc8443e00f0438a2e0c0aea32ab443924e3827ded9a1ac8bc1d4aac5 +size 27598 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png index e1f971aaa2c..91247aff19b 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bdf406b413ea0aceb0e459796e4e28f8f250650b4e9192b0105ebca2661923d -size 27594 +oid sha256:0dd0004b68899c6ed014badf0d1c5603a564e4d79bd84c34608a622799a2c147 +size 27478 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png new file mode 100644 index 00000000000..31b7ce8217e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b4adf1bb086cb46f6a9cc20dffe23cba656a571a4f6651a82063907e82a117e +size 5843 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png new file mode 100644 index 00000000000..e96cb9b338a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d7ed9363bd7b6f09be20f58019260dceb36c8cbebf1c74474b94e227bebb37d +size 5926 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_0_en.png index 4464e8ba313..9009f71917f 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc7f78c0ce55c6e1b0759576f80511d7718291e5903cd0a7cbb19c46bfbec686 -size 17684 +oid sha256:768def050ec8d603c718a9e7732b62beb89a659d34b4b41046cecb08459c68a6 +size 17603 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_1_en.png index 10f33937a80..e82405c8498 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bfc723cc3cadef302fae4db56142e7a4ecdec4c43593e0d1122a8b3bb8d4770 -size 7616 +oid sha256:03df86605a73df3692a163067cdca41048e13fb769f9ed5de9c54817976b0a03 +size 7530 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_2_en.png index ea0ab6bd305..179e4801110 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fb195657c1ac0379839bba6acc0935f6b1b946714ba252d5797fd4bc0c514db -size 6753 +oid sha256:aef1852325b4a0539aaf3d7667fbe84bd32bd8e0018eb1678cc53a99c6cbc746 +size 6660 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png index cccd463c66f..e7c3642e9fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d742af6422e7f5ce0070831e921180006a5b48d52c042fdc52030435597f242 -size 5835 +oid sha256:ef744b07b6e40d1017713b3e44c2e6d358495ea9e1ff88c183c963994e216738 +size 5554 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_0_en.png index 61b321d08a4..7248bdc8ec2 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18e3bb4c57bddd1422c68ddf3a02be0d948e21c55857efb33f71fd4f0039d37c -size 17408 +oid sha256:de83329e8f9f0720eab60d4f89b97a99924b5504eca00f1665fa0cb252b89202 +size 17177 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_1_en.png index 0c055546866..d90b063ee73 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c100f4c1d3d7c33085b76306823c4050121534ff096a9d73124683ff8db6090 -size 7541 +oid sha256:79b50e11185dcdf0810d8f46cd83ff9e2de313524d3e889343435d4a1eefe16a +size 7340 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_2_en.png index 20f4afd2723..2550777c955 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec5654de38a98ee4b078aa3dfa093a686cb89a2f200eb69f59e78041eb8b2f60 -size 6743 +oid sha256:2347feacb167ede5eec2e26a8372bab7e5fa3953e6703918fac50c31561a353a +size 6557 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png index 6e685f122d5..ab8bcda2764 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4731443df36855c8785072a8085fc9968ec231ff46ff24012b0ae72cd084e46 -size 5867 +oid sha256:04a8e2e856805b0888f901278537a217e233445b9f09060e1f4b764db87146c8 +size 5589 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_2_en.png index 6e612f359f4..2d6df8b039c 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f642737475ed867d2c1ae5cf34af6261298c33c761e6f9182c0a9b75e3f583bc -size 19091 +oid sha256:fcaa6f6abb01095e62ece79207536c035e70335403a4c6d73c7e18bdc7d0aa5d +size 27024 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_3_en.png new file mode 100644 index 00000000000..d65b4da9387 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1e37b08acfdf40279b0990dd65c58d38d2248026f0d2fe2a1c396245ea931fd +size 15892 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_2_en.png index 7727e0abae9..ab7ccfe7fd5 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3929cf3c7bae2c26b5ad499383ffc5d5aecfee7f5c9d24dff0c1116bec018836 -size 17074 +oid sha256:066175e7a3408f163dc0b1126b3644ab83d08f2627bd0429e3fa4a14e1444c03 +size 25335 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_3_en.png new file mode 100644 index 00000000000..aac7b5148e7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8db77f38d81113a5411c40c1ebf811090ce809fd0f0df3d29eab05d3ed0bb64a +size 14114 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en.png index 6e612f359f4..2d6df8b039c 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f642737475ed867d2c1ae5cf34af6261298c33c761e6f9182c0a9b75e3f583bc -size 19091 +oid sha256:fcaa6f6abb01095e62ece79207536c035e70335403a4c6d73c7e18bdc7d0aa5d +size 27024 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en.png index 7727e0abae9..ab7ccfe7fd5 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3929cf3c7bae2c26b5ad499383ffc5d5aecfee7f5c9d24dff0c1116bec018836 -size 17074 +oid sha256:066175e7a3408f163dc0b1126b3644ab83d08f2627bd0429e3fa4a14e1444c03 +size 25335 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png index 75f9984454d..28032f8d609 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f58d92030e4c799b64c93b5c93772c6901cfd0551c2f4bfe009cb5a17ba7190a -size 47394 +oid sha256:4406e5c57eea02b3ac315b303e0995e6ad0ae1b25f751cb8a154220e08a2bf23 +size 47284 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png new file mode 100644 index 00000000000..fcaae762cc5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc85189c4ebe4539c02294c5fa98d41816321848de5d68718de2dffef010719e +size 45074 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png index a9750251d08..5f4d6441ca1 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f943e3209743896efce11ca3b429e51c6c94f8937b8db75b1c0d94990a42d944 -size 46508 +oid sha256:35820145f9f9fe836035774390808d5778cdca064730a3b4ebe62f66a26fafae +size 46350 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png new file mode 100644 index 00000000000..1bcba3b0563 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a3e19f270562bbd81c9508e90319b819c08ecc766f5d08adc657aa2781ad679 +size 44112 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png new file mode 100644 index 00000000000..3ca626466ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4051b09fbcb571273ff3edf21d482fa75684b73846a4992aa495274dbc8fcb97 +size 22602 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png new file mode 100644 index 00000000000..d8831b99f68 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2f1145b53dd1b031d3fb213217fd5128b5dae6f5882f0af5f339973f61d8748 +size 23556 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png new file mode 100644 index 00000000000..605f09a3798 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8dc7e4a04653998514fb3ca02bd220fdb0979c5ce8251031cf90e51f6a7265c +size 24082 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png new file mode 100644 index 00000000000..37a1c8060a3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf820f7343125063ff3e92f3d4902e427a424d148760168884cd143628925010 +size 22111 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png new file mode 100644 index 00000000000..0b3b32b30fe --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bd58639caaa55c921244a15b0f4c1d3997b4c591cab4ad46ddcfc07d7a4fdc1 +size 23010 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png new file mode 100644 index 00000000000..7c80a5e9f44 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9dfd90cd62e5511075857c5642660228b553208bda00cf07d51db087b8b37a9 +size 23508 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png index 1ae7f02292b..b233883a8a4 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7e096cb891c043f72d3745e461f639a8518340b00d4feaab48b4ff87127de3a -size 35344 +oid sha256:304f728ef7c65c66a2d59d28b8e7e84fc62d1087ea033169a8a0b8a9cdde1e94 +size 35560 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png index 5ee6ccb186c..0b5c513fcda 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbef81d76295a27716ed6d93779ea9c7bcf0d4523080b22de1853877a718662d -size 37556 +oid sha256:75c63231868e06a65e71b7a37e7a39b4f07b8b5250aa9f5364ccef0be38633ad +size 37823 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png index 20cdd0f7322..bc09c3553b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41c923124d9246e7e39a4be068f679e68ab244d6cd094a2015b7167cc46e8e3b -size 39021 +oid sha256:4d5a9efaa811f4af21795f59123f4a5d10c461b9a1ad9c1304c174d8745e63c2 +size 39199 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png index b3f25525ac1..67d39d6d14b 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1474e83086bb97b16a593c4e184d55fde289f15240ab1929d6959405b12b3ac -size 34541 +oid sha256:077c904bf23a2372b3e8aba3c7a439efd3df0885bb85fa22d5f24fa74698babd +size 34728 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png index c83e63194a5..5395ece9285 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67557332f2879b70d45057f40f0433c457c3d118c5f3d91d93ac3d7fc283c343 -size 36399 +oid sha256:6fd45ceb6bc868c8849cf72d1833861552cdf64e80084b64971d0f8324e86604 +size 36582 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png index ee3108580c6..f34f292b1a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f71dbe257ad51dcc2aa57c699977f9a0082fc45ea8bed6e2356a1035a9dc0c6 -size 36507 +oid sha256:a254756821c5b265a8e1d8688ec8a14f4431173a191ce212634abb57d7c13570 +size 36665 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png index ecd3fa6368b..635e8b6f9be 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:705f53f174aeaa0173b618adbb718d86b7d1a25767aa2801866a154f365acf9a -size 13361 +oid sha256:e7818e342a7550aa0c0a2375c84e7911ccea44371366e07efe17a5fdca688216 +size 13360 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en.png index cab77008b0f..1de4512b081 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3467e62faad4ad345ce5a7406896f2b4f0f2f36e6f1b66e358cba644e527b934 -size 35240 +oid sha256:7d635b3896aa6d4af2d593ddd48a17433fcb3aae84af1beb47fd51aa44c6bdba +size 35504 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en.png index 3194ee47206..dd18b26434e 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b37e9ba0da4ab5964faa1ae8e3a6deb8fecc1e0d07e003dd8193b75c15b9bb8 -size 36641 +oid sha256:41d808ae1d6ba34546e7e853abde3bb6437d0159bae57cfd9b05c30686643ffe +size 36878 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en.png index 0fce5b1140b..9232f3efa8f 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a939e0560716022da2805a39cc38a0bc0de0410cf8794b4bde849771aa94418 -size 27396 +oid sha256:8a1f0000389a72bee08923d441a8f49e6c032706caeb0589751feb1387a2b0d0 +size 27586 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en.png index 1411b8d2635..db6b29c5b92 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e320397d5b6c10de012de93ecfb7cc491653773abea113001a05e0680466964 -size 34147 +oid sha256:522422e51e79e11c80bacc1d1037855f94b42fc54d8000538b81ead7921110fb +size 34358 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en.png index 51ab847fa28..03e5461c142 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7df6365bed2db73bf965550868d0eaa45b54c97e42675054198e3c2d3ef611b9 -size 35270 +oid sha256:27bb7d7bec5fe3b7856cd4ed1455c25971f72f0c4c24dd576b409fec6b187242 +size 35492 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en.png index 096b69d7222..6dda271b961 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33704c8f01eaea139534bbcf13f074b37bab5f128f7d319998027d23180c3c9b -size 25381 +oid sha256:ea1d0747c5a7b4ddb1c61b001962a4a613af6bcc9658cc1303df9c752946c87d +size 25578 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_0_en.png new file mode 100644 index 00000000000..d77f4d1242f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8dc4806d2d363c326ac777011b95a98111e69d8f7acaccf926dc959bf8e9636 +size 311242 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_1_en.png new file mode 100644 index 00000000000..a67a1981e40 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b55b1c3f2f76ac40cba5f7d49d897a4ffe22d6718444ca8a1197173c660f182 +size 306017 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_2_en.png new file mode 100644 index 00000000000..2859c2ccedd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:644e67fd8ccb1754ba4bfec6de5bc8c75a6603b75faa96847076cc27ad18804b +size 309657 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_3_en.png new file mode 100644 index 00000000000..48d2f299afe --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcc26d88db59b7499a9e1894608831b418a8088b248c5fb390a43ba8cd5aa319 +size 304249 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_4_en.png new file mode 100644 index 00000000000..5adfb27ab3d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:171e41e96dbb6495c667a57047cf66e5059a19b8dba7d56e799113cd5a8690e4 +size 304775 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_5_en.png new file mode 100644 index 00000000000..fbacb365c1a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e93b2b62089570439aa10baf41d0ded8cdac23ba234a5bb32ba118f41e1f7385 +size 313315 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_0_en.png new file mode 100644 index 00000000000..d3eec1107f1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:344fa6743ab2f43819abf6045d4d98ef0ff91f6b31e6de4a87235d6b96160e53 +size 392425 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_1_en.png new file mode 100644 index 00000000000..0a84a5b6324 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed48740f02725790f7de7a867d237449c07148367dc32ef7fbbd7b40d0ed9a4c +size 379715 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_2_en.png new file mode 100644 index 00000000000..a461b0fbc94 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6a6c752f1de16c4c64987d8f603d76ec0ed344b59d335b71004ce85500d852e +size 383032 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_3_en.png new file mode 100644 index 00000000000..209ec00b37e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b9101e8d1502445e3997230800ac1cd4adc50a5392cebdcdc4dfc3e5fe56abc +size 364584 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_4_en.png new file mode 100644 index 00000000000..89393180f40 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee7b3e5bb14da106ecedbe4827cded220d7222b68837c3c63ae3dfacd66ec2f2 +size 365133 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_5_en.png new file mode 100644 index 00000000000..b622d48595d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa123acbffde2545f7f76ce58ede9d1fb80668d2d8576bf5f920a49f91c1e474 +size 394540 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 2fb4d86674b..4e3b4827e55 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:a139f621872543b69854d5397d8b5c51f44fea6b4909e578ba1c7af8343175ce -size 31995 +oid sha256:c7e2aacd54c311724ab4482bbb61ebaf6114fd6a7d9b18f58c4850c6a7ff841a +size 32029 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 6ee1109e826..aa8ff94402a 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:3d724e2fadbdb0a740861ec0ea34f0a6c7f1c3269e79c23d38108adaef5b1c10 -size 31876 +oid sha256:0fe0babaf16d4d2e52b8fa6c5d898a8ff003d210887a0db4d1738a1c2b48b613 +size 31901 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 e0e02f51ea7..fe94d672a04 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:ec27983616e9ce41f675f9b19751beb7fa99273b4fff2848c2ade021b2589468 -size 33728 +oid sha256:0d98322268b8557830a443610e121c475a51741f1ef2c226f7ab5fdcd5c5ee45 +size 33755 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 48d745149a0..a8babe65858 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:52f729159fe5298ea15d6400dfc13724e673c1cc8c588fcd52642bbb27ad87c1 -size 31312 +oid sha256:fe161156e9d74c030d5f4b4ac6a91710c11010a16412991cb2633dc93b726591 +size 31338 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 2d6850d583c..419bc991baa 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:257f325a12c3e19a925930efb0903bd389a66b0afe91312b42028d9d165016db -size 31209 +oid sha256:742252374f295089f0d88572d51e48d08d50711ff5b84549e0b75d69154517ba +size 31235 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 5b66276db40..73049fb03bb 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:6b6a1e121a09f5aa614d7971193a26377883c2867a5ad37e999b2ace14a4cd0d -size 33082 +oid sha256:78a9202f7a4d80ceca59ac17abd6917b0dec3fcad8a86ebe6073ad411d3e93fa +size 33105 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en.png index 57654729cdf..b8bb0e59b89 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c102a6c2165e33817a6dc9ff502da147fb9bb871bc96f6f13c4d2cd5082214a8 -size 20754 +oid sha256:f1e5d6dc63254ccec69fc48fd0c9a8615c46559cd68ad3bdd9668e327ab81fa3 +size 20775 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en.png index ff81616052d..2466d1e1d8c 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a93d12102fe17b46cfc8cfd242793f3ea720cb8baf678463df9c9bcb36ef7c09 -size 18414 +oid sha256:28f2292da30de2b80d1b9a1794ce0eb3a95b6805b65385f37cc3f0507d36982d +size 18430 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en.png index 54e93fb0902..7558f6c353d 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d93bc23317adcce615835732b243ef036ffc71b6856f9f8182850f848508083 -size 21727 +oid sha256:56944f3343736052dbd6bd186f9b87a636fc80e8bd50d76d7e99ea9240ee8b21 +size 21743 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 c4c92a4fc6c..30df18319a8 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:0ec0328e6bc295866e3dc948f3a5a7255885212fd216c37aa33308a98f94c67a -size 33008 +oid sha256:9f7091f771462311ff4f4ad6445fddd03c31de877a2eee4d0865067d776c9fd1 +size 33029 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en.png index 676eca6e4f9..fbc6b525955 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13565f986f38ecd5ea944933fac44184ab57ea7d085c736dce2ceb0e5ebea484 -size 66894 +oid sha256:37db3f1216f73e069a7ea4e51b389a713e71b07717cd4b6cf86155015d421bba +size 66908 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 ec0899bed18..0ddd87c634a 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:e6cf7f3fbe51b4497fa38309e5efc033362a53bace1cde16429132df384407fd -size 21211 +oid sha256:ed91e958b33d44714d32c135a4554850825844024c2fb623527c9b9bd58d2356 +size 21234 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 03c272b2f03..9ff965c7a83 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:1f538e3ee79fd900d59bfa7d89c9c6ea7a2141a04120735ae55022f84d9ae636 -size 20876 +oid sha256:3390715442c12efce16df6f91fcf36fc46a0262dd4ef6aac49d6493c17493adc +size 20886 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en.png index 70f5db5290e..2d4d6af26fa 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5db9b8e6fdb2ad5ab4e41ec9361ad39d3e583f4cdbb9b4aded0dd6867e73ebe9 -size 20487 +oid sha256:f0bb1d57bbc586eee2e35cb9441cbd53a5bb8ad6f72aff5e9918838e410ff9cb +size 20482 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en.png index 0055a29751c..8ecb2900f0e 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89fb5b65f23282a6dd7da2b68265ccd4d2aa9e30b0b7eae6f7504de2f04b017f -size 18229 +oid sha256:666a5fcb07381fc719cfa6a375e4300cc111e53e1c1968b39fe1efbd11f316c8 +size 18225 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en.png index 0e13a00cf04..e1bfb826c7e 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45feaa59085a67542d79e1d5705442ecb7a5ab32e30c39045b24ef043bb35589 -size 21446 +oid sha256:ee79a8a207370ff710bfc7a8a6eb704b2940b5d736929cc17b43b4f737f0d05f +size 21440 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 54ec5c90659..b14ae7e0aa2 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:3c06f1c27a766c69de87d04ec38223a36ddbdb56654cc46e3f900f8ad6f899cf -size 32384 +oid sha256:1f8b0edad15fabdd9594570b14ff7943b2e2f498fd138c3abb70cb66fd517cc4 +size 32386 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en.png index d4a46becfd5..fd084544dcb 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e52cbbe106c18fc9d33f75ceed31276ab4827e31c420f42b6b3c90f85d8572d -size 65481 +oid sha256:d4e7563132db636fe51d60bada7553d51330867477c7436eee77048ba6a0a47d +size 65480 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 4c860516908..f405db0600c 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:af78253550eb58fccb94384360cba55518d0599d4c8c4908f5bd7a53dce914b1 -size 20972 +oid sha256:23c9b41da39eeb6f42719aa54d35f3765c0e844f035ef4b872723ba481a16c61 +size 20969 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 73888b9bb40..2c8d7acb274 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:519f7cb9ee14d6269f1899810a359b527e7a7be6538759eda1cbe82712685580 -size 20601 +oid sha256:d63535229e5fd6f96fe215477825c380bdb7dabaf2458e7e46de4697631f0b7d +size 20595 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 82ce0ef3c6a..6dddbd97b09 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:b1b50c558703a1fb83dfd8b4dcc8484fc3aaf019e4ab466f0e515db401f0797d -size 49999 +oid sha256:4e9e20979d0585a13cbe652907a0e1bdb9eb9a6a69b70af51acdf5950b690549 +size 50024 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 a99a39ec089..99ca127a296 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:269e494c1c817d27645aedf555a9004d296c4a86629f294e8e4ff11c2352c5b4 -size 49036 +oid sha256:d98e232766a7161d0f28bc9c41a59d558a2bc7dfec2d7251597da96f87f6d941 +size 49088 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 2f53a211817..d64e0edccad 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:d664509d4d3b3f3124952fd477d1cf990ede82a47bba9ddd0d44644b2a67e4a9 -size 48628 +oid sha256:0f0968c1206960c522fadeb32365fed057f8027d21dde78d62939084643a7e0a +size 48655 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 4ad7d9cf3ee..a160d164c8e 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:8fe6a4c7c7b5ce9a8298ea6ddec14fa891dfadc374c4443fd3e75bffd2bea98b -size 46542 +oid sha256:fb9e7821af0560357806cd0b379c28e8ea7214494edc0f85efa13a719f023772 +size 46588 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en.png index edcb1f99644..dc56c9ef07e 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:900f489028d4af30bffc3fbfe49c377d27bef887b2a376973b9252bf9132da0a -size 14734 +oid sha256:ee07c8d10b22fde6ba769ffb9f4165dc361a736f406462971e675fe49f937c4f +size 14601 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en.png index d6082e377cf..876adb14add 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27200062b6eac1570c9868989fa4906a7c604d0e62a391d0f7299496f13fa7c9 -size 19832 +oid sha256:82d2a63343a1888fea014a660839b258e384dedb5172878f50513543fac908a1 +size 19675 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en.png index db055e76577..a2a2133bd56 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5952fcc2001ac647fef21bac6b5994648235fd3e721aa810520e5436d298acc7 -size 26351 +oid sha256:6926b4809939097a505ce1864dabcf5ca50461269b89118154b5e692317aec9f +size 26204 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en.png index a3d5b97bed8..a23153e5c17 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b291dc84c70f765eca548262fa352633eab6a43e79ebbd31233b01661adffba5 -size 32512 +oid sha256:0cf8d0e215067d3c6bab9f7e7669ca4639b896f7dc418631dd9959fb99452ca7 +size 32385 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en.png new file mode 100644 index 00000000000..fb57633a419 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48b327608bde4f150450306fb3a3b2508dd437b43e301fda58a8bc279ba093f3 +size 31326 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en.png index 37f60b9b57b..758b702ea01 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df999d7acb6dac9e991a2e45a896d8c006034b0205cd01cf97a496f3be192790 -size 14024 +oid sha256:cb9fc1376b91a4aa5a591100bf98fa15a138d26f5d43ee22c48a61a89943ff9f +size 13974 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en.png index 8ff27f9321d..7c76229aef3 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ee864b015608e7c54bd206c76d0410033259d0034db9875ec7bd126a1a7a11e -size 18941 +oid sha256:bf7a6a0f41ee301fb203c25c4636d7ce96d15e76eb536c7573e7792657dcd459 +size 18892 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en.png index ae81d4fb052..ec5323d6d43 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b2a4e2925fb9042a9a25edc6f1ba439fd82bd8dac004c1bfb6d4cec7408a785 -size 25216 +oid sha256:3f1929531fee3dc938cda1f865260c83569cd5980ba5da5b7a0b3b6285085aed +size 25160 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en.png index ae3eaa710aa..f0985eeea4d 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51d18d0bcea22da66c5766b1a9ef0b32f72d9ed9c4202e27c7604dc29daa9b2a -size 31178 +oid sha256:e77dc96ffab18d00cdced2486407f3d70d0213a9f2937672c12e6d920364b17e +size 31129 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en.png new file mode 100644 index 00000000000..20ae4c9cdd5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c31eaa5cbb8850fd523915a401e3b9ade2a58845b76cf2c7c4fec78685479159 +size 30164 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png index c84b993cd2f..f5f864b6aae 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fcad66725dfbd432d4dc62d5d2464432496db0ff1b76756292da93486dec7fe -size 51518 +oid sha256:cb99a065e901322c590b5331880e8035b6ab6101d4e2f4603ceb3abce2344147 +size 50804 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png index 206a212fcfd..485a3a25527 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f8f3e8071ee6403cb8b27e6c17f35628bf2152f0fa878513c871a4393971e05 -size 50518 +oid sha256:ecc79b3e57881971e5bfc7d8e2937b4e9ccb18536851be3fc79154b544aa6895 +size 49800 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 5646cfc9741..94e2f83e63d 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:168ccd4dd8afa84f6e08bd18b29a14a90474d3d05d14a13047ad766186ffafc6 -size 77524 +oid sha256:12c5cb63cd1ec9b826bd11f61bf35cb7416b2b95fccba9a398f583e1da2ae296 +size 77522 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 e4aca60ed68..c66abca93b1 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:2fc2fd1c67088e29a3d8e2dc633b411cd8256d821243d5d2f96bcf6b8e5ab6cc -size 75979 +oid sha256:12ea05aeeb9850cd94152494a8eb7e5378ed0bdde76f8966ac691386f90c3b23 +size 75981 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 284b837df68..c10d3965a49 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:b45a5903d4b2ca61940f697076973478739054b74495a6461efddfc9439db067 -size 61736 +oid sha256:33d7730a78fc487511a0af9b0669caf93f4887c654a63766ee6260b512a42a8c +size 61733 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 d9780d03bf7..a95e0ac93ab 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:e26369765383ad4e9d22dc0c0a2945fc28ebfcdc5f8f387dcebcc3db859de544 -size 55677 +oid sha256:7466d9a726f8de274dc9a50f7bf02fce4a4883765f603cb8882506e655e65f6b +size 55678 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 c0ac4b509f3..4b34de7a93d 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:125ffdfb5eb8352b8daee1bc17feee0e638d1abc7157804ad547ae936a8a2b53 -size 52971 +oid sha256:4f4312c4b50990a911089f81a8a543bf15139a9b335171889098a4c5922bf728 +size 52968 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 2195039d1ff..c0968fc6333 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:288f1451dddbb41d0a45b99f8d06f89fcdc87b6f29696af18a6873062675fede -size 75681 +oid sha256:b39a36caf124faaad066971353b2628c9cae517409df6cae5fde36313a14ea33 +size 75689 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 aa553071e21..5f125abf9f6 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:1cc6acf5ef063b3bf4d101afe0c65435e2c4a68990ab3e860f57ad0bc18b32ec -size 74073 +oid sha256:2e370ec6ae660e2fe85f39b9da1bb475870feaeaca52cb6827b9a79c02b1b29f +size 74074 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 bf21751e388..b998adbc0d0 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:8433b35df49a3fddb810b9397be22bd8815c52d5c136102ca612ff13f439a6cb -size 58228 +oid sha256:405ae68592358e099b1639f9f19ebb4e7734b94588e33ee8c208abdd069b7ad9 +size 58229 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 3e2f5cc07ff..523ca963715 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:f32a16cdf84a60cc3d74fb86201355dbcae8ec3dbb4b95c1999e0106752299f9 -size 52817 +oid sha256:b4c8007154f0128fc930aa654278286dae1a075916f67f90d70f3dcc753da2d6 +size 52814 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 287e53ea409..865df346795 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:8440f8da2ceb292fa73716bb23cc234bca2cc524543f96392c39fadaec3a0a17 +oid sha256:97e7b6b0eec29cd8f2805c52543892388403d7bf35125a54e7e683a791e5bdfc size 49675 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_0_en.png index b99d70c8106..31bc91b9798 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e241e62bcd807e5b2c423179429dd1ebb9c495355acc81e95ccbb70dd3ab9edf -size 10920 +oid sha256:294a6660a39ca98bbf18600edadc6862bbd4cf9667ca251680a76611c9252fa9 +size 10935 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 new file mode 100644 index 00000000000..b1595478183 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b005a634331a485e2f80fb773a147207c782eaf2d923d1a87c5b45597309c658 +size 25794 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 new file mode 100644 index 00000000000..e6d14a0cd42 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d800ec0c02e8e2f8fff755d12d2e657d6fbae10a8b9f070bb627b3ccf9d3e159 +size 30566 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 4bb4feb9128..cdb89e0c5ab 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:70b295d68841ea3c484b348473a50eb2b2421dcec556a45325ea1b0a3d077e4e -size 37042 +oid sha256:c8e9bf680eaa007f0b118ce64af8d6fca9e4cd15d33a819e76ef7496ad4818c1 +size 37051 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 3b5eee9cef6..4df9d9aca9a 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:4251ddc86df66472e9d8a9d0c45e87d68c7684fd2e194cf1c4547dc6d97421d0 -size 27456 +oid sha256:330ffbd4d825fdbd65015aaddda3258630187959032e1b981652ef6df3af3d40 +size 27461 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 4bb4feb9128..cdb89e0c5ab 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:70b295d68841ea3c484b348473a50eb2b2421dcec556a45325ea1b0a3d077e4e -size 37042 +oid sha256:c8e9bf680eaa007f0b118ce64af8d6fca9e4cd15d33a819e76ef7496ad4818c1 +size 37051 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_4_en.png index 538df41e03f..126bacdcd0f 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bce941d887e500daa6fba231d1bfb0bb62a45ece7ce42bf14f38b7ff9994a947 -size 24153 +oid sha256:8c6d88498d6f33e5709a8f62730cd7da7d888a2797f30901761e3d6341024834 +size 24143 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_5_en.png index 81f60aca410..397da8770ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9c41cf85be13606688bd25e9b522ba47f5c014dfcbf567d2df25c24098ceca9 -size 17184 +oid sha256:02aeceaea7ac55691e89d3709252ebbc5d2caf174d4414c0925b84fadaab9848 +size 17169 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_6_en.png index 3fdf7c4ed39..b6212b8b5f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fac37eca733ed42a7bff9016a878e11e81c9e55caa2b4e855f9b6f25d0aeae4d -size 22976 +oid sha256:3d604c1621979cf7eeebda1e2b21ecd54b643e9b24fc2616bba54f0e0a16a122 +size 22961 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 266e3482f7b..40d0a6cb2ca 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:0acd38e69927671b3b4d1c63de34b9764c29ac99220d7c2519244a3108f8de1b -size 35253 +oid sha256:8e704e725980f6a78ea3917af69347ebac52f910c69d29d8afa73a98a4772ee9 +size 35261 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 4901179a111..7c8a48e5999 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:b5c00e506855d08de4a803e06fe52dd706e5780accb7e0f3702d7466224e033f -size 33397 +oid sha256:1691d69c3391a86994f883bdc248353e42b41e5548670b0da3698bdfea4eca49 +size 33402 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 9320cf5607b..126216ced4d 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:762f4b470d14bc375b9536e1435dd9db486e2084f1570bd8cb70a10ce58c7f8a -size 34325 +oid sha256:706bb49312971a3c11d3e47d23c40eb4e3e5678f28e427d6bf247164b414329d +size 34331 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_0_en.png index 46384c6b7a1..078980f1696 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:524d066be2eaebc46250236a68a9cf1ac5e48d40615b69497e61196e394de006 -size 10523 +oid sha256:872e8998206156f35024b1586eac2cdaf20d2fd250227e56d99597191cec5842 +size 10525 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 new file mode 100644 index 00000000000..0344224a85f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6408d329ea127961b08cb92fcfbbdd93fc2c700191e7c97ce4ebad147647c1c4 +size 24899 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 new file mode 100644 index 00000000000..fcddb192f4e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1764cc1d1fc5234ef88a1f1cf9317a234cd7ee7195a190a0e827442c0d75db2 +size 29601 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 b33240758d7..84eb5b54f6c 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:f7242f50a654ae61d66d60d7ef78b0a6308988b4f1c88266ed53877d61afa887 -size 35765 +oid sha256:b05c0229e206d9c18743a1e65107212a4ed383871e4ed68fa254cc86734c3fd3 +size 35766 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 2d6b8c32d9a..33ced2e5b04 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:0245baf0dfd48495ab500f1897f405f1827cdc18eb3dc02b1516282f5e94c212 -size 26494 +oid sha256:e184ee4a9687e98d24c6563319c9266cb5609db66d1fcbcdb02e9fca11504bee +size 26501 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 b33240758d7..84eb5b54f6c 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:f7242f50a654ae61d66d60d7ef78b0a6308988b4f1c88266ed53877d61afa887 -size 35765 +oid sha256:b05c0229e206d9c18743a1e65107212a4ed383871e4ed68fa254cc86734c3fd3 +size 35766 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_4_en.png index 6112b7ba4cf..b531a2e699e 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:841e09aeaad6def513d6fe8f5cc674d81577de30e99667688b625c733e03a93f -size 22043 +oid sha256:b6fc3d53bf76326ebd58df19cfda4c2ed173860d076d3b5a364197ef08175335 +size 22055 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_5_en.png index b25f7270f92..38ccd52273e 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87e527c6091f41f249888fc9b78ed819685ea7f0bebad2287b773e5794a455bb -size 15736 +oid sha256:a15794901869978fd4656e7453875c4cf36857d16aa9346e24a18c499c334a5d +size 15750 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_6_en.png index b8908f1261d..bea2337f62e 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aec0f8ca310f3e84cdbceedd3311153a961fac56ab4f852d7086bb9e6c93739a -size 20874 +oid sha256:fe9c6c2e306278e2794787673697e03cd713866019eaf482b8bef04db9e3bf17 +size 20890 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 ed8d136eefa..fd225e854f5 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:968f51a647d87ec67c75be0a3dc76cfe4bedcef65f0ce0a9077683405bb65162 -size 33955 +oid sha256:0d849146477f01e96d231b3725e8425cb3e50488058e2cf649adaf62fdc3505b +size 33952 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 070eb44dfc4..894499baf5c 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:75f0edd45ff6208829fb93452518a7eac4d371bf5cfb3c1a01a405f0de535407 -size 32421 +oid sha256:2349f44fd068968d9127e98fbd3e5d2ddceaf4e3038663be0c46af2ab400f70f +size 32429 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 218e8394aa5..2a9837fab22 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:340ec2076e4924b111df622bce11aba94434a3ed07b3437bd28649d513634624 -size 33353 +oid sha256:6d6b8e23b0f97f0ccbf7e53f6b7633635bdefb9ae65874b5809cc8301481e7a6 +size 33357 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png index 3bf8f0bd3d7..3f1178a3ce2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bcf877df431dfe4cf3c7f19d58c413356cdf77ae26f9dfbc9f9136fea3f5c02 -size 29361 +oid sha256:aa296da3903113375080f73c78eb9546d06d204160ad20b0d633f6d01ac389f5 +size 29368 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png index da9d93301b9..30979e2d145 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88d5893c849bfb7945535f1f450e0c8b8975b5b4eb3ca336bff73aa607f2f34d -size 48171 +oid sha256:97304fcb86c07e6571ec5cc41cc1b704b3f478a0ae576e6dff4cb02ff6abe402 +size 48185 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png index e0975da1a9d..07d5d4bcb4d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f0451276061db5c189062b32414fd93c2f174c3e7597ab723c3f1a0abbfae28 -size 49821 +oid sha256:207c305fd9dbb6ff1a2c0afea06b1d997e7fee26ce8e62718bf13af9287b02d1 +size 49847 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png index 6b36ec50596..28fd4cda379 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8ec0f80c44ea01158f58e53c0aeafe6df0586e13ff452a61c8b6a9ffb070702 -size 42824 +oid sha256:e89f7d9d4ef5a020e35602791408fa8510eb075679cd6b27db7d040a7f4d5425 +size 42810 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png index fe9b8d0f552..3779c6c75ad 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22aabc7669365a40151e7ecd82a3324a53d0f55a370e0dd803d843a5275b1434 -size 45949 +oid sha256:72d6edf74b360066116d00773aa8543296dda70a2d5ecfa7dab08edc4b68cf22 +size 45934 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png index 2f90f049140..39d642b18d9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a82048429ed60a4b246c37bc1b104e166c8c2402884766a9e487ca98ad45a57 -size 44391 +oid sha256:805fcd53f17e95f8f7f71362ed757df74f772974157bb43cb2d5b847672a92d9 +size 44378 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png index 70d0754dabe..32347a14383 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7e8e9782859caf125c6258a612d52532cf005d1d21146e21068882b586f5d1d -size 40788 +oid sha256:e7545ee0afa20fa6930a189f63ea10a52122461847ac3b2d1d344373e1af392a +size 40806 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png index 8e3e0660c19..e14deab068b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:640a1d04239f041ae395b56eee251a2942764c97dc50db3a8d4c3bfc44225a1a -size 44670 +oid sha256:59152d79ac479e33ace26c422c14393b6eb1a40f5f5bb0b16f26c4d95095fc60 +size 44657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png index d1cba8905f6..cf2081510e0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:150a3d6bd18e87b15575f7f452b222f07e6c3ef227032cbfe84a7728d17c491e -size 42088 +oid sha256:18c082a73aeec362d27dc34fff00f749f0b256e2313ebb3083767b84994dcf63 +size 42102 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png index cf5c641ec78..12fe6f17980 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:737959115454525aa41c67d3ff47aa1829d78c7d18648b7c14b5e62ff6e44436 -size 44511 +oid sha256:9c9ef4665145527c6b12763c1085784564b2827a6242ecb7a6d61e9701c374ba +size 44505 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png index 9778b3513b5..43271d6f4ed 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd031f67683678815eb3f4b55f3ee65fb79096035705019770a626bfc5c00794 -size 34009 +oid sha256:37bff48c23205cd1dd6b546c8c2e5db441eb6006826f8f671bed9e3642c4ea44 +size 34011 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png index c281247aeb0..62b901127e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97fbbf4f3b9466a086a8824f9230a1ea5525314d818e19879a2c5340c9495138 -size 28642 +oid sha256:ce30046844ea37838469e740767a15c80b9c6c40ab640a8ffc49f19ede68f581 +size 28666 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png index c275736aa02..fd84a78ed04 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69f31cbb83a84e57e8a9e7bf4ff451d5a192523486c400abdc293a467ca0ffa5 -size 47200 +oid sha256:f7f569f869f34e9bb9555ebc24f71f54fed0902ed33e3d603cd87646e9ce037d +size 47207 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png index 17714bdf54c..52fa96f315c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:410450770889b906c279084668980b41357e432345bc098560d8b06d6e35c86d -size 48841 +oid sha256:c040408ef0ac15820fc70c3ae7cf1a6d2eb228eafa0cdaab576062ad4035a2bf +size 48874 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png index 674ba67cce3..38cd1bb4a24 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ce4a7fdb57fd4afae868aa733b013fd5e4daea6aaa736256ed72d9d5eb759e7 -size 41988 +oid sha256:afbb80055f588d4f51a51f7588babe420298fab8c77958f6aeeaecc2de1e9c93 +size 42000 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png index faee81cc799..da7c861ea13 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6721b3503beb8ddfa9efc52a7ab269c6aea9fcefad2c1b1b9ce31b4240d48719 -size 45204 +oid sha256:9aebbe22ced8f43f681c72f21ebe912c7ff0a2ac49b4d66662d8cfbf8ad0a79b +size 45213 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png index 2e828a00058..8ed1f9beb42 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac363ebe575582b39b3ebd7857ff891994f512064f167fee342a9a2b2a35dd1e -size 43572 +oid sha256:9c4f2edc759c82ce3623c9d75cc3eccad468cc936045a2775eca72484d3a71f3 +size 43576 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png index 084646ff715..a651535d3dd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d30a38861e317010616e7b17614803439f345e376f36b32e3e6e832070ada71 -size 40150 +oid sha256:8ca599e234d790894fa3fcef83d706e9c5c4cd1bfe0250b17dfc3268536a28fc +size 40178 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png index 133a8cbd14b..3346833d1d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96f4023e2a502fb50dc9c03ad53faa932baf1e316652651e109f21f531adb5de -size 43819 +oid sha256:7098630b753222254f84b0691d51dfbc23c7959f39de3f806c58dbe4805c6c5e +size 43824 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png index a6e70d1e6e8..bf87573e7f3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f52f074d2dbf93c3170c80dd18f4601ad1e2db6c57773d0e10c3f8c29ffdc9f -size 41303 +oid sha256:95f8957db3931bc3083908680f24e368fc3c6e46753f8a2fcdf68faa9ab46a39 +size 41333 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png index 5d6104964e6..8f87725c32d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:504ac65ffbcb0705f3e73d32970cd345d23d19e4f190420f8953e55250056329 -size 43641 +oid sha256:c9e221e2a1aa205bfcbbed20a4c362894271677468a1cf759f1a2d8ef296632f +size 43654 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png index 720075f0075..fc8f3c2c174 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f72f6ec83324b0b6e3719e1e0f59387cb0376149decbb780970dcb7f219a1551 -size 33053 +oid sha256:04c934cca707c2a32cd94422770011e47d8dea6520def65b9e498824baabc52d +size 33103 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_0_en.png index acdab75b0d2..c36a7bc54eb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada -size 395102 +oid sha256:cb50ba951a464a7bae750e779f25e2deeda0355a3220fdaf19f60c7c7ed73649 +size 395064 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png index acdab75b0d2..80ba8d6f670 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada -size 395102 +oid sha256:503e64a8acb9630e3a6cece0028283407bd54fda2f8ca7bd41c09d12491b48d1 +size 394757 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png index 16f3005b0b1..333a4833e9b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26764da6595dee37219d5bbf3e53bad8db6451d7d39fa7741c61d96c89938bf8 -size 51246 +oid sha256:0ce509e6357edda6b8206eea46a7818386423c96abaddae8ae8e886244a93784 +size 51168 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png index acdab75b0d2..c36a7bc54eb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada -size 395102 +oid sha256:cb50ba951a464a7bae750e779f25e2deeda0355a3220fdaf19f60c7c7ed73649 +size 395064 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png index ccfd70d1bc7..5bb1d1133a3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9d5ee3eb77075ca147fc7ed96c83a846dcfcf8e09bc129a24daa5ed19f262a7 -size 51217 +oid sha256:089a84e094a191836ae654f3214b1b3a590a1b13e0d1972c8ef2ac82813b47eb +size 51139 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png index 60abc0e23e0..1e37e5322cf 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:419052ca5383fc2915d09ac2193905bdf69748b683ef0f3be59ba2a75db873e9 -size 89346 +oid sha256:21a0d4fabf74730d3aff1ec23502c18d84cb53899d8cacfefede48ae8fde19e1 +size 89266 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_7_en.png index acdab75b0d2..c36a7bc54eb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada -size 395102 +oid sha256:cb50ba951a464a7bae750e779f25e2deeda0355a3220fdaf19f60c7c7ed73649 +size 395064 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en.png index ccf8c11bed2..970f1aa848e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af63055e54f738f10b9b84c4544ffd27483a1977a8202158dc6866887e61e96c -size 25193 +oid sha256:e83fdb82f802f77ed962957c059a027ab6c1aba43a996fda824c188ea26e136a +size 23662 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en.png index 02192807520..c5f034e5991 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ffda5a15b4c2a56009940901b0f5b102536efbc77a540c9814c4e644618509e -size 25186 +oid sha256:44617db3cb0a2f51f50e983ee6594285e58fb37a363176e6407b8cd31bf31557 +size 26106 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en.png index 5c6871cd9b1..a164702f811 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59d20e7c73b9c6ed4ca590d39a94ee18b58c65e49bdda20982861a870fed5934 -size 28015 +oid sha256:2d3c8f5951f9a447f5f35912ca8ed4c7c845bc4f51d628c6238e61bcc0c3c56e +size 26479 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en.png index 234e7f99d2a..9aab98f331a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9370c42c42cbb9878244c4a1318d7e59725afc90ab1bca818a2a2619a41a6479 -size 27931 +oid sha256:f58b3497ac1f04114e9c3d0707581bf4f2b1ede0224e5a7cea1b4d48e0197a77 +size 27713 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png index 00407fa4bbc..be11872339d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b79329ddb864ea2100974330facffd3a50d1cb60935ec6079a56760fbe8f57e7 -size 54972 +oid sha256:19066a9752af1dbd27a3403dbc0a85d3b7dfa6b794cfcc314934a91c3e680283 +size 54925 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png index 2fea9f6ccd3..e439e53ed42 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33169e5e527c72bceaef87283056841b5cf9553abe18f774ee1b2ad6fadc4a5e -size 65046 +oid sha256:9af45d39c400733792e3053e38612205a8c61ef044101d6beca4710b29e12e7b +size 63435 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en.png index 73b3cec7113..00f6ddbf2c2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab8c08410091863015fdf8e9cfeaf4259f470b500d7901d3dffff83483fb53a3 -size 65168 +oid sha256:ed176e71df15c6da072badc6db227580ab5b4a4ef41932754fd21c9816c6b16b +size 63857 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png index e6322f47bc1..7fc98b879e6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec81ab9e31dd4a2aad6ff8ac92a1bcabbf7f807c8e6ca8b91873ed6706f8af05 -size 55396 +oid sha256:70e6b6c940f935d585e210ffa10fa2926f074e4d1ee255e8607345379d2a52f0 +size 55326 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png index 106ed7843e7..19313223b2e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70a380901f412f68c9b3cdb3b9d157cf5ccf3eb1997110a5037e169b1e592e3c -size 69116 +oid sha256:4511572afb1134a5af14cd5cc0bfc248602761b1b32e35608de7dcba97830ae1 +size 67284 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png index 6c06c9b772f..51acf2f8d02 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee7cdf903b25290a383c19ffa8f55759449469019f4b387366a0c538d59cc4b8 -size 69210 +oid sha256:f61cc2b5b7f836d898ef9fa850230ada79303d4caa0f56005a0cb78bc526c184 +size 66888 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png index 8ad64f6d22d..7b9540d8718 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1bf1424ee914c60298255f489b5df011c930112159f8b978152727ab1f7a553 -size 56543 +oid sha256:32ba97013216a7a9be2564780799f7e4baca9c2595665bdfc20c6d9e1a9cd293 +size 56561 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png index cbd07418a26..c9c389aa25d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:650304d70f57fab91df310d61ae5a816d46c7d00c2b0f02887f176ecdeccc8c8 -size 54643 +oid sha256:60b400fce296214017e19212667f5c6aef597fbddfac8ba74a230496f3c99668 +size 54275 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en.png index 3720d75672e..62df3294156 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a1bdd84ad99effe4448d089473851431c886c1e79f025eb7bf2b52e5b5bfca0 -size 54851 +oid sha256:10b99246041a9448e325ce2ae23647258131e8ea61b6bfaaccd52e173e51205f +size 54890 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png index b0a41c0001f..45033defdf2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bf367d9a4ab122b34fa4daa0bdcdd2488360f571d51ed503a82472073a31828 -size 52717 +oid sha256:b53620a65bd4c18031dc9e03a0d39d265c7c172393d2d29e79809f5ecc473a6a +size 52355 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en.png rename to tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Day_1_en.png new file mode 100644 index 00000000000..e815c273641 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8b6e164de87ef06710aeef334107086ec751d322cd62dd45a739fa850e9ab1b +size 29300 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en.png rename to tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Night_1_en.png new file mode 100644 index 00000000000..17fb62e79cb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.link_LinkView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43238bd863880c513672efd8dbf458738e4a4807071f9bd35b37c79e6dac5918 +size 27279 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en.png index 052885e97ef..8b6135c4faa 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19f6f6dcdf49e9ffd2fbb0ea81c3fe0374faf79cecccc812ea1e1d8c05578671 -size 22344 +oid sha256:8823f1cca4a240779ade39486854332689d114efe2d2213a9ab0720113c7f04b +size 22353 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en.png index 1e43118d49d..226dc557794 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12266ee1fc3477aec0f47e731294ccd2f93ecf4a3a1fada6cf4cb8ba6f7430ac -size 22661 +oid sha256:49916050cd93d9b4aa4a90aa3ecd19bec6095e8b7cb472a3543867c0ce6f9ffa +size 22783 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png index ed2c2a778f3..d0a5982a5fe 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71c1c1e0cfc895d0401b65c3c89c9cd497c97d16d9ab6a35abeadb8215295091 -size 23929 +oid sha256:87982943e795eaee2a230a10574a3d7e6d007b3051b98857869bad5405c7e20c +size 23974 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png index adcefeaabc6..f3b7608da1e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c7de63b603441a2f0660a9528181380e15c73d9fba21efa51fd0bac6d934e52 -size 23181 +oid sha256:eaa756639583aa8ac515c631318554d9a33d3e45bec004d21f1a83eef58d0761 +size 23168 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_DisabledComposerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_DisabledComposerView_Day_0_en.png new file mode 100644 index 00000000000..3ab803e4845 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_DisabledComposerView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fac7865222fa43b756f71af4203fa8c1840caa2e1479d6cbb66ef94fe4cef3e6 +size 6728 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_DisabledComposerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_DisabledComposerView_Night_0_en.png new file mode 100644 index 00000000000..f21d2053b2d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_DisabledComposerView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5192b4472c687b8b05e2e751bb988276f0507cf6b8d85a0e50a46d8abe9f4834 +size 6387 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerViewVoice_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerViewVoice_Day_0_en.png index e7d309320e6..e6200b266c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerViewVoice_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerViewVoice_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42c14b676c3290f0b11271b4f7373f622f8791921fd72094fda550401a042d31 -size 8547 +oid sha256:6cd306c65e8c4cfd2d0e2893b842c68093f4f003a5a1ba3528c6bda3a7163ece +size 8552 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerViewVoice_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerViewVoice_Night_0_en.png index 7042cd7091c..d8d7724540e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerViewVoice_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerViewVoice_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd129d55fd69ddea811ed530857c83fdeeb2ebd68da496875a3a35185b97b2fb -size 8197 +oid sha256:5a90c2dbc00c00b9b76c86f321f78fe36c2f2e9e3444c608be7a363fdfeccdb5 +size 8203 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 37713151e1a..84c33b7ede0 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:409936daa72c7e70334f2ff5d62e84e41b5b79277fefb064b4a9183bd3925549 -size 16357 +oid sha256:1b2acbefb2319d650215b5933d8ef2a4f407b83e5b7f26379aebbc617fa7bdc8 +size 19418 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 dc1fac59118..2ab01f2aec6 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:99c74f986295bd66f52346723a562a8d87b4eca9bda021220e8e916fecc86784 -size 15106 +oid sha256:bdaae3b3badf316b130b67fbf0a2876e6620da4e4f22277625bba3590ed2d7dc +size 17911 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en.png index f8be03d1eff..cfe738540bc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:536272e1b1a06848f0f5ffdf50c833156851530c56d03dba1c0443a4efa2a3dd -size 20043 +oid sha256:b4ebf530f28ccaffdf6c816f51c3fc78903fc9ba4a0775301af26e187d32e6fb +size 20779 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en.png index ae22d49102d..a0944d9fcda 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:078fcdde92926d5bbe965cda4e5ee83d0cc04866b05979873d71a3cf1e6a346c -size 9076 +oid sha256:432f60087c3edff9d184d5aaea7bfc50138c5928257de86ad5cca459f28e03ca +size 9947 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en.png index 95af94340ae..836d72c9770 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6cac5cee10ab9ded373ed8275880534fbfa3f88c7f9219278b3805f30afb766 -size 30876 +oid sha256:a41c6035d2d506b36a5c2678302f13c65c77bb095342e5367370c0ae52c009b7 +size 31703 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png index 54196b69191..55d9a9f41e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ad96f741491f692e470d175e43ccafa8daf63db12dff3f3f5c1575c2133e6ba -size 42579 +oid sha256:a7711aa43b9c18e15475dcf1a8972dcd5a7d60441991584dd9be70648e83f072 +size 43636 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en.png index a8c91448c64..b9296469e0a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:747a75188e93af724433ea20b4ad373260acb3ae5f35bd832f97027646f80c8b -size 18160 +oid sha256:26d70f0c51e1ed3e42fda9db8c5b7904b2e8c28a4de226518c115866fe231e2f +size 18973 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en.png index 1d0bf47b5f5..b15af482068 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38838c6edf589ae6d309ea26cdc672363b1ce2c56a2cf7fd5a5bf826f86aae44 -size 8820 +oid sha256:b499af9f0cb9ad3a849832708e28e6b604dbe85199bd29d51dceaaa5c105a6dd +size 9744 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en.png index 3ceac1e1c51..66f37b81eb7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1bdd621283aa96a109b0f391874c2a2bc4d187db5ec408ab2d38f2d14f54eed -size 29870 +oid sha256:293bc0c9e67e2ba87bb88166ad8f21c6aeb79720f96279cfe33273c0d65d91aa +size 30779 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png index ea89a40bef2..1ca14953bff 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8d25f4a85ad32798451ae898fbfff1d7efab386621f6df01887d876efe348cf -size 41452 +oid sha256:865cdad040d77b82ab7f8d4151595b7843b2d74dea5a8e5a5516ccafb344b5f4 +size 42502 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Day_0_en.png index d1e243898fe..faac6640420 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94d5c829de7020a80f0ce3a89b731febc4a1636834adb2d831650756f3b12ab2 -size 43728 +oid sha256:8fb068f011864a5045c7bd65ffba67bd4b9234f42f40960c1eba1408328f5b3e +size 43782 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Day_1_en.png index c555bbb3c2e..4c690b2ad64 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a59f101c511d6b3ce269edf80cf961a7ef637cc14d36879e79a6af46bf0f1fd6 -size 44853 +oid sha256:741ad1b881c9598c42c91603e84166768b5dc5a713ec191dc1c34c05360dbb92 +size 44908 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Night_0_en.png index f6f120f536d..a8c5947ca0c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43a6fd18c7957df16e71645df99796d922e5f4b530047b366ae9c8d41260d8b7 -size 42445 +oid sha256:0e8cdaa2d979c68923e554754a6b5e5d9b31040988c2d4e6fc8248a28a8aa8dc +size 42409 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Night_1_en.png index 424d924e896..7b65db25b31 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.report_ReportMessageView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f61dc062ae0f2cbbbb1001f6a7dc85c5c1f453a72757ef56e0c931354eb7f56 -size 43785 +oid sha256:ca3973d21ea5575ab0921f78c4443c69c21f1f3f07ca78612c57bfe58b261002 +size 43748 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en.png index 29b1a5cb41d..241ad3330b4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ddbe76235df4343660750e42512a431d10911442db54a2441fcb204ee4a6eb38 -size 230808 +oid sha256:03afa6a6d6c2b3ce7fd8b665e3bdf2b1cfdfe8414449817ec5d8c8f4ec5c6d7e +size 227835 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en.png index 6aa56907d45..f6c22c0964a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcc7cc69fdcae48da59f2bec88950aad996b869813d7f569bb205235efbd2731 -size 233656 +oid sha256:d6ac3b50611b79a15d5d80fdbc3790fda66f7877d58e65c64362577e377bd4db +size 230660 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png index 91eb069a234..446ca5cbffa 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:971d327ffaf387e7fab5d64787fc37d7bd4aa2d54e34be66685009172c04e36e -size 233849 +oid sha256:3c3a0afd6986943481bff9de80e38a603b0682e8d391da52859cb7691f3c47ca +size 612888 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png index 442ba065535..8aef2143fd9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71e0536389e7f914996b300cb5b53eb3728b8008ef225f7107e7401a5bdab7b3 -size 234021 +oid sha256:9d8f46b60243bab1c969723c14bb43020fb84cad4a8c69e7cc2712e2306efc96 +size 611660 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en.png index 8a2d3cde85c..316d89d6129 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33bb2a79802c96a71f23b0e543d9145f0eb7e879ec229c9b228f20af48c4f75b -size 8650 +oid sha256:9f00d7b0c76b11cf20022688c514194f8a4a2e21cb5b60f306fdae1e246dc784 +size 8743 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en.png index 96a477f9225..aa74a600401 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:529a175a4100f820737d840abd5f94422bf2ca8ee15405aa7b818eea87c2046e -size 10894 +oid sha256:2071743ce1a021d1038e0b2623bd400cabb602f4c4297b305c6fdfbae911a0ef +size 10977 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en.png index b0428d53c04..6379d57f237 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1a1fe889724ef16253c5c53d7219e9d551babf386da9154b456f9349fc3de26 -size 21507 +oid sha256:e8dba730bc0ecd6c025ebe9bda083038d9d140d32749afde32cc25df780cce24 +size 21582 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png index c54fbf3183d..a7bff4679b8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d2c41387ac8ac6f06845e584abda8c7cdfdf59c5d969d8611724222d85d061b -size 10746 +oid sha256:a7c1f3c995daf433b45774227dd6564de22b3f7bae87bd1d4660b89b519cbd47 +size 10834 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png index 05c8b7f2cd0..091590ec412 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08183886dd8ecc5e402ab02cfe798fd23153c5ebed7439563f070d0a84ebed10 -size 21131 +oid sha256:2326f58228a4b4af85355d11cfb7a418670f69a7fbe9160ec16fa84e72232eb6 +size 21217 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en.png index 47ddc7ebcec..e9f3597aa0d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91b047d8ea5a4b5913c1e7fb67d692a4f32972b990cf9b773523bafcacafba00 -size 8794 +oid sha256:f8a636e7dedb70283f4e98aa3461fa556ac12406b9561ca47bf4863bf34457ee +size 8860 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en.png index c0ffbd2b61f..c7b664d1763 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f140bbe0fd096841b6b41aa6764af589df4e36db0ad59047b35a57f1ba910092 -size 10953 +oid sha256:81a68bd404eabdb8d27cff3538506e8d313d291376c393b0bd2668a3a4872d2c +size 11023 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en.png index 7bc220d9399..bbe6973cabd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1979f07f6e0173f55888474ad7a5866026035ee961645573775ca482757102b6 -size 21000 +oid sha256:9dde4d710f94db9ac5c5e2664abc761aece09520571649123cbb5a08edcde117 +size 21064 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png index 8bfb865969f..4d953af229e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f96ad2f8c31fed8d0bbd8c4cedd2da39535cd45bb143d6fd10f2e92c470b3b4 -size 10809 +oid sha256:7c238c556521bb3c30662bc83b80523db8cad71361f2550dff2aa7d1ef6ffa4d +size 10876 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png index 3675de2ae92..1797b318406 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:151eb0a92ab7769d997bc3a459c67bb5cc04beac48cce5af7ee3867d46cbdbc0 -size 20849 +oid sha256:a44f81bf344bdb7a287a05f059adc47383b0a86bd0caa73007bcbffce23b7410 +size 20911 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en.png index 5606ece1f4d..e92da8ad49f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6ffa596372693d66770886ac7a863c1ff64078e2300104a805c6c358b4c4fc0 -size 10571 +oid sha256:820253943de021dd1313b9cb791c8b1dcf1c2364e82b46c13ab3006de792df12 +size 10572 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en.png index 15f41e8462f..f4a53ca072d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfc3dc1420a58d25b5fd248f0b77c3c336bb03c402c904d0678c35a5abe291f2 -size 10753 +oid sha256:0a2d290a4df8c178d275a9f6f10f96c8bb143c507bdbc252797a7f4c1eb3290d +size 10120 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en.png index ccf3dd66950..6f2b9d9cafb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8beabd7aadf0324e9b66278c49fdd896b1451a805fd15e69aa6c4af37cb7c150 -size 9055 +oid sha256:fb0d7447f5305bd1f7ae767503932106bc4360b6d709c22bc97b1c0f22b083db +size 9049 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en.png index ffb879e7704..74d2c0a2c19 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:397d76d3fe47c61d112b09355097f103ef3ab38caaa99d838d44498a85969289 -size 12419 +oid sha256:f0ae0e60786dc7d51df1e06292c461aa209c9f0924b93ac63120325a83107243 +size 12446 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en.png index 7fec52751b0..d6f3eb76154 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc3b043dcc28ab54ea0564e355b56d39ff79acd5180c9a34f727cb6e113b2611 -size 14473 +oid sha256:5a0465bed6a57fdcae114dec5a613f4e5ccbe3bb3897b393913edafa5783b43a +size 14476 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en.png index 8f51f4d170e..8c00c4609f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aba63b0f223f8480d40e6c9c48ce44a1ae1a8bdcc3d101030b9a0bd5f1e9ebee -size 23773 +oid sha256:c0bc5e9822b5fbc1d7261e04253dc5ac0f75d8a56795f0659602859ab4f76a30 +size 23776 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en.png index cff8f7e35f8..db31c75a0f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5c8494ebb4ceaf3a31b661aec47f8a33afeb7aab1457483b7009099a6b56f86 -size 8960 +oid sha256:8506037019dd437e2fc9042a4920ff77a33cbaade58b0b690bb74ac76f5700ab +size 9116 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en.png index 4eb8417b2e4..755edd2de1d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a840d602bd7cd65d2302627cdf26882318f9c5bf43d5e94897ce8ccfd3ee7a1 -size 10446 +oid sha256:9fd6be47a1c79ea05e47d993b6bbc63b31e6da7944532a316c9be5e2f9e35630 +size 10457 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en.png index ac1b0df7565..63733c02d05 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4d9879eda9ffa9c171c5351d9ef8e7b32cad50996717445c9b87cdad8a3d973 -size 10634 +oid sha256:0c7590f222a1dc470b9360ee7df118de3e21fc477af60ac907cf15cfcc023163 +size 10064 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en.png index 52d602b3328..f6df784d7b1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:077f898ad7822b6a5ff54429bc91e4faef377fb06de28a4cea73483e6adc5ffd -size 9025 +oid sha256:cc559d023cc8e79d5a4cd1223406f77e6cdd6ab4ab2b1e847d357233265df9f6 +size 9028 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en.png index 3809d0c0d53..225873766ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:252efa710f80f4ea57c909cfd5cfee2beef982a209367138f10972dd5aa49db2 -size 12264 +oid sha256:c87dfe169200962e8bb69555bd0f4f59235f2538f2790250f79459c30efc8f24 +size 12268 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en.png index 3ab5d1cac87..fcdc314446a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c857b7836bc9369cfc950b774479dafcb717d6acc40cf7d8b9b34b99974573db -size 14377 +oid sha256:f255871b94468ba97cbaf0a0490a246ab7d97ea9f4d2fa8ade6b6e296adfa374 +size 14386 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en.png index 8561e181513..d168bae5914 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d0a687a259fafe830560b762a915e478d680dd7c34a295d72e82fc7c427a815 -size 23403 +oid sha256:075b7ab751958221a6aa1df8a691911e45995dce2e054e13b12798a8032e2a4c +size 23406 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en.png index a851ce84e55..325771f2a75 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dda55a19381d9f51a270afa644894f909d8c479be0a5e57b977393c9f1253683 -size 8960 +oid sha256:71eedd04585ea965eaf48c7344d53749b1ecb3ece08e3fdce6b1163e94769960 +size 9110 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en.png index d1f947e8bae..15d2e64a76c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e6af20c51f9641a04b372f2a7af591204d1e6042445b1d8f67eb305fe8ad31b -size 8065 +oid sha256:22447debcfe5de53996985d2e4517dfdb669d221914dbe651cef8dfaf9fb0a57 +size 8078 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en.png index 9bf45e3681c..f7f45bf6cf6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4290fb93be02f761288cd1452729766ac0f22a8dfce4ba06ee93e8cd2da7a853 -size 10352 +oid sha256:00b0a66559b2f77f6dfd4054b47dfa13ceb6757f351da60717564c6e3e1a27de +size 10358 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png index 4c80d9632c9..2699bddeec5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:727085653ecf863579d7fc952bf1d09aab8a41548e087435c1dd7b2b82350910 -size 21484 +oid sha256:6b24689a7262ed4ecf2a4016b5c0f58b5ee80a72a2cd55efcc110db043529889 +size 21482 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png index b3e9b8f8714..1f83b62e486 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8989d1bd6505db4067a8438963deb6bc8858d2a4b6c5d669df036ebca60e539f -size 10163 +oid sha256:3c7f6fd93ca20449ec82de037aa761e225add666b5b8e8c1b9d539082f6c56c3 +size 10176 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png index 299ad8ac2f6..8288b1a3d3c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:518c57dfe5e6f5be40e32922808bb3eabd17f6aed525a5e62206dfee8bb14b13 -size 20576 +oid sha256:868703b973e96966638741c5f41c004a803ae5b4b00771fee2dfd16d9bc66213 +size 20587 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en.png index 5e15420394b..cf7c2e7b1c3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:faadcf4bfce3abe68b993a4829bbef4527bcc833b3799b4053f05ace1f10a53e -size 8100 +oid sha256:f2f998c65c9315d93aa86b79b8bca23685eaad44740f9495201f55b043d47f3b +size 8119 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en.png index 6b14975f4a5..2eccc54d192 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b5d4739a47726471244a9848ca8a650ebff3027d85d569c781973cddddbbfbe -size 10330 +oid sha256:7d9087cffc5c634728f0b2c57996192f9a36d6da685729e8590fc210c9a9bbe3 +size 10339 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png index 5f7f2f5d516..aebc6bb1f75 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cfd7c3d4fa0a2a289a15438092cb272d57be91ea221d73865a04b5ddf58b5ed -size 20996 +oid sha256:c8d05da65549c834dd58baf98bf63a1e51a2ea2bc89147fe4b40e967bee8f479 +size 21002 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png index 90ce08f5339..d5200500ac2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5745d06abd77e48b77fc192b911bcc60ed124af423d87c213fad67733bbb2943 -size 10113 +oid sha256:a6753ad0b100f59e3fa116c8a3efe58ee42c2cee64af7abeaee9651c60dd14f0 +size 10135 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png index e446ff389d5..10d6d2fd8a9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:905386dfc27904f4cf36d26346e7a3dcafbf426ef4a1ac80717a45d1da0dd7d7 -size 20174 +oid sha256:6bf410f8cd99508ee0004b513d0cdf2fdf3cd15d7798417336d59544eb76286b +size 20192 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en.png index eabb733771d..c151d17d1df 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ec7630cd05ce94f7163f0aa6762378080c35f04a1b59ce2b285e6dfa8e12129 -size 111484 +oid sha256:5403784952c6fddee0c59906399330d9f31f58f128f4320bf72a7f26196213b6 +size 278427 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en.png index 2080330b564..42704f56473 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92984da13001f38360a2871de6d0cd84bad33aac91059a6ed9e784cd701ad623 -size 166389 +oid sha256:871cede89b11408db3873b7512f204a30c10461e5182ed1dfc0ccbf459e5ae86 +size 509431 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en.png index 55399a703f8..be7dad6a73a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3619ebab31f741909dc550fbc0a89b143b1edaeb395379314228ab490b726910 -size 121439 +oid sha256:0df7697308d09639445cfeeb3b11ce770442b9e5d05245823b265eee75f34ecf +size 386760 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png index 1b6fb4bab8e..c151d17d1df 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 -size 3642 +oid sha256:5403784952c6fddee0c59906399330d9f31f58f128f4320bf72a7f26196213b6 +size 278427 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en.png index b4a3a867284..09e6c57d8ed 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7de6b943582cc341aebd7dcf553d51b3284414e988918267e7e2ca7585c36bec -size 111996 +oid sha256:1d6888c7a0170c9d5b413f84e9e9b649fd99b89584f0f75c33e2602184236313 +size 277274 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en.png index 6e2d5f83aff..bdf2a68f058 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad066040b511f4270b6b4b05b7043f451d5d8b3cf5b5526aa34c26e5acc8ca01 -size 166511 +oid sha256:c2f098a79f6aa516c3624481c573b730de1d978616068ca2e9994850922a1172 +size 508910 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en.png index 6df8e4e6750..392d24ca1c5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8eb833ac28952ec172d315aca358f44e34984611d98ab1f146384b2b94bd678 -size 121980 +oid sha256:00d6355d80faafa3d23e9048d5fe7f2e8a72259a2dd548e1437f97167b2a22e7 +size 386591 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png index d6fd8eeb701..09e6c57d8ed 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd -size 3659 +oid sha256:1d6888c7a0170c9d5b413f84e9e9b649fd99b89584f0f75c33e2602184236313 +size 277274 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemInformativeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemInformativeView_Day_0_en.png index daebff6da94..f30e124c46f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemInformativeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemInformativeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c64b4669cfc542d79501b91fbbc6f90f223671d8cf033285d568c5d257dfbd8e -size 4874 +oid sha256:a483e181d208c2b05117a1ece9e1c8df6c4e07b7b101adba37b66743c70443a9 +size 4869 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemInformativeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemInformativeView_Night_0_en.png index 709aa7a2ad1..8917bfebfb6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemInformativeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemInformativeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85d73903746bde241daeecc74e1a7ffc51b79c7e263c03975e0533af1839c6b5 -size 4828 +oid sha256:9059ca91b3e87c2decc696bc585cac1dc51783f180d149f17e09e3bd2a5f8014 +size 4831 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en.png index 96a9770836d..29bb0fdbcfd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0e53ece51bf03c62b187ae05bc4fdd71d9f30dcc9b4c18a50cfcb4619419091 -size 12673 +oid sha256:9171394ed6b0633e7da0bf00fa238c64f71b6ed489245480a88b6014ac01702d +size 12680 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en.png index a2c86806408..d10b2366488 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1bbb9c0beb334a94920f3f9f7f6458fa2ea164661a5d78cebb71470368f98ee -size 12565 +oid sha256:926c43d2bbb27ab14b97e7460d259ba219459f093676157ab4f8c5aa2281aa56 +size 12564 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en.png index 47722c0807d..0bc90a7a37b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0db7953caa01f68e2e9b182b58cdd4d02bdf6912ddae64a36fec0636092575b6 -size 46825 +oid sha256:ed44c850f2c85d219c6fc10d9f180f49edd14f1c44a8010a8f192607284d8ce9 +size 46854 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en.png index 2655007acec..639eb9e69e0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01606eff6666f83cef3290e0d986453a6b2b22061a90a60e8db675438ff1eb03 -size 49037 +oid sha256:86cfc57d9caa424c1b5e4984d2ffe6060f5e142168c6a120616f3163ebe8353f +size 49061 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en.png index 2d0a6aa749b..c2e19a6265d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37695b5ff4b53d50c0d2b278f9a41f961a98a2e9fed25ed239914963c3387314 -size 49142 +oid sha256:974d9d4a1af970fc64da8a9aa6dfec69b407fd4855eb643dd6b7a5c11b9d4be5 +size 49164 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en.png index 7fbae24358a..303dff0b35e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b9fb692a5035151c4e2c4975f07c1b51b366065bf8c4a4f25a678bbeb832035 -size 49209 +oid sha256:63284e61186a4233cfb14f969821454b4923a2453350ed58d10fa07210541969 +size 49229 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en.png index 94e34de7a17..78d8d677b0e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba9520116a974c22c709fec9b90f7053664a4938e6a96f3b21fe340717c33a54 -size 45692 +oid sha256:3e778a1a3590c849db350d2d801e600555fe46286d132833ae8243aa7cbaa70e +size 45696 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en.png index 1f62dcf5703..4e58b496384 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:249df92cc411ad26af2700546e2fca7c0621ff4e6aa9747edb1ba6e2e47d52fa -size 47889 +oid sha256:dbf96dcc3c3e9b9cb6dc3300ebe00017d898515a07df85c0ac26a5cc452cab86 +size 47887 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en.png index 11cfce1e3b3..dda4ae9673f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f2e4dd94801ee28848ed04bf3c4846631359873e150e433a397c305b0a33dde -size 47887 +oid sha256:aeabb30b46e100487def9cedff50627d18a73371705dec42941db1bf9bfda5ec +size 47890 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en.png index 2a903d5c0c3..df6dbe9e87a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9a67a0384cb8af35f0154d2f3ec9099a792b6b9ca9c5abc17e1653dfc643016 -size 47967 +oid sha256:234750e0c8d262c520cea6b25866df2b23399643efb6ff9ab2c68e1edda4630c +size 47971 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en.png index dc5b30809f5..5ad0a26fc9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25fa73b168ff01f7478d936fa71aa2d06ee5885e8f3b68723f5824846da55bfc -size 7418 +oid sha256:8205ad34aebb10223b92a1c76ae55a0237be9c0654ae41c970731ccd28efd8f1 +size 7420 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en.png index 1d31720c32b..12644a0f519 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d01d38ca441da42301ea6368dd7859eab1c120889396c335feaf65e88677a9b3 -size 7393 +oid sha256:82c0772496a351ce54967db22475f83e36156fa4322f364df9bc96c15e190ac4 +size 7392 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png index 40ad608500e..1a37bb193d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bee6962a35cc2da956be0488f446c9a3d8831cec97f30e0828440068f764a0d1 -size 34241 +oid sha256:543d5ae9fdb81983d787f6712f6478a5dca7cfcc3091d5879182978488a0dc89 +size 55150 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png index f1d9752b9eb..bec0c7c32a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f08bf0cad0c94da75a0efb97dc92bc3e3c36cde4d833a6a26f9b0d63887785bf -size 47453 +oid sha256:67bdc28eff7830debb89f73c859538f45c73c8d3318414a54eadc14a9426d8e4 +size 101874 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png index 4238d4bab88..18f4e77293c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d5253e52390f9dcdd46db4f8d98d26be7cc8d05176d707a8a8c10adf4e7307b -size 57064 +oid sha256:3db1d6d23850cbc6ae15b4942b6d10d5c9a65acb1d0d73f35f91579386b94d53 +size 147057 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png index 1b6fb4bab8e..1a37bb193d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 -size 3642 +oid sha256:543d5ae9fdb81983d787f6712f6478a5dca7cfcc3091d5879182978488a0dc89 +size 55150 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png index 1db3339236d..c4bfe4172df 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d37431be2add3d26b6fd41a94a4c2970d3303de8d39154f1b36f33ec4ae6bf44 -size 34260 +oid sha256:f7e51e8be368bf0b21736f82f2da1aa9526f7a328f760f1108bd23b3bbbd04e2 +size 54911 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png index c6c6efe376a..e3f05bf82c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a7cdbdd2fac32dcf3c08dcd2abdc8ec96c7b5d56bdfafb5b8594e84ad5da884 -size 47375 +oid sha256:3c446a0219ad13ec361b2eae277e07572e0e13e06b5dbab6c3a6ccbf1508321d +size 101527 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png index 94bac0a1610..7dde6555d83 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0f597867f02b8c31fc094f02b39bea68a00b553cb999e3231f4d046e09da9a0 -size 56893 +oid sha256:43cc19389a22848f730e5f653b82f97581f92f5ba866d351f40a33d24f896923 +size 146574 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png index d6fd8eeb701..c4bfe4172df 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd -size 3659 +oid sha256:f7e51e8be368bf0b21736f82f2da1aa9526f7a328f760f1108bd23b3bbbd04e2 +size 54911 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemTextViewWithLinkifiedUrlAndNestedParenthesis_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemTextViewWithLinkifiedUrlAndNestedParenthesis_en.png new file mode 100644 index 00000000000..0395a856105 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemTextViewWithLinkifiedUrlAndNestedParenthesis_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1569b8df58c7d50962135f71feb063b5109a255a263276f24803429c5c90bd6 +size 20034 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemTextViewWithLinkifiedUrl_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemTextViewWithLinkifiedUrl_en.png new file mode 100644 index 00000000000..5e324a44109 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemTextViewWithLinkifiedUrl_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8950a95d4e484041d4ccd90dd942ef1671b9b06f63e295610d600bab81b093f +size 19282 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en.png index c54ae5d6595..0cb199692ff 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad8cb9863545cd2a5040990aca918d9ec956a07545d2d249696fb0f788ca1960 -size 7568 +oid sha256:5d4bc2844e37773804f17f664f116cb5e308372a0c9190b249f4eb621353fe96 +size 7571 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en.png index 224ffe04856..1b77c3e0edd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:181dea8404461e4504e18e34ecac7bd7bbd74861c8187f89cca1bcca5be0f0f2 -size 7576 +oid sha256:3af89e5335fc87ccd58c1a44d2e307a0c1e6fe9658e6bfb1e72975021698739a +size 7575 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en.png index d47114aa04c..944db7b863b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac30ad57539970ac0162ca54847acf1aca069d692a6400043419c9f67abb8e19 -size 112134 +oid sha256:3d484043eaf6e32e5c0895ae56cec62d339b8c3d47acc1a90a8c5a82fdf89ba7 +size 275766 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en.png index 063dff301d2..b069ff05360 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:516b6f1840f7712c2551ce84e3b9f928b15236ac2bd5fc3fccbc9d108253f078 -size 166963 +oid sha256:b4a7c9bdf67955f0eb96c4cd59ab16c78b5a46980c104b99cb3ca89a8202386d +size 506789 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en.png index 5970115840a..cd7348d3d90 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be389bb29fd43a600a38b7f6cd265fcc02e128f6c27d3e9aa07daaf8b2a81f42 -size 121848 +oid sha256:6233363e62d9dba580117270d8ae3c1286cb132a9c7528a132cdb59f0a61808c +size 383811 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png index a1719dd0a54..944db7b863b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:351b2b559fbe17a1de36c51e63171230b6807702c6b56233ba584ca65ff5eff9 -size 5015 +oid sha256:3d484043eaf6e32e5c0895ae56cec62d339b8c3d47acc1a90a8c5a82fdf89ba7 +size 275766 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en.png index ae287370621..e46a37d9a8b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a2d415770201b30648585d8a7512dd4851fa9dd41fad576945b9d00b321742a -size 112348 +oid sha256:cb8b28095fab428b0f08646e55532ac6c5531896591c32a27d2db0903575ddfc +size 274731 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en.png index cabac7a737f..f64da45b548 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c407338795b3c2f438f4c78e9e3dbeca9881dfbdd6f415a253fc4010c6378872 -size 166970 +oid sha256:b6ced0f778044c0438a812399a0f75909eafa74abfebe8c06eb497503493ca12 +size 506418 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en.png index 4eb66f78b8b..468bb7197d7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a9a400077aade28bb806185ae3f3c1a7aaa10ef68fcb550583b8af0e3349c06 -size 122248 +oid sha256:51d821d25096f24a154b369d0000f595212ca65724a2c751c1ff31a44704ae57 +size 383741 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png index 03bc461c8cc..e46a37d9a8b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13e417d71098d8c863e72753263dfa27f7b398ed5ecb6461f7dde7081fd622fa -size 4801 +oid sha256:cb8b28095fab428b0f08646e55532ac6c5531896591c32a27d2db0903575ddfc +size 274731 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png index 56489451bb1..e899c7d79ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2be7690996fccc6133a3d61114228a0d53dd14bc15018e968d7e9720b151e50e -size 233146 +oid sha256:bb05880b161ebad9cb5c3f78ff217e9ee64e3d6bc07996e32966d9a3f0a2e30c +size 608283 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png index 22320f76449..78afe10f31e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7f8e6bd1eec91e55f1b0ea7fb6c591e5ddcb5f06fd1fc2e00994d657da9477e -size 234646 +oid sha256:80bca1e6c13e4e345a60b53f6c0479a17aeb05f42f3c13a9dc933e0e6d1d7b3a +size 606934 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.group_GroupHeaderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.group_GroupHeaderView_Day_0_en.png index a055cd29804..19a5a35f100 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.group_GroupHeaderView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.group_GroupHeaderView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f3310074905bb66f2dc7bbc667933ecf85a6ce95eca77cb40b3755832051532 -size 24017 +oid sha256:2a66661fdc5bcecd40403481bb6e0af1a6939177366d67ebefb5e0bb37d80187 +size 24024 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.group_GroupHeaderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.group_GroupHeaderView_Night_0_en.png index beb37d84ab0..d6ec7439125 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.group_GroupHeaderView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.group_GroupHeaderView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce83c58642f9b6784432c8fb7a317650d2ec63d6e8ec92d132ded5827a3f1f0b -size 23690 +oid sha256:d3f2ac3184f1f301ab1fd140f17f855230347793a6f1d65b06e094b08ca67095 +size 23692 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_0_en.png index a0e14b3b7eb..f2ee8b92ceb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58876112b3ebe320f85ae814f17b1167a4ae81855302cf30ef3b4b2fef536c78 -size 4594 +oid sha256:9480ef97ddc3c073834c376a65467e1a640c4d16b9f911f3cd3ad2ecec50a3ba +size 4596 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_1_en.png index 36fe671fa56..55967b65703 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c08f8f356c6223e1473766744451a294bebdf3a57a65ae8439143a15bf5ee56b -size 4436 +oid sha256:6a3ea4f485f82086cf0d8fa927f8816041a37fbb29c3d78da09f4684657db310 +size 4435 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_2_en.png index a0e14b3b7eb..f2ee8b92ceb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58876112b3ebe320f85ae814f17b1167a4ae81855302cf30ef3b4b2fef536c78 -size 4594 +oid sha256:9480ef97ddc3c073834c376a65467e1a640c4d16b9f911f3cd3ad2ecec50a3ba +size 4596 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_0_en.png index 7b268d666bd..159aa2df69e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b74b282cd8014565b890c567b0bacc0b353277500d88e9f67471c712b187a656 -size 4581 +oid sha256:a6488f4506a6591935dea9a0deaf6e580492dcad691f076de96d216dd58f220b +size 4591 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_1_en.png index 70a8d14c742..5da86bf6ed1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a7e31ff4f1aa8c1c66bc362165f3bcde66fee701afa48f95616002b505b498d -size 4425 +oid sha256:bc329e6823a36356b9a794675e37f9e8a64a0da7e89bd82bb1be22afd7b93854 +size 4431 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_2_en.png index 7b268d666bd..159aa2df69e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b74b282cd8014565b890c567b0bacc0b353277500d88e9f67471c712b187a656 -size 4581 +oid sha256:a6488f4506a6591935dea9a0deaf6e580492dcad691f076de96d216dd58f220b +size 4591 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en.png index 02414ad0c3e..db5ba1a5aea 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2becc435316b7adb15745bd0afbc2634cbc643b3919b2e7405b8a2b9b310acd0 -size 4451 +oid sha256:c899d8ca8e98ae73c8acd90cef3967834901929833c4d8287356a320f2bf4c0e +size 4523 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en.png index a106ea73f79..b9cab062231 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb901534fbf1e05988710ea799d94daf9fa6a881edd84463ca0e065282ffbdb2 -size 4439 +oid sha256:f7579ab37a0ebe9671a46e591037fbd8991d3265e203d6ad0dcec6a6b7ceb588 +size 4386 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 16e9f5cc433..829cf58db89 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:138be910fa88b2e59882b909f5d83824503d88676d23d327c1f4948121cbebe0 -size 14297 +oid sha256:f4494a2cf13d267a1051ea3fbe4476d2354b8c7827d1b93aabacf5bbe063dc3a +size 49269 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 5808ed69cf2..8e03a6e18db 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:4b9ce5853af7f03ac91da6bb318bd4f033936279d211e87efff4ef6ec8794f51 -size 13988 +oid sha256:affff20aa09000132ffa8535a81d888263e9d530144c8b96e15d25c1873254c9 +size 54426 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_5_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_9_en.png rename to tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_5_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_9_en.png rename to tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEvent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEvent_Day_0_en.png new file mode 100644 index 00000000000..8384f46c274 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEvent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2ea87ef947697fc3ea21c8953149db1c729be5391a4fd65f63b7b04aaf0e8cf +size 10465 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEvent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEvent_Night_0_en.png new file mode 100644 index 00000000000..5f1e275830b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEvent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ce07ba72b4542584273ee4004365fe50da4747e4aa06c46ea0d6b192647a1ac +size 10204 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_0_en.png index f498c22ffeb..a76b26e9aaa 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74bf9b7af113c4299c1d9807ac017117b3967e2c6edb0c3f84dfb2e8aa5efc70 -size 5543 +oid sha256:b491cce2773778b379853898e495301778fc97bc31e045360b39a2b09f29f844 +size 5789 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_10_en.png deleted file mode 100644 index f60f6ad5b1e..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_10_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:643d87a9c1f2befdb5407a88a7590fa8598807db995d10c8b9c6460910a61b2e -size 5594 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_11_en.png deleted file mode 100644 index 0b38c37c2a7..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_11_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:27e390a8045e54f5042bf038bba1b24df23e160abd662519d16483323d638ac6 -size 5411 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_12_en.png deleted file mode 100644 index 1563e58f860..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_12_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9fc37060281ce74491304a8b64e10e1664a29c1fb0d590ec80ac2e8336610198 -size 5780 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_13_en.png deleted file mode 100644 index 183b92a6cbf..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_13_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f179e72eede44c7a1add39698519b07649bb978dd63170405c57fed261bfa871 -size 5618 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_14_en.png deleted file mode 100644 index 41780cbc77c..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_14_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d068727866699683382b9b2344d685246568dd4a1fa27a91bf73c3b7a4ef76af -size 5773 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_15_en.png deleted file mode 100644 index 89f4be732c0..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_15_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d823b36a5eeb8e888a5351633d47f8fcc70713021d224d37f302711c189398ea -size 5605 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_1_en.png index 5bc122015d5..65909d6d48e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e65e7d7fa6a9b781e67c28d160278988241e73107f721bbfb3298dcb317d167 -size 5377 +oid sha256:675122178c926902a7c6b92ca15b7652a3099de144b9f990c7ef8a495e41b148 +size 5833 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_2_en.png index 0ad5b2ae4fd..67df9853a15 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6ffa4545e93d9ff24a32c94b4da23029b7316537c7328f4347f2a3de257c9df -size 5504 +oid sha256:6b5981fda0fd6d7016f77c4fe9955690f2d6dab5825f320f1795d3fd460ffa63 +size 6076 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_3_en.png index 748bf14d95d..5b61d53e6d6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69eb45afa5b3b408c4ad0a713f87df56e6574e23538ed33694f352502b8401c8 -size 5333 +oid sha256:a9b027e16bf586857cad161205b7ca0445932d00c1f6b630dc50126394233153 +size 6192 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_4_en.png index 1685d89c08b..d5ef562f0e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3ea0f61fd842467be5ca9804265c29243b329ed7852a55dc7447dc3140e6658 -size 5807 +oid sha256:3fe1e1da27262a08ed7c59d7c0951e72836b26ec554688f0426ac9a38f436d0f +size 5846 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_5_en.png index cbee49d65e8..ef512ba6ed2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e37bc6f4a19e25c238f3fb6c6e919a41a1da3fde930b951b7a08b15b7ab97e1 -size 5666 +oid sha256:8022a0fc45eca8d244a4aa2ddaa25e50c1ff03ad380f39c4c4693b8cfc978148 +size 5902 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_6_en.png index 97a25a3169a..e20b582e849 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6b7b54481a82deeedbdb66ce90191a4894fe25244cc3c70fba571fac74544e5 -size 5899 +oid sha256:2e1a2505f900b21e0396bee6733b5ce9170fddd9c15038f121abbbe30ae95281 +size 6076 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_7_en.png index ca29f20049d..f20043707f1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfd4525483b1be593b9ad6fb098ff7597c7300e6c5b50e2b8316941c4cdba405 -size 5704 +oid sha256:cb5c7bdb55eaa8b5cd532b3b1b60158a18c4f13f559b1b6040807696343b8775 +size 6096 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_8_en.png deleted file mode 100644 index 43a70441126..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7450b6d88936c9ea016d6bc39dd9bcd6efb61e28a3282d2f7808a178e8ecd355 -size 5545 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_9_en.png deleted file mode 100644 index b3d1b807efa..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Day_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d29074ffc73aa10e27b4cb89b65ea736d50952e060bcf312a04b417e7f253e9a -size 5387 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_0_en.png index c8aee9eda7f..c58b83d0ef6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96f0584206f23483d1ad6831dbfaece6f7531edcc76bcdba11d7cd1aa57fca4e -size 5659 +oid sha256:c98e2c35da18aaa8f5fce58515e86f171cc4df8787d34ae3b1a9567d49c4137e +size 5924 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_10_en.png deleted file mode 100644 index b88bbac0985..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_10_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:00f73509f6efad9f6f2ce54f3a9706b9ccb2720e9119a0c9ba7abf8eea3856f8 -size 5603 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_11_en.png deleted file mode 100644 index efee6943a9b..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_11_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1d1fc4bac417bcd906c813e3d0442b2d38593c9e9fdaf7d93aa6dc20ecb3c44 -size 5417 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_12_en.png deleted file mode 100644 index a695072c6f0..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_12_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e7045956cb362717e97cb10573cccf7774c872664c9b2178e6e73195eaa9c76a -size 5980 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_13_en.png deleted file mode 100644 index f80ceab33bb..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_13_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8e2728835c30759efc96ba5a7d93af9b692fe059ec2f09b879f4c045ab6b6a5e -size 5804 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_14_en.png deleted file mode 100644 index dfb8a91ae21..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_14_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2edea761c0bbd7f9d0d68f39eff5d69cf5dc9599a71c27ba2c5730d862c99692 -size 5801 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_15_en.png deleted file mode 100644 index c8963479ec3..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_15_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a35938676790d0d69ea6688f3e5d1c0440684dd69ea2e97162e60fab8c250c26 -size 5636 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_1_en.png index 28315f21c13..b75dd051f08 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22d7f35612dd607d238774b3dd23ff07f8a22b882b15386d6ebed94eb6db642d -size 5483 +oid sha256:d8ec24f4135061fbf08aebd29b4b7d8bf3f988bf3b0d28bf602410afcc4a80f4 +size 5815 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_2_en.png index 82ce25b7e4d..373431a91fe 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9dd8b04317b9c35c13ec156c94c275074e6779a6ed6505a44fbb73d384fd6e3 -size 5512 +oid sha256:d86a47bcce5e883f2b94b98258fa7c944532d96f0db518d3bba4365cfde530ed +size 6222 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_3_en.png index c2e484ad40b..ffbd6b1195a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cdb682c8952c3c0b687e391f412ac5a39c2dd6b8152c12ed161efa045fd97a5 -size 5344 +oid sha256:afff10df66307d03f037911b93d58396ab401e020cb74952a4ef76d8564a6cde +size 6167 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_4_en.png index 2e766a434dc..80a74dbf26b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ea127648d6a70bffa2cba9a9b576c3625a06e82fcb2eae3adc8426d621309f1 -size 5892 +oid sha256:9e0d88f051140b6a31b3007b6ab92f9ba742f7c2de60dc3924a05834533e0e96 +size 5976 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_5_en.png index 531b18baafe..499fde9e32b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c3da8139ef0f63b421b5637cd6f89ea5a048bd7804018740d528dac4d2179db -size 5754 +oid sha256:44113dd74f49efb18cb98b363c8ff01323cdd8aada466560a8360205732e56d5 +size 5906 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_6_en.png index 702f44844bc..5c5992d39f9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43a5225c0a3a6709fd220144469bb2f7961f4257816ed70c770093f1c2b5dc18 -size 5854 +oid sha256:f47934caa1ed08f3c3a9f70830367b7180f5a2e658d4b6c99f39e339f51dcddd +size 6235 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_7_en.png index 344b5be2f8e..c77cfeda163 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29975b9dd1530aa42a10302c45045929ab81c87e970c70c432a4aacb1daf0bcf -size 5686 +oid sha256:a13158fb8eec57147ce15ca8ed29ca485986b99fe4f2801ab166bf6b90cc586d +size 6110 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_8_en.png deleted file mode 100644 index b2efbc710fd..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cfdbc0d7f4d7ed694f91f3e8fb9cd02701e2d2db98373bba6361911983d6d225 -size 5674 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_9_en.png deleted file mode 100644 index a30a2e62c85..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageEventBubble_Night_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:956670b8fb12cc76126c8fd9bb79c877ebfa81ade6783a91b8f2147a434e23a0 -size 5502 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png index 4a3994d148f..aaeb39225f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df2ef6fe59cddc72475c85750cf76384056152c8271bc480748fde31e7d2ef13 -size 36201 +oid sha256:dc2234e1f00b0edbe3389464b1ccf375a034bb4f6daca6c80345c85dc2d4a267 +size 44822 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png index d60d1bd163d..9c06f766d9d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1ed76be54d00c0290b315b2322288d5bce83834c8b382b11b83bc4f82bb9cdc -size 35103 +oid sha256:caa64bc210b30027a5f0eb1ea8c20b3d170b37ee3e1ac8f535233b3209feefc7 +size 43436 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessagesReactionButtonAdd_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessagesReactionButtonAdd_Day_0_en.png index 77d0ec8505d..f6390b1c5fe 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessagesReactionButtonAdd_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessagesReactionButtonAdd_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfb30e1058f704ae5f44ee4e66342f9baedd6e2b16df1f190cf822da6c595731 -size 5042 +oid sha256:c326b7454f6962e156cf043ac8492120e2065ae2e9cc46948e458052fbc3179f +size 5041 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessagesReactionButtonAdd_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessagesReactionButtonAdd_Night_0_en.png index 2c097237e21..d850efb749a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessagesReactionButtonAdd_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessagesReactionButtonAdd_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:993f6a463c77ec57573db206f31813b9e977dc533ebb7d447acad29a4cc56abe -size 5186 +oid sha256:9d922ef07c2f6b1d8de8435969633402d9130fbe057582c54d2fda04a09f2837 +size 5184 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_2_en.png index 8b2f180453e..3372fe9a312 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e924c2fc2dc6b66146364d999b77874c883fae2abac9390a1a48c90f61ebf3f7 -size 4928 +oid sha256:a0faf9fe9bfb693677702c674e7888ad35c95457a2931283710df25b63f825b4 +size 4925 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en.png index 2d422864fa1..4e3ed79281c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a4dd7d2d6607135bb9e18f684a34df17e0ef1fd2b48fb85bbc6ba82b82e09c5 -size 6213 +oid sha256:0c3c0729ba11de1dee2e8cac49a67d3e7d4625e4130ad6948401af7ca77e9e4d +size 6212 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en.png index 35852751580..2a089de0011 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f45afae6e96d3cddb50eda9acac94ba7b556e480eccf56e5f7c4c17a7a5bfb7 -size 5009 +oid sha256:68da96bcbc36a9ebecda5e83d9d0ceae45d8b69d844633903f58e9915a18af52 +size 5012 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_7_en.png new file mode 100644 index 00000000000..5396163fb11 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de9876d82b13a220cd296f6117bedf7c1a260d9560978701065ce068e22b1c8b +size 4485 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_2_en.png index f703f2dd982..85b52b4946d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ee2232b7d0feb4c7310a695f3b1e58ddb466970a8cef4051e35f7d83dbb1629 -size 4863 +oid sha256:04f2470a98af964ca1ab6abd62ec6aafe339f86ad6035eb4a8caf1cea42000d2 +size 4852 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en.png index 37f721d4c4d..5357f627bba 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfa1209e3c4c97dffe0f633643a8e4da3509d57998cb047651a7e18daeddb566 -size 6080 +oid sha256:1a3b363b2764e32fef338debbfadf5a4711bb6e8f4ef799375f2fbcfaf127069 +size 6084 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en.png index d5a4f8dde6f..5e7915a5787 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e07f82770e48bcd04bf5532dd353d0f84d6128911e692a8e728f1b768d0b947 -size 4988 +oid sha256:30468b30d88386850da0be274531dd7fc708e5c49bb91ee81fdd4f0fc2e90d2a +size 4986 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_7_en.png new file mode 100644 index 00000000000..7148c0371e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dc95cafaeda3d0cd45ce37408de0858543f1de07a29b18d165cf2c9a438e9c6 +size 4465 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png index 53b231bc1de..9fdcbf7e6cc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cf7e89fac181c1a0829e14d4ade4f236267e4e19a3aa2aa5c216f7f86e6aa87 -size 169050 +oid sha256:6693e2e93d21c61c1e3bb6b57bdfca788b8e98aaebf861a5a3ad594344441195 +size 379233 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png index 6a79ccf27f2..bac357c0b88 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20ff9eca2a1ce793143485d886416503fef228e967ee606595dcd726a85c9832 -size 168179 +oid sha256:10fafb2e4788ab76d609e74babe55048b44440d8bc6cfd35e78e8337a210b240 +size 377760 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 8899d3f9365..9f6c3f4cb7f 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:c9cedb56721217846e99cdb8b61d00b8cd58ea69d4a3048b252999a36d27684f -size 167010 +oid sha256:b3d1789882ff07396fb4daf788437744bb71eaa7cca127fadd1bae3cea343116 +size 294216 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 f1f2ffd49ad..5add18e222e 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:832f4f74800d327961b252bf8b147d8f6065aba82209e1000894602a2085f226 -size 166144 +oid sha256:ca042e33785370974388e4949b871c9cd8936f1578f383088c47d12514f8fd46 +size 292986 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowLongSenderName_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowLongSenderName_en.png index b0d8acccdda..bbdd286574d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowLongSenderName_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowLongSenderName_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ca49a36674d994decb5710a0b969458928f003a0f39b701c1c60afade4f1ae2 -size 15776 +oid sha256:839f50a35d5e9f13051a233a4d4bb75cb5de245efd16937b856c6e05768165cd +size 15720 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 46a51ca8b30..f218ef6eff6 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:f65633c60060d3763aecc775e1195ec20d61ecab22cb142d91a4b8ac2ed73976 -size 152510 +oid sha256:660bd025d6cdd9a9cc43309276c46cf14f2567322d849e7ccf99d1741cece894 +size 377499 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 d11d0b0568a..18cb5301bbc 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:969a525cc24bce27af53ec127d9e8c97c53c3f136da8b21e0e7c4947112f4311 -size 151776 +oid sha256:daf954ba004cd67f85913e8ff4215a0d32bcba8726179ed77bfd8c9557ce3ddc +size 375420 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_0_en.png index 1e1d23d6cfd..07bcfd74ddf 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:928e775b4328cbcdfc8f8c13099fd6a53048deac7a876a0651a772dd854cd02d -size 28576 +oid sha256:cd8fe1751c30b5cbc6a9d31227da02b0eb647fed993134ecdfee360f68198410 +size 28544 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_1_en.png index 9cb993276e2..5b5bf2452ec 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcf77a1ddc786a985e4e1e56e553a17c456b8d8d0a13bfa70914ab39a8bd52ef -size 31114 +oid sha256:f2ab83152164617d6a76f84fcb92bc6c16e102c58fb489bb504b3c914a326a20 +size 31111 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_2_en.png index 4d0dbd931df..8a84354ddb9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93f6343aa0e76535d28717b12271b8d3b123852d9294b060f766ae0989febbd8 -size 30271 +oid sha256:500cf2222f5f0d35a7773f42de8f547bd6a81e447dd5ca383ba3d0e3ee208c0e +size 30225 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en.png index 4c7f4dce27f..863589bcc15 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96ea6176a048b8d47637d96345e7d3f64cfa6f2c0942ee022dafeef080323f66 -size 32592 +oid sha256:afc37df583c55bd7ca8295da4dc6148bc5e8fdb01612b2563cdd8108151227ff +size 32541 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en.png index 1e01e697ae0..7f4ee2edb6b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2751b08b35dda3d7b897cc0618964935aa968dc5cf1738e852286e8149c69a2 -size 33958 +oid sha256:2e50ab664fe28a486af410bbd93074d8d42dfa578627285f8ad22496755f01c5 +size 33929 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png index 9678512c21b..18f3b8cbc10 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7a5a6750213db875928a93c8116618a0194df089658763e570effd7a288b51d -size 30706 +oid sha256:87f484e7ee64d776f4874883bdac7fc321a04af86e3a29bcc93a4ca2cf2e70aa +size 30666 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en.png index 26a6d1ebcc0..10cc5a767fa 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23cb761e3dd47d763d046d53ccc1a1b76b3bc3c53dee6038f2654bc5bd75b111 -size 30571 +oid sha256:a7ba63ae1a0d204db4318a0ec1041532e1b0ce2327908f37f494da04c5a5b975 +size 30533 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en.png new file mode 100644 index 00000000000..b647981436b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c87881859fcb3b3c08e4e583604ca455e2f178ea605362f255af8950d069095d +size 11267 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_0_en.png index 4f781b50e29..12c46fb9d10 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:658f03aa4e13b408243826ff0f1658e354a44402c3b39eabd229e5cf086201df -size 29136 +oid sha256:8b03dfd48c56e2d0bb108a147b2ee04cc66c165bce1da7c724e1ecb4ddb4d286 +size 29139 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_1_en.png index de7dca86005..8c64ac2fa67 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09ba6ac814cd860273d25b4cb47e891e18cea86db33f3a96619c742c6ccd2ae3 -size 31768 +oid sha256:561714bc8011f7f56d89565f1e0e529ea3626aa5162da47058d4469f16af0641 +size 31742 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_2_en.png index 7e8efb7399c..d91da948376 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67163c484e2a0bce436c6ba04b9201a4d258ef38727c127b65ced0999d2fb579 -size 30575 +oid sha256:94e94d793eb718cccc535a58053d5b4dc2de3b65b007017a8c5647112c78cedb +size 30527 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en.png index ec53631cee6..60aee97b69a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c9fb8c24b7259894d5feee5c82b0d0e43487ce32ec37208bb4adf451988351a -size 32895 +oid sha256:a9d8e84329989a02b19269f6294b209fb66c5f3f6a35de7f9a2eee6da517dcdf +size 32903 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en.png index 27ac7d39df2..ca990cae351 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0748ada58c8868cbd971cc5dd97408ee272c9c6638391fe9c47190facbbd0e76 -size 33894 +oid sha256:25d0849402abdc3639c8bef1da49daf4de20124f367b3e1e413b0dcbf53bbff7 +size 33863 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png index f5863c8d04b..8d28ce67f54 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a5bc7b480b31995f7828229b6cc70b12688552cd08ed2ffd35e66fca5539101 -size 31076 +oid sha256:ee7292caede2b69969c7f62bf025b3cff5b59ab99eceac51ff041e6fddea9389 +size 31082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en.png index feec1017f4c..37791be7d24 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e4a33d0f86354eb3930c3227f96d4012606cd81609eae76ddfd5d9f7f216ceb -size 30893 +oid sha256:898ff20bd42490e66ece05e6b6435ae951b004b69b44855ce9c1f0b0ab06d635 +size 30894 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en.png new file mode 100644 index 00000000000..bcf06c96f2f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14bd470786ea2a00673adec49d674fd2e575512187056a7c3fd56bf7b4140841 +size 11204 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en.png index e847cd3292d..02e8c70de96 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1c226a36c42d4aa725a941ed58036dc5e75c9547720e00b75c64c7fe33491af -size 31647 +oid sha256:8fd4bb89758d8253aabbcfcee28d2b7962c97394fd2c9bd37bfda15dd8b59abe +size 30255 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en.png index 562bcf09f20..f9c581a64e7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47afdecd896bef06623b81ca53e0b60a0deeb01014903906fbd3fbf15ea4a5f6 -size 30592 +oid sha256:6e6b5ee7f8475aa22f47ca7643f6cf93a2f170710859c22e1c71026fdc1721a5 +size 29445 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en.png index 348a85cc4a5..d9f727f2612 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a5525a54573e55affde093f036e9d9dcf9313fb545306bb367e3f92cf20b48d -size 80132 +oid sha256:dc29aa14fd015ea0a4a2783ff553c086b41c86757aae760b64451f78deca9d45 +size 80122 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en.png index 7492e5284a1..d622d385b0c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3ba6d9e751b63bc92bbf5198e430f4f5503441de822b669f72c20aec9412e79 -size 80522 +oid sha256:9adab952ced4b0c51b0e3a5eb8c6cc7c8c717d79dc77ae1eeef328119ebcd597 +size 80489 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_0_en.png index 5e74e308e2e..3cd2696467b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b958e710dbe02df0227d8e911133c2fe1a976f8b473d11d0ff538f26315b756 -size 25544 +oid sha256:7d46bea9b9177b9f9ce4e08d8a33fa0161662c1611d5ffcd2aeb8aae1d1ad441 +size 25553 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_1_en.png index 0e52bb6071c..197785a217c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b0a963f8dc72feca1f87ddc504e41b9d7d1d40242f9ead03a82ada4ce41bbea -size 25012 +oid sha256:dc61b0fcd136cc1087dd0449d80930b86b2f8c8440b55b756450ec26829bd9ea +size 25015 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_2_en.png index 5bfa10ac6d8..f0dd1f8797b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1950690f0b07d0cdea54f486832e2c89ec7c633d745bc2849185d3e7f14bcf86 -size 29316 +oid sha256:2b1d4cecb06128bfc3de98a1a927515d2770220927bb11bb4be040c8ac365274 +size 29310 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_0_en.png index 461a8e18b64..3520dbd9ca1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef17d8523647d65cf4ffec14af068d5fec1185e3c6864b45ba66f0fed21860c2 -size 25105 +oid sha256:e56a10732744eca3e422be7c7509d2e107b0b93b657fba610812d3c351bfabbe +size 25088 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_1_en.png index bc80a4a665f..9b134ca9cc2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62ea3fd76c03b02cdc2367faf1b4ff20f5cb88568cabd2da50b37b004afa92ba -size 24552 +oid sha256:55b22ee6757a6cafb99aa4f35601638dc3dac31c09b0369187379c78583b8e1d +size 24533 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_2_en.png deleted file mode 100644 index 72a7a31c3e8..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6eeff40cf2e0457b210a784bd5f73303efaa3356ff650004c6de3b9dc9138c67 -size 29911 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png index 2e06388c92b..f100a0416bc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c86e8244db3e63a63c173c554a419eab63445b4c4bcfe0f69a4900fe19faf6d3 -size 151392 +oid sha256:192f07e6dd138911a30e5bcebf75a68ed59bad006041e9ba2fad809cd8a94f11 +size 364786 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png index 8c00dc6f7c1..f015705293f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8400bc01f801b6d6c32dd5d4f6792fa8f89b831e10228f7875e8f7e89146d1f4 -size 156895 +oid sha256:3e4f5b7139fb1759e9e9c17e0911ab9fff66f4ee14545f2f5977a7d1ef8c7aef +size 369967 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png index 145494bf10b..76decf4e09a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77817ceca6ab94dfd5ec239f281e15a0fd4be7a0e0a7e036f825710d5cdfe53d -size 151421 +oid sha256:4ea40e231b7a23ca3ececa68672f13a7391fbc7fd3719ba98d67e02d6cfe0034 +size 363024 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png index cc7b9b4548f..8884e46a510 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9365f8bbf8aca62b6e76a0492dd7c9b28481d14e16ff89f44d4ab57f000a991 -size 156559 +oid sha256:b12cbdf1b95c272f5482c43686ef549801d2c3d5bffd75b6423d6a3225e40efb +size 367997 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png index 9d71a5386ac..c740ebf66fe 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45553883378459e04d228dd13f5e835c81718b1dd07db10f1800dd93afc32ee9 -size 142634 +oid sha256:582fc51bc3449b2796d3efb0211eaf14dc73fa0ca1817e1d53f29586dfc36d3f +size 353092 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png index 0db679cf2d3..9f42bbee34e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc0da74b89149a4d0fd136bf56e2aab545229c01fd83a72cd4fd4fb9d8fa84d9 -size 151647 +oid sha256:540912d38358bdea807311e7803397cf402b434f16860ce434aea7751cf2b28e +size 363471 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png index 81cff9aa707..85ed81e3d85 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:545647e1c8c03e1517c5cb6fac8caca4eb293775714f5d8ba3c44b15e9191a5e -size 141956 +oid sha256:6eb1b7b869f25e0d505ba9ff8dac4e6b63c4f59e7d8854a20233b8d4d77ac1e9 +size 351780 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png index d3d6501ab33..0b0af20ed0d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5ee904904ca3a9167174a61f5ceaec59d3ba67fc783639bae168959f078647a -size 150760 +oid sha256:d41539a90d7f82364dc3599423ad6a1b51e53be0e70df926365694b75dae6b8f +size 361273 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png index 33c722995d9..cd30e4d1e8e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c54d751ee267a696c0dcf591636ee00080db05208692fa4c7eda444396727dc -size 156265 +oid sha256:d086f9df50219d9134d3cab7e4c198d7bac812fe614d2375e80c2e00766d7161 +size 369509 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png index 468fdd6071e..84c0c6732a2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90e38333fd30ac4714249ab8709a30621c4fc1f2e0ebe753df0a114678df019a -size 142215 +oid sha256:9d11576c0e48980bf1505d074a84f11170f042abc92cc80c97b92f5d473588d7 +size 354292 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png index 30e8d2501a5..8aa5dacdce6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48fe571432922b2ee2891a582dfb52fd535ca8c2515df98ed44016d2533c27db -size 154251 +oid sha256:77dd4e2eed54c37a022d6a6beab69953cf64fd9725df0b954eb329e647a00036 +size 367658 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png index badbd682e8b..afc2b8cbd0e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f9e3ed9354b363106baeaea5002a85bc1e030e5cde5581a823ae996fc0346a6 -size 163443 +oid sha256:d1312e7e3a6b0ead94a30e0d4971277305469fbbaa2015df55334c37ac1a0136 +size 344733 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png index d8e7d370684..47dd865024a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8b4b1a0b5778fd07679a866d00306c9068fc9c72a1512704f87ef071446f772 -size 144974 +oid sha256:0000cca03cefb845bcc349669b40abddbb5567c4f7d79481e1d17edbd1b705e8 +size 357300 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png index e773d290b3d..d6bb3466d8b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e8566e74889b763b3843c3f1d98e6dd002ddadd67e26a6de7b91dff2bd57932 -size 143450 +oid sha256:c9f8957975959f22f321edb1da337268630b2035670ea6a7f1d0253ceee23904 +size 356520 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png index 13d7642a559..9a01b3a0c88 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08caf86f4b7242139a1e53a0b8d0736782be02496b858b1af9a1aa6209ec33d3 -size 150903 +oid sha256:59865d91c14d22ac51f03c5a89f22998aec74251ba61c0db013e549ef71bf724 +size 364285 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png index ed5b5e3b38a..ecb2198054e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6be795f587d11a9bcb8ffffb097ff6cf2bda119fafda15ea48421d4b4af05e0 -size 142574 +oid sha256:1a13e340822f9b7faea91575a5fe8d82a078f7dab567d5f3e573c6090d342d79 +size 396959 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png index e92235eb7c5..387dfc2067d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d31b4335075205b9332baf8e04daed8e57e8a5d4b8c145cad91498acaa5f9cf6 -size 143373 +oid sha256:9406216df3f9744d7526ef2bfa208abe22796a5e95c1007bcaa6305b7a7e6dd0 +size 355618 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png index 2db6bea09b1..59d432c2011 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74d7af64a2c2736f16e7f367116d0be0c6512d89bd1fff859f4a99207b9894ee -size 145167 +oid sha256:b8369665c1e4e9e6e34a44897d6496946beef7563703bbd64b0dc0182b4f81ed +size 357542 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png index 633c7ceebe0..f433fe1b6c9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:104b43c0106da2234ceb27250c632e17e9acdb5351cd4768e0a345ac5143f340 -size 151442 +oid sha256:71e21fd5b02aa8ae98519996986e3bd28c37353cfa16fd832f83790a0f2946e5 +size 364744 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png index 597638f2450..a6b63736dff 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6aceaaa2ca90fc3fb6ab3b1af31e8fab8676f737574617a700119a1dbc4bc48 -size 142845 +oid sha256:dbc3ea0006d57682c5a59ccaf873196b6f5c329d300395935e355e50c9f01906 +size 354991 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png index db8b4f61c52..c9f4dd107d2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aee3dba6d86d7a042649a39757df61f27ec23fa957ffe8a5cd0c77fac6a3925e -size 156135 +oid sha256:cdcbd58a833ac5554260389c9d455aec0bc40ac36bcd378d1a47fd3d3e684b24 +size 367589 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png index 1fa64f47087..c1d8902cf80 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c29dc6c1af653d70969e73cf1f67c9787a5156b336d7832993d4323ba5505bfe -size 141996 +oid sha256:ba5dd0bd237dd41206cfb92cd5d57e7c46710fb532fb447139014b8da811a324 +size 352565 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png index e66ad5870de..a127b82fa85 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9adeb033047491e9eff08f087bd4aa3bb1a3cedca4587d0f04935a10548126be -size 154279 +oid sha256:3f4d22986976b525a4f4ba5a6ad5372ffc0c77f6b3dadbe3989cdb98ead2caf3 +size 365876 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png index 23b32cf68d5..1b81a6a6e41 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad72a84a11b175855cf80c608a0b22fbd3f582b25d511dea35a91e7d860a59f4 -size 162172 +oid sha256:3d551e6c58cebf53fda518877916e13204c52b4db2af3151678546d7b279e14c +size 342627 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png index beb4e13cb59..7330cb98e60 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bf85afb864f47b9d11ddf00be361e9b60be7f6c92072ffbbbd0c0a75d4d2d9f -size 144907 +oid sha256:93319839a10d070b61501e95bf3a555b493c21d5e328213373fe9d0c888e07e6 +size 355803 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png index f1af7350c46..edc5dd51a78 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb600b890d2823da01ba95f7a5e1a44c71cf7c87c00dd4178a9939ababa66971 -size 143402 +oid sha256:ac9a160d9e58e3bf44a491e4f1e3c37720ffd91d622729750fb8219428dd2085 +size 355002 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png index 40069060f8b..5176beb76d0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0e9dad1c0d8cd9ef6a59a05f6d10806c499bae39502c8bacb4655a9c99517f8 -size 150857 +oid sha256:ae4443f10013aa72805aa09b36688cceb013012424843b00b6eef0496a66fa7f +size 362403 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png index 56ce7019441..7a41ce872df 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf9232176cb465087624f5e26a7d03cbcff0287000767fc328f71c8e7397a6c5 -size 142471 +oid sha256:37e3043ddf1e8fd9db9c156c9cdb88fe236c7f76e166a75871fc388bb448ec07 +size 395179 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png index 2d67fad7386..f5c45a8b064 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7b29247d5f3d9a6e598b661542fba680e849352c6fe8bc77f8d206d89ecf49f -size 143071 +oid sha256:2a842b4b9ea2c1b7235444df058d501de4ac57cc7749b30fd91f934502a7429b +size 353792 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png index 658dce9da07..40f2cae44f8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cfdfc3db13e98e93653e71ed8030eae544a74ce4bacc7d9fcb18165a69d9484 -size 145144 +oid sha256:3b27153819669af47324316fa7b6433b57fc9cf64928de3f9acbfcdd0657d3be +size 356028 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png index a0f31948d3d..dad70f4bdef 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2138f335a2e17ba9beaf9f9e30f3a02517bf3a3841d48ab9287621f7954fca8e -size 151370 +oid sha256:009b303d3bcf767a4cd58172e5b4f21c1a341ed272ff5ca9f62ef349808740dd +size 362905 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png index d341c7c0558..0cc82f902d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1e0c669adf450d9720df45424dd59c51f2d2a5d867bd166a56f9e7e3cbe0483 -size 142607 +oid sha256:434720f8ad2aa0cb2c8fd708cc7467af92e063dc2c3600a3783762e0364592c2 +size 353266 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 c5c02618790..d4e9b0e675f 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:e8595ddd19afc9ba533038fede1d22f4b2f73c339dbcdcfd180039f49ada24b0 -size 184211 +oid sha256:ff6e3d41cc1d3a51a4a6d48f47407832d838ed135752e200f588cb12062c18c7 +size 412176 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 5f38409082c..88638ca095a 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:843f2ba8f9c4ce1ba229d4e4fc517ee18ed599cdc6b03a165b738eb774e0ba74 -size 183572 +oid sha256:dfa4dc6a76b73b8b2d48234676de66eed10750ad2c561a065909d4745ab6f4e5 +size 409878 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en.png index e384e2f1c5a..b86b025e1be 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5846bf42b05878c92d080a75ddb5f6285f74f23acd8676dd7937f60729eeb775 -size 52521 +oid sha256:544f7bbee71640356001f0242487e5b10e8e69b4e1cc9d336d6ceb388ff97dc4 +size 52410 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en.png index 601699d4617..e511ae721f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c40ba76a333adde1f32b628ef3d58278a928d3469528c74f4a65218bb9044027 -size 7707 +oid sha256:9e0bd3d7e5374f17cc08ec05e8a0ee0df873093afd7192b0ffdbcc9f8d336691 +size 7711 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en.png index 4af432ab1eb..05c4d92b696 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e6348ac9e1b74900165063cf0abf55b174799dfc4939424b193933e7d72d2e1 -size 7857 +oid sha256:e6584ecab4a1edce29cc737473f83f31f00ae76ea006bc755193fb923b1bf9f3 +size 7855 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en.png index 2213b2413de..1c611df6f15 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37d73e9663a9cfa1d3a69892f159ab27a8bc480bf24b64529ca978349d9e2842 -size 16823 +oid sha256:bf32ecc91206e24bca38616066b94d6d451f69b1d274dff63e30b73f9b0b5fe2 +size 16822 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en.png index 2d8226e1c14..74699799d55 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24184c76715671c2485dac6d6ad0a2caa88bb9881bf2ae256d6c298efeac3846 -size 16876 +oid sha256:0d791d1c3a79e409a43fc5fa3aa368ead2c8aa7167f86e50d477aca6341482f6 +size 16877 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en.png index 138d43c7c86..2657113f0ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10294b87b703183236c057fe5fef7dbec0ed663a03722496a1d5bfc689ea8cb2 -size 24915 +oid sha256:8f3d8c59fab8106fabb9ef38ea08274dbd6eec1138ff5fdc1a5cafcd58f6a38b +size 24918 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en.png index 8bd4c370ecd..af6e871b747 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a73bda9b33c533d00f6c54dba4d6669589c8d0d95f71642908f2f6e05ffa5d24 -size 25932 +oid sha256:e739c2791ef4530fe2ed107f187ad9c92c83e5b49d8f1e4ea7381a0259abda0f +size 25935 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Day_0_en.png index 6e5e7be55f4..d9ab5b6e9cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44d84d8124913424f6b0f2b7685801a735ddb0888d99e7f87e04497c871f6b8b -size 10458 +oid sha256:2fce12d74225547cecff2e235597daa20438fd9f7808992c2d9e2e283e191011 +size 10460 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Night_0_en.png index 57b66567e4d..304ba0849a7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ac9b242d7dc0c5bc15db85937de3e76197ef7775344cfb52a1efbeedc4d9dab -size 11038 +oid sha256:ba5ed576050c76ecb850e186e1a8d015d5548fadc0c4a02c8e8f24483c246a70 +size 11041 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en.png index ed062db32fb..0188d606a6f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb18da560195c672833e45663803c6b3c65c206eb4b4de79a32cd1b91ac3f67f -size 24066 +oid sha256:0002edabe1450333860f6f4fc09a4f4875202f32bf62fb13a4b08793fe3116fd +size 24065 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en.png index 9bbeba41d8d..34a8f34140c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c98454537185e32d9368fd68b3421789f03e5a3ed8ceba21428326c8313c6bef -size 25036 +oid sha256:7f22d13894b5d7b460f1d1496ef1ac2af7674b3ebfe8e642f70f445458f8984d +size 25035 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en.png index d383d5067dd..bae0139967a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60e866fcdeaa07dfedf98a5c4b43da4047ab859c8681679a7baec03349e6c92a -size 23979 +oid sha256:6d351c5483fe233198ca58aba11193a75e38fff2ee9ac4df6e8c3329b1fed6a7 +size 23983 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en.png index 3ac9fa53d43..6fbecaadcf6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:207f625a67e1be1b4b186b7b3215ebc1c7f73c37faf2df987631486e967d6828 -size 24822 +oid sha256:24eabd49efe1b6b4ee36e99571bb454af1e99ce5902d9b291df861cd08d33541 +size 24823 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsView_Day_0_en.png index 56b0706c3cc..70f29587fb0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d902ca9a9fead6c3717aedcff0fab9e65d2d27cb320ae4e88a642e9c49b93564 -size 6241 +oid sha256:6eee476d812c9840b2a53ae938f9f79446c252705e329ff66999fcbc96d3face +size 6245 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsView_Night_0_en.png index 74d1f6d15c8..5f8cc0c2518 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemReactionsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd159b959458a2273da7bb0e03cc92f7496987b364329601efd893ddae864df7 -size 6622 +oid sha256:df0fe66ce8a3874805aae2775d3af7531d63dbe7033a041c7dd8e335d1b20cf1 +size 6623 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en.png index 3f23320fe82..7a7a4d70286 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:956ef0189107838716b55278cecb81bdced1cf769930d7649dfaa04a100fe4bf -size 32902 +oid sha256:0ecaa69aebfd037e4b58db4dcbfb41d29ffb973a88dde1f79b60574bc65e84ab +size 32909 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en.png index fa9be61ea31..f5388ea42ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35e77fbf0ce9b4888dc8470a478b4ad3d8b329ff44006b3ed6095fd4d0a90e98 -size 31300 +oid sha256:bf60d51e707192d0ec421a53a30fb97af9c09cb19a94efaa4222e1a5b5b78bf4 +size 31304 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png index 0664c186fb3..630f40e5be8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ad5b8ae4808e02018d3508a8d5525e6d6a50a1bb2b3c269e7aff493bf7a9181 -size 37397 +oid sha256:fc56f6c3203e71a98395f194b020f5a82030947a7bda56299a89adbe8df52486 +size 37383 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png index f2807d4ad69..b2eccb99fe8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4365177e203ed78cac635259ce8a7e32bf71316e45839a29acc07ca8540331f3 -size 35248 +oid sha256:b56ad84259529e985235a0e42589ea91544ffce1cea64d0022cb3b3c2c056316 +size 35257 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_0_en.png index a22e03550ff..bf71b7fb25d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36f98cfbf710150976311cfad03d28f1a6f4cecfac417fa6b562f6acb5b32a69 -size 50144 +oid sha256:bf3f23066d6466f799677dd139d92ff26aeccdc7ce20d1ea4a1fa2eddbc28ecc +size 50157 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png index 603946dc271..c00b560a881 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88e93741ed388372d2c2fbf0acdae2e0d40cee76f955d73166009e557eebdbcf -size 331229 +oid sha256:a78bdd930bc853c4adde525d48f8477e3731cb3e76fc6802ada6350f73e209ce +size 331247 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_11_en.png index cc3728a12d4..e73f8c4cce8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de798b2e9c58ec6a2b3ca4addc0a1c1ee0d42ad8ea4cc4cfe148a68c55dec76c -size 85203 +oid sha256:6eeeacf0a4282d7fd5bc8ef3b67680d72d254c51a8ebd009c9c197180a9a6f0b +size 85092 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_12_en.png index d7e78df0f55..6a521014123 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1519dc4ee64be29dbe82c552ac843031fb5f01df8ee17dbb1ccb6b5e924178f7 -size 51630 +oid sha256:c242b43c5674dfbe72ad5399ec2a1094fb5d9c8d2ff5d627e49c6f51010fbdf2 +size 51631 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_13_en.png index b19d991f9bc..276b7c713ef 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca067c9ebf1c44c761dc60736987170e458dd04aa7c00d9cbacb89856a93d03c -size 63388 +oid sha256:9aef1a43f5478f4a8e2715211695d0dffbfdd823d8642244c4411eef06794bee +size 63393 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_14_en.png index 58da9219b42..643f3ab3b67 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a9101972099ef39d05896261e00a6fdfb35aa8792c4c2f18dec7b8c9cbb7cbb -size 48006 +oid sha256:117ffd9be0ee1c2ce742c1725f935662ddbbf060bf02a3d121f3b331f577eb03 +size 48007 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_15_en.png index a20fc1db15f..d5247e1d684 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7d84338cdffb77eb42badccc37d9e21b2bbd4b25cb0256944ad60d525724f24 -size 64455 +oid sha256:986daeac63a5185195c9972fb6d6d64cccc1eec53855c755b88d8e4c364acceb +size 64484 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_16_en.png index 45f88fe612f..c8788f93d5e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf2215400c0a91cac40b5eb9e4e350443858654c2eca2bc098cd6a05a1807a97 +oid sha256:4b9bfd23b94519ee37e26e5a6c99e7ec2ac1513cae6cfa8465c71856d1e8afce size 55197 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 4da3e6185d1..ba6aee96063 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:9604363c3bcf9818ca6946e3682bbcaee0220a557c3e481e92fc67be68cb4671 -size 64236 +oid sha256:17fa1c2967b460d47e33dbbbc1f10085941d2861a619c9c4e09d0ad62a6a0e5c +size 64217 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_1_en.png index 5c4336be5de..fa42cdf3616 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5c4849fb5125781a2caf190a8f3340035e755cebd4b97d235d26ce54f227ab9 -size 71139 +oid sha256:30ea27e644a6ad86506cebaef9ba6d3558b30945ed57e80ddcbf6cd22896d408 +size 71126 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 93a042ec5c4..1bd497adfd9 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:9c0531cdcc5d6af2d51e3cea9fea6fb23dd8fcf957a525470c27db5ebf740f04 -size 200824 +oid sha256:a8fc7a5830027eb95476df6e378269c1ff37d57c75aac50e3353647d4e84249a +size 492060 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 33d5dcf9b28..0a12d7dbb69 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:ba729e8bbd503e7fe73c3562f57c234ff2b02e56399b0e40e58ff5f30ae6e81b -size 201867 +oid sha256:949ea4bca530c513ff4e67f4b3b67a69215931ab1fe9bc42b02ed347f02a9bf6 +size 487167 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png index 4cbc5eb7c7c..b8116a6486e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1df19b2aa5d3544efdef608585e0ad6305c81fbd3e0ad8017ae950c14f58a1e3 -size 70255 +oid sha256:8479ff3816b6d08e1b61f9326f525f55f40b6a542baedbfa5f5acbb5c766150e +size 70264 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png index a0b6ec26057..8181ec17f11 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2fe80dd136456c6b8a091165d6bba33631a994c014da4248f59efbb51a736e5 -size 84989 +oid sha256:4d9a77ed75ee8797ff6e76b19d8e873fdcb55b2c361094c875874403887803c1 +size 85027 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png index 60428c55e3c..312b7152499 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a105bbd340c597da31876ddacb89bc90ba8e7d5454813755edf5a6252ea6f985 -size 72749 +oid sha256:4767b56c7df7009caa31e905cfa473edfce505b9171214987a0c2be8f76f0558 +size 73338 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png index 733f835563c..60aeb3f3303 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d61791b1bc4d3ae255a63029b772644e39befa5a2fc4022e5c8753a4178522e5 -size 103041 +oid sha256:bccefb7d0464ec26c54333804962362923da1cffb834a31b35ccf0460a6420ef +size 103533 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_8_en.png index 87388685687..26a40610207 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84a743de111a89828996968d5a7812a075faaab73e637cc9714c256a39771654 -size 53116 +oid sha256:039d1a97453e7c74cc8e2c04ecad719b3dffa1cc023b611e8f6e909fc9d51090 +size 53106 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 63a6bb4892d..746b23606a6 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:f2080039781871927f6afcaefc6e8fd5df6a4155138d1c78244663bae4a964d0 -size 374223 +oid sha256:d49b09108150b2c53d6a54c8c2e8f417d6454099ad37ce0c95a23685825533fe +size 374231 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_0_en.png index 3373710f31e..ac89f3d57be 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59b63153b6f88ccc39529a73311c5e7459764e5a10208c98ae5217ad4e391daf -size 49214 +oid sha256:ec0671a85557e44cd629bc2c426601ac3cb25a0cb095ff2034d73ad0878b1344 +size 49225 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png index d913158c677..74e21a46256 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:583ece5dcbbf4769cb9254ade84a7ff99854295e2e9102f248191f062db16c5d -size 148661 +oid sha256:b62bf46257ee066163663c8c86a717d320146f50be88a3957e747fa512cb025e +size 148673 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_11_en.png index ba97caf3c87..c0fd6ef9db1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0850c06b9ae1240e7c9fffbbf9a1168197c5a87ee04607054844bd38fd2cf2b1 -size 86334 +oid sha256:09b245abbbc0164df2f2d6e88e5e02c66267ba50dfa20134241cb4df6d7c770e +size 86341 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_12_en.png index 636ec36c914..849cc3a0774 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b4867382de313aaeec90be3a3bf2ce86fd136044deeab681793434ad5bfc4bd -size 50862 +oid sha256:349b482857cbe0a6b2d7787e177205d1d5d3f564b2b4a9f2f2e81c57b90c0e74 +size 50875 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_13_en.png index bc8c73c2902..2bb32b2a836 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f38974847a9d279b7c15403923a24525f75700eacee4b0eddb147231c03c121a -size 61904 +oid sha256:c8221d98791f152488a3f53dc654157b006d465a7c9770c1f72884d0d82df723 +size 61878 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_14_en.png index dbc4a992e49..3d0bea0389f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51dc16c48ae9cd10bed51bec32b0d99b4367dad0da8fcc1816d0fb32d651dc00 +oid sha256:5b754901c4b62950ae48537523aeabf734d49425eb13211075c424c4a54f28a5 size 47162 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_15_en.png index ceb9d679d5a..3f5a9b19228 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c570b3c97adff92acbbf4f4c9e9bdab4b0bb7a7612e69a4c715bb9e0d19abd6d -size 62861 +oid sha256:e57025ec733a1513ec594e217519546e363b07df1cab7547d89aecd19f8c55f9 +size 62874 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_16_en.png index 431d4e0ddc7..eb7205c572c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83e76acb9e992577c499e316a679f6c1567afa07d6dc1d097488ab6cd115f8ee -size 53915 +oid sha256:871ca03e59cecb80a0f587ae1b8de0e015e094d032bc0e09dd3bae135dde1b7a +size 53936 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 eed3a30a427..30d3526c3b0 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:b3e6be9ea405183bdd747c788e21f0928f6cabcb10a3f1eb471797ee8728e792 -size 64275 +oid sha256:0d380a9ad7b10dadc52f7b485f999ecd0b2b121494815eef6cb7dc6090273e76 +size 64309 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_1_en.png index ae95a98bf75..b8373406d2d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c948c4b9eccedde55d53da19f6cd1c8cd5d6a6e16ba909edf796b5535e1d079 -size 69346 +oid sha256:2039e099ff7fa3cc0fff14efaeac2049d5bf44196d95f652852030278d84d788 +size 69310 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 07a1027631d..a8aa36e1b8b 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:c0a18c96105384799acc08205fe1f132a4432942b0fb9b58a2d5e2617bc6590e -size 199675 +oid sha256:78ad98cc2dbd846a72b6f481677d0964689ac7f165be662f8b218d6546921dc6 +size 489004 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 0c145e25b0a..7d0e54cd424 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:0c84d0ddc031112b9838dc15fafa12951bdb26ed3af3c1b94ced43d313251b25 -size 200421 +oid sha256:3aa03a48407e9c8c5214fcc8392e74c2323a73fba8418d7a088575c6756612ad +size 484132 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png index b396066ff9e..3b61862e829 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:642cbd2fad7a9b7cffdb531f5de021a137f2379c2415b5dc25b818ad232df54c -size 68795 +oid sha256:41616d976daeb5528d013e20d6aff81f5ad8d505f9561fa02ac6395b6c03fb0d +size 68754 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png index 0ec2e46dd56..470080efac5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d87b25d58faca74f0caf580370e7ea218b315b8533071e9fdae5058b8b028f7f -size 82915 +oid sha256:2a33a913e668ca5e9cbf32cfd5ca6ce3b250e4de468763ca8cc52852f147574f +size 82873 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png index de3c35303e0..4d813dbb313 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:319e3a13d378e4c29621f374d96f33790162d944dc033cddc68c4c9a5027f53a -size 71682 +oid sha256:d0959fb663ba273bb318671502767da57a1005a3123cb0383a4ff8879d6af5d3 +size 72120 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png index 070ad9b1f92..663f92bf7ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0560cb1813e06b78f421dc08859bc396adf2e8d348f0439cc9ca82c507c8bea0 -size 100866 +oid sha256:410399234cd0a90f1371fbee9b7041607bef515643450945462b2f4e7f899a83 +size 101174 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_8_en.png index b8d2ce88697..9682b0ee924 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cbbbff4861ffdc4b75b9b435e898349c86d0e72167d1ff512a4ef88089eecff -size 52758 +oid sha256:a1255723686d5fde13b2c93f8d2a61ba8a42d92d41c50a5ff39b3fc1fd4edbb9 +size 52725 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 852bfc233f0..e7f2974ae03 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:798082e7d988202f48413d76c0ae5c286fce2d3c22cd182a69f30aab0d6fbe47 +oid sha256:08770904e7962ceeb239a3f01965879be825fb03e91b1f64d04c05da82f09005 size 153199 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 4cdade6e95e..33bbf7d3d52 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:d3f2c9de8eeafe23994d98605942b910135e5be357cf16fe61adcac88d5f25a8 -size 57402 +oid sha256:119a16be61f48adf4be664e2ac086006edca82c12b5955827a29b87088085e0e +size 57422 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 5c905c60bed..80f277717bc 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:fc48b63a1e4e91a7d91e19fae3a7f1d419117b94b330b1f1ca2ec0761840fb99 -size 57430 +oid sha256:a099b2a5bf006c24489835aae46724637c7d2c3b08d1f47904bc0e0ed93b187b +size 57445 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 index c2b9c93351a..83ec1b08f47 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f749ce126bafb5f3a3027cc1ce06897faddb6a052175956d3e6c7459c2222ed -size 60503 +oid sha256:4d03bfc50eb1d3624f1dcd14b9d1cb515fde906f1f3688501239330c91fe2aac +size 60516 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png new file mode 100644 index 00000000000..ef98edf77b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ade5d45c72d66bc1ede4ddd84976981d7cfc59b90bd17c97fcfdf6ed44fdde9 +size 59753 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png new file mode 100644 index 00000000000..59cded12225 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66ad9872af4087c25611715b1d369893daa948aa2e1e4b8bc6fbff6067c3295e +size 59685 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_14_en.png new file mode 100644 index 00000000000..38e26d3deb7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45ead771fc133b1c992d090f35c93f0de4166b52966b818d8a401e75e5b63caf +size 62982 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png index fcdbb89b2be..852403d7602 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:109e604b9584e18f1955f9824c43673cef227aba704fdc98b86e2798ca29dabc -size 56561 +oid sha256:0555ba9f6b8f9110ab81ef0e94264ac6b2c43fbe15c9e196ece63f25f6021d47 +size 56581 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png index 94446737093..363b1f1c5ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dca6dfd32a2128fe68de26e03156742cbbfd3cb858f82871c940a693516ce223 -size 39171 +oid sha256:b3b712c21975f5950fb792375b8c89e2bf843a92447ca5333535bcb56b11910b +size 39238 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 9cb1f42f410..cb85490a196 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:1521e7abc43b6cee8d783e395f9ab67295bfab09acfaa26f271f6583a6c008e2 +oid sha256:388823a879ca9d7d7794bdb8fd03da91e4c47c03073e38e18039467832c99c5a size 59831 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 c57fe9e758d..9c67fa0694d 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:62dcc6e1b334cc706ce1520c08c4cfc730e112a9991283834b225ce0548f9e7b -size 55221 +oid sha256:516a010d56f5cfd205b46de49b49d730ad2012fe574e08eb1e6746adf07ca373 +size 55224 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 9cfd8d77af3..5b6906e54a2 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:bc1755d89b6ee527e81aab0fba75f7642f5b0932be2d61b0202bd6bcd5f60d5a -size 55193 +oid sha256:4493799d889522a540435c10f0bf3642a438c38d6b219491afff4a72a169ce1c +size 58416 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png index 922d09a4308..ff037f124dc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a90aeb879d3819506f3899d822f5f70e29728b9650f22aebbd307477f7d95f41 -size 54179 +oid sha256:61f05b07526302ad051a8df2234a15438f8b2b2889767acb7697f3caecb68aa0 +size 54189 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 3e6af825891..e70c83ec54a 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:392dcd73f5f4996b0f6780114e3ed4ed6ab408e7a694c17c9636b2a082daa6ae -size 58352 +oid sha256:c053c3df134ed9ee672bba25db0e730aad83a47454ba9e74981e8178e6cfb59e +size 58361 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 01c7b438a6e..13e75e4e896 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:d89ff85dd7a99faf50ddc167514c15f9f86f8c083e5acbf602183c01bf42db07 -size 58982 +oid sha256:6d5db5c87b3b1a8f22a41a64ea39262e2469d805cf7b7fcc6d73622d1f8b052a +size 58999 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 49a60f84668..fb7b1a6405c 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:0ad3a8bfdd7eea2b3ff9b24ded91f46fb3f9d9d5e2d169b99ccff42fee8d460f -size 48905 +oid sha256:91526175f646a959c58ea84a0479bb7ae7bf821684ae329201d8c1d5ec772e58 +size 48887 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 164a23e8617..57388f1df14 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:5ca213558819b9265e82d3b22703bf1ea798e7bd8064b7d8694f4ee56ba40d0d -size 57020 +oid sha256:10ae9ff40ba2df95b741d516f31994bb1160df5c2b1407daa78aaa882e0db5c5 +size 57036 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 db0b54c5742..ca4e901e9fc 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:2c58e77b63f813bae7784a29bdedc19bb067b2f736b1c228bb146ca8e8f46756 -size 57068 +oid sha256:cafaaeba38646ef92103d3ddfd7c3c26ffed1e2bf1586569d2194a3aabf4c183 +size 57088 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 index 330b0f53173..480716e90cf 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f4319d951caafd65c7f8bb574e450465e87a55d5fcf2c4f42177cc1494f8b92 -size 59771 +oid sha256:cec8142f1f293d3e684ff7eb2002d05dca6ea959cb95dce327e8a4f5a78cf462 +size 59797 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png new file mode 100644 index 00000000000..7732b265fb9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7567339acb87356f182961883bfde348cbfdf406eedefc2ecb54fba9cd26e2f2 +size 59291 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png new file mode 100644 index 00000000000..9ee85846e4a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:668c3e76f40df6ee3d64d595b840d81d62ec3f52474cd2fc261109aedbf46a69 +size 59291 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_14_en.png new file mode 100644 index 00000000000..2b750a8c5c4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5864ca4671dee1caef50d0444ffdeb0c7d2a5d9ed28ebc64937efbc469f6031a +size 65288 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png index 51b4ed5c882..e149b92acd2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbc4b083658c24dd574bafd3e66ed9298622f6c5ad0ee726c24c1bca6294217f -size 55881 +oid sha256:c6c0840f6795a3b3e612c0848e2b54cbc977408fb5432074e7b5771d1bcf030e +size 55906 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png index 515b536adb5..6eca2d3098d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4d9f0142ed6d83339bfadd1736a1f832bffed776f31f7995f0afdace690a2c3 -size 37300 +oid sha256:8289d23929280efa5cd5e688c64a8bdac5ef966e0cbbe331b9946a51c2703cfa +size 37320 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 5c69f0e4152..fd57712fe56 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:9dcf745eec6b0cdf29be48bd1d7e637fbd7fb4d70a42f0c55f0fd53ad5d70106 -size 59235 +oid sha256:88d3b3855bd73c340619cc2ca14a19cacca304a3bd442a4c681f5dbfe248aa34 +size 58950 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 26099d557b9..29014feb9f8 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:b0a321dfdac5d5b966c223972b9452ecb8eea34d36be8f57bd03801399678f2b -size 51218 +oid sha256:4182c26a99a119fbf450bee488d92be31e291f259fb6d21f84f05d77324a1840 +size 51270 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 b377553a6dd..e58339319e8 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:1e29e68366a29aab72a9e5e79e20e1a02e5869974cc63cfe99365bdf31e413f1 -size 54784 +oid sha256:b44213fdccb6b194466d3daa7474b77378f96ab9b35559c0ba392dde49ecf175 +size 58055 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png index e1b9edef18e..bbccfbac19b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45df1ecf99266ef43891a1dcf3b3d3b99c57d1b51179028589b74f83e942126e -size 53739 +oid sha256:629506ad32c56d94906a71a4d779290f1328030d79fd05db3754e4113531587e +size 53656 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 c53e05731d4..2f0466a60f5 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:ef712d64b0ef08669a3b6e87045711ac223155229f39ca0a915ab86a3a58d694 -size 54141 +oid sha256:b30bd082aee7c78292c602c5320ade7738c0909038717cd5f8004b08243a2b3e +size 54172 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 1fa6a1784d8..7ace45e8e17 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:38ee3e41f6258befb1970113c5f638353b3f9c67a34fba581108cf92b5151e65 -size 58500 +oid sha256:c435641bf1e5f862499fc34247227200a28d47907e273c5f18331631d36fbd60 +size 58520 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 46f84f01a12..1f1a7f3ba82 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:d3403d72f765aecaab595503edaa8959c2eeca65c2d2a23ca5b222a80b70c51d -size 44767 +oid sha256:1f87ac09baa02003c48bfd4b8c946e650afbd6a42347a1a85a205bce40cf094a +size 44783 diff --git a/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_ConnectivityIndicatorView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_ConnectivityIndicatorView_Day_0_en.png index 789520675b2..4586de39f0f 100644 --- a/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_ConnectivityIndicatorView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_ConnectivityIndicatorView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b62c398c035a386bdaba180f3090e122b78131c741cd6fdb97ad533137b6a15e -size 5531 +oid sha256:289da5d5bd60210d50d3e398b4b930d9255eab079137fa8245fe3a4cd5c382d0 +size 5530 diff --git a/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_ConnectivityIndicatorView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_ConnectivityIndicatorView_Night_0_en.png index 7eff425d13c..b25e0985197 100644 --- a/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_ConnectivityIndicatorView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_ConnectivityIndicatorView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e55da84c46cd5d7f211f7323425f561b234c50144b8477c348015828b5656be -size 5386 +oid sha256:dbc78ee3adc4780bfdc8bac8192a7414092cc6412ecf18e3d24753769dbad868 +size 5388 diff --git a/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_Indicator_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_Indicator_Day_0_en.png new file mode 100644 index 00000000000..4586de39f0f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_Indicator_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:289da5d5bd60210d50d3e398b4b930d9255eab079137fa8245fe3a4cd5c382d0 +size 5530 diff --git a/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_Indicator_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_Indicator_Night_0_en.png new file mode 100644 index 00000000000..b25e0985197 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.networkmonitor.api.ui_Indicator_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbc78ee3adc4780bfdc8bac8192a7414092cc6412ecf18e3d24753769dbad868 +size 5388 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_0_en.png deleted file mode 100644 index 51d29f82920..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9ce6f663ca845a54b4e1aca55f1405e00d29084a95b6a61721e5136efe30f7fc -size 311755 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_1_en.png deleted file mode 100644 index d712c96ce36..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5bd68fbd9f16f6d20d9fd8caffc1c766ec98c80cd60e33a31ba262241104e8a0 -size 306554 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_2_en.png deleted file mode 100644 index a303b4946f8..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:563c2a20cbf651c88e85dfba98c02fa60197b298ee621b5b4b1bdc4af51a456e -size 310172 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_3_en.png deleted file mode 100644 index 202144a2d8e..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b833e535b35cb63190900aed69d7b7e0e34a4499fde6a3cbbb93980d7d79bccd -size 304787 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_0_en.png deleted file mode 100644 index 3e65f22a15e..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e41a284f2eedb0c7158079f059e05f8f505436ba91a82194faaff459bb30e4b4 -size 392983 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_1_en.png deleted file mode 100644 index 06aa397de0d..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c37df776d96b70e3c1dcb8818022f04e08fde2dab52b731f4c3b5f690d925de7 -size 380243 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_2_en.png deleted file mode 100644 index a1677085608..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4f2c6932790cd4bbdff4dd07a728290aeb2eca47811c0b3d220e17aed5e8e34e -size 383588 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_3_en.png deleted file mode 100644 index 076ebc920dc..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f59fc12655ab0696f8af05a725f817bad80b71f1b8f004e20ce40b963208e07c -size 365118 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en.png index 22094243577..44d30ecf582 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca67b2bf4a3e0d654d4ed3186e5d70dbe82448ee2028b7a30b9c780e5332b294 -size 21622 +oid sha256:b7cc2cea4d546d18bd4fa7c327f6b5ca42d6021c01b3e699547c47eb3e00ede3 +size 21658 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en.png index 31937d60bb0..6e151cc94b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e359f9178a033fea29952aad2892e3416ef6b4b7924ca085963140d35ff4d22 -size 21268 +oid sha256:6e7fb2de909096d9c88197d2c1360bf1998d7ea092b11719f3549722ed342c9d +size 21281 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en.png index 054061a5c80..f965bf20da0 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc6bc3a1bed25b08233701dc3931a9c5057c17fd811b36ebcb8fcbe78f89fec7 -size 21466 +oid sha256:2ecd903f3d127c0bc14de833e0724cb9942a246677382aa69e9d113b2ff6704d +size 21395 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en.png index 58d00585e91..7b716ea37cc 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c5581a46a24bfd40fb4284cf6c77c1955c74830d134d379caf863704e3654e7 -size 21018 +oid sha256:ce73d101556c4cd97b46a4ba720c03f0310bce96c5b88fc3544ad6e9efb00e10 +size 20982 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en.png index 59eed4a5cc8..32abb7dcdc3 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a2a1910e21509ebbaf5117e2abdef0dee6bb301afeeae821d0240a72e5ef721 -size 21538 +oid sha256:00e6514bef617ab044917486aa8d54e9422ece062fd956dfcaa6b52493d150fa +size 21492 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en.png index 9afe1b76304..43a95660f52 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4051a95ba6a727fe0f688bd56e14739ddc617b9b3466b80c6bc60c90f91f4a4b -size 21123 +oid sha256:ceecaeaa2193d68066bc54f19d4c33087496d377fce3bc4b38241659adf201dc +size 21046 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en.png index f3372cda75b..382ddd43c59 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6775a6973259a21d6fa47666bbe134786795c4518b55ca2a7bd8aa84348258c -size 21870 +oid sha256:8e6d21979af7d84a43854f203712c4fa7cb49411e238dff803d90bc1a4d98376 +size 21917 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en.png index c5ccdf3def9..dab60b94f1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fec8262afb8861cb2d8f00417171553335cbe12bfae9f78c022c29f44efba08 -size 21235 +oid sha256:c37af705d8f682cd5278eb0e345b4c66a7d1dfd147b1f840f200707737e356bb +size 21253 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en.png index 0a39b8d1970..fd20864e912 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bacadedeeba30f2cbe00a85071ae5d8ea1e29dcbf6f4173b916d86cf5a7b0054 -size 21837 +oid sha256:d75ed3ea29e666aec482f6d22c49fae85a22b1957c8764cdb250ebd232ec6fef +size 21798 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en.png index 57c802e70fd..f5cd59819b4 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4149eaded704dad502586c1677d4ec8ad69741877a26d41e6b9c92e0ccaac938 -size 21209 +oid sha256:b4456064b641b3accbb6779a52cfba3cb2b9281755b7102ecdf66ac137b57049 +size 21135 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en.png index b36cb7af619..d6bc8455718 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a46dfd4eee3d73afdd565295fedb75e5ff9be6b382120dae867218d54d95a382 -size 19623 +oid sha256:a9e2f28fa40bb703cac5a8375e6bcba2a175de8195816bf74710a23435d5a4dd +size 19654 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en.png index 70c00ed6884..2112ce692e5 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:227c4abaff48d7c9eb6c5d3c6bef5eafd743a28a824cba0579ea3943e8bb4c04 -size 19326 +oid sha256:224eb389d42d4f504f027d853570dcde1049c0dcdc58dfcbc42a12587de36311 +size 19352 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en.png index 3a41810850a..40bb4ae7d21 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0252970cedc7df470d12e7458d2bf2428e2e843124696e644542d7ac409005aa -size 19454 +oid sha256:079aafb2a1a4f6ed0e99a09694d515c419f9fb4e736dd2dbe4d245b00769305e +size 19392 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en.png index 0d02eff7151..d700b3bbd4a 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e402c49b2434f244bf36b5f09b5a550e20280d18627b9624eca135d5288b6d4b -size 19093 +oid sha256:be12911fb0a0a129c5bd4b67c2130f244afca930bba4c882ef5953866b70b632 +size 19048 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en.png index 7519dc652ed..3f242b4a96f 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:758747be120863a6f2f88b257ee277c83d7445a41d71ed664a6d7920cfb011f3 -size 49252 +oid sha256:00b4b67348946d5568a47d435fb884984fcc53514a254c3e852f817f2a0c3d03 +size 49285 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en.png index 0322c5b34d0..9b9aa5a28ff 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d60ccff326a07c50e60e28f1158ea701226b92663befc25a68cac27e9e394be -size 47947 +oid sha256:8c4fa353f7b79f1e38b41aad2611a5264770cd56e986119b13112d6f5fe32421 +size 47968 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en.png index 374113b566c..d7db9cb1388 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d4d192e27e8f705f1049e63e6cd9759830526e43132502033fd198d9216346a -size 47244 +oid sha256:5b9e14dcdaac0e2250796a7049488b9b88282b3b3867ab6f442fe3c601c88d2d +size 47274 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en.png index 53fea58d96b..7418f3b4c44 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58b7377a27c180c531168e48819e903554f1d85525626ab4b6d79dab7f322c43 +oid sha256:ce9b952da4924907bc245c96166fe4c4e14babe91accf6678de30b6be68de8c4 size 45889 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Day_0_en.png index 2d0a6aa749b..c2e19a6265d 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37695b5ff4b53d50c0d2b278f9a41f961a98a2e9fed25ed239914963c3387314 -size 49142 +oid sha256:974d9d4a1af970fc64da8a9aa6dfec69b407fd4855eb643dd6b7a5c11b9d4be5 +size 49164 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Night_0_en.png index 11cfce1e3b3..dda4ae9673f 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f2e4dd94801ee28848ed04bf3c4846631359873e150e433a397c305b0a33dde -size 47887 +oid sha256:aeabb30b46e100487def9cedff50627d18a73371705dec42941db1bf9bfda5ec +size 47890 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en.png index 47722c0807d..0bc90a7a37b 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0db7953caa01f68e2e9b182b58cdd4d02bdf6912ddae64a36fec0636092575b6 -size 46825 +oid sha256:ed44c850f2c85d219c6fc10d9f180f49edd14f1c44a8010a8f192607284d8ce9 +size 46854 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en.png index 94e34de7a17..78d8d677b0e 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba9520116a974c22c709fec9b90f7053664a4938e6a96f3b21fe340717c33a54 -size 45692 +oid sha256:3e778a1a3590c849db350d2d801e600555fe46286d132833ae8243aa7cbaa70e +size 45696 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Day_0_en.png index 374113b566c..d7db9cb1388 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d4d192e27e8f705f1049e63e6cd9759830526e43132502033fd198d9216346a -size 47244 +oid sha256:5b9e14dcdaac0e2250796a7049488b9b88282b3b3867ab6f442fe3c601c88d2d +size 47274 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Night_0_en.png index 53fea58d96b..7418f3b4c44 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58b7377a27c180c531168e48819e903554f1d85525626ab4b6d79dab7f322c43 +oid sha256:ce9b952da4924907bc245c96166fe4c4e14babe91accf6678de30b6be68de8c4 size 45889 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en.png index b3845a0dff6..0fe8ad0b3f5 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87f388477415d917cbba86ad6c9a4439f75b129fd8586e81362f14a1d5270337 -size 44705 +oid sha256:28b14624d7237a5e4fc2fc1d8bd9515cf81d7cd6ab5dfbaa2685a6d6c11cc80d +size 44727 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en.png index dc2b993507c..6111183d266 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5fff6d8fc2f673d9a8de5f93dcabb84249c9db6badaff560f3ee2d74195981a -size 43239 +oid sha256:4fb88c95ef66f80a6646b894d2bf17bbd25136e2ee17abb91b9278c97635bb6f +size 43253 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Day_0_en.png index a2e90f7926a..f4121d22f8d 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73d64e6f6fd0f979c1929f3f1118d26890e55c301031dd3ca0f64647429dd42c -size 9358 +oid sha256:a17ec9b8d9e1a2825ec34ba4ad803d3220adb462fde29555fef9fe069a427d31 +size 9380 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Night_0_en.png index dac28412c29..10ce9acb1a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollTitleView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5b23e028c2df95e19ee96b0715b3da7f32ea15d7b50a6bd6068add0263c0c2a +oid sha256:ebf6ea8e1eb03c5449e9e787d97f5430a017fef821603b9be89e431bef0a5fa8 size 9081 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_0_en.png index 6e1625ccf9e..41d71b3acc4 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b85dc08c15c268ae04b6a89d630587a600d1f32cd2fffe72902ac1ab3f7d398f -size 32057 +oid sha256:272ed57bf6729689e7170fa732d2b567d3dbf6122d2f845ee5cc07dc0c2c5efd +size 32187 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_1_en.png index e2c022d9f55..e33cd8d8729 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1d455bd313f90002307e23823e14189bf53ebd0f0b1cfa4478514d2bb8ab7d0 -size 36440 +oid sha256:e7caba0f3107bc43779bf5be51530ef526fcd288f50ef14c0eea97223eed476a +size 36483 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_2_en.png index b3fa4a1e62f..73dd58c124f 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccff9b92e8cc76e5a5eb470ec16903911eabe26a9c42b4ba74a99fd81bfc8d88 -size 39237 +oid sha256:fc9278cc844fead8f441db3b131b57318ff917acad3fa5ae52ca1b42dcc6e4b7 +size 39203 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_3_en.png index 5ad1a9fa71f..8ac419c5e6e 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc85c3e76d57bc1b46a427ecee9ca64c238c419a346a43cd52d3a6e9f4199ed4 -size 43958 +oid sha256:f2af79b972de4a3d5416a10fc95664b22f75c5c55631303fe53db51483602b01 +size 44030 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_4_en.png index 1f5b915481f..d2fa6978588 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3399b98bd9f8d3ba2175125656db8871ba14ec98f80595d7be95bf8438a33219 -size 26803 +oid sha256:4eddac17671b040b5574db6d141b95a077c1718e84f7df051bd36f383baa3cb3 +size 26840 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_5_en.png index 011fcd4c770..4c5758a7c4a 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d59fb94b1c8430065b7e40b413d250e631511acfac88cb01c24cf63e3d354cc1 -size 117561 +oid sha256:d8ed9847c8bf92c70714d1a0caa8bafd85cbcb334a3070857c6367b6f3ea95b6 +size 117501 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_6_en.png index 6cae2b6c8b8..adf0b4c75b4 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23ccb6cb1ea3bc23c062760488e7ee2d6bb46a6f706702d11f910428b02a1b71 -size 32714 +oid sha256:2dbeea25d8031fc0c7a4a39338de3f4957f63211a6df2059e60b96693b27607d +size 32845 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_7_en.png index fe5a48ed57b..aeb6faaac6e 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc72e6d2bcd13e67155293b4d1312e75edc6f8a7143d820e4cb0c839f4fdfb34 -size 33753 +oid sha256:a94c3ce8ad498aab12af09733ed75b96f1bc45d478e782e9ff63a55f5d0cb4c0 +size 33838 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_0_en.png index 1a643f3ed95..cc0c4a30ba5 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f12091a5467d727b48926ab2182ae7def5f1a7820044613d0a15fbd4bbd622c -size 30976 +oid sha256:b5497458c993ca46eb67f2ced9484c904ee0ed36c6cb79d74675b30ac5068be3 +size 31171 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_1_en.png index 66ddd34ffd8..02cfa56bf4b 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:921d30810d6e1b6abfbd24bc7cef4b90916db0b41dccfd7de2298f81882fb5c7 +oid sha256:037637963e7e9751856a2ff7ca21f76caa4407b4aa40515cf39b1d6e27ad719a size 35224 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_2_en.png index e92a4d5832e..dec0a17f255 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31810b258665042df6c5475d03c1697b846bb63fea2ee5378d482977a909e99e -size 36611 +oid sha256:6b5335be7a39e3bccfe2b22ae0a29c56ad43050b17cd1a79ce8639512ec34aab +size 36604 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_3_en.png index 10261328620..39dd1434164 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe35cfe537168054a634faeb6c10519b2d13b694d532b152728df588c2f3649a -size 42636 +oid sha256:324acdad67b430fe4d32b3e6483b16ae46144790f679c564ef2724f48a2ec841 +size 42712 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_4_en.png index b3ea2e859a6..30ecbf7dd69 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58a98bf2a38cb65d90f8e6181b70b673078830182d9f506598df5bda75cfdfd7 -size 26089 +oid sha256:d3acde55065227df4ca120e6990b3c559091136ec78e8853b7fdea37ed83b9f9 +size 26127 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_5_en.png index 8034eed4c29..ba847181c19 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4af7eec8cb1d89c71a9b2fe1efb28dbb956bac465e215ba7414432d7a37c272f -size 113942 +oid sha256:daf33ec8b9f6abc71e8601db8089a6a279bd7dae57ed27d07131bebd6322ac85 +size 113870 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_6_en.png index fa4924465e7..8fe83aed979 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e66b8b86ecd2f3febc7121384357db22a0d0cd5ca5b6155bb2c02dc9fc43180 -size 31627 +oid sha256:686717452de4f4972addb3825a6418c277ccb3abfcf726b63bdc482f7e1d041e +size 31821 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_7_en.png index f744930b7de..636cac585f1 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60f6dbfe226ffb43913052ba9c061733ab1690997425bd18ea3117d88983df92 -size 31132 +oid sha256:4156a74501e43888a005a7b60c5902d12ab61595633a131ef168449f44b57135 +size 31248 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_0_en.png index ac5893ce75d..b03c95c661c 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da5c63531826c6f691b5d305cfef633421d6a8ca1dd112876c0e731e7989f20b -size 57145 +oid sha256:7f22dc77b9ce968a369254ed0e901790e6b921f5e4cd2a0b32d45500ad29d51e +size 57086 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_1_en.png index 533591f3d0f..bf2df79279d 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9946e1797e8470c7ad2525d64c696231b9a2eff203a92da77f395ebf47265b98 -size 60975 +oid sha256:c88afe41f0ce8e26eb7c4c8b46b6f31fea6a16eb211b0119e6445064780d3787 +size 60913 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_0_en.png index cd0cb25645f..3e71995267a 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8796b64a1159a5dab48640eb431a81324e3cfbe3ec16530e3e011ef3d1e66297 -size 55447 +oid sha256:057910d2a37c535b0f70929d6440150e0c2ca04978e61cbcf0ae160c364bdc48 +size 55377 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_1_en.png index ccdf930f438..accd1dffcd3 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33a2ce240c280320741fab065d97bf5d04f70143c0c3b0515711685258f75ea9 -size 59221 +oid sha256:0fb361c628d6c279bef5841befb5ce860d1be7b1776465ffac5e0151ede10e90 +size 59162 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 new file mode 100644 index 00000000000..6dcb4a0233a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:733b71c4199d1a5908b5e76d8e44f5781cee39e97d57a049be99f9e66c3d30e1 +size 46936 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 new file mode 100644 index 00000000000..a7fb1c20dbc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef41e0df758991e44e9abcf3d7d511c7cc932711db58e94ad1b4617fe45c60c3 +size 46826 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 new file mode 100644 index 00000000000..45ab8e10044 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de45164ae8112206c1cf1a67c284cdea1509f4fa070f963fddf6a87eb9183ee1 +size 46813 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 new file mode 100644 index 00000000000..29477d75f15 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c3c1d2b2fc9e5a16a1bc57551238c5190f25a873575eaf3731c36aaf19d7f00 +size 46802 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 new file mode 100644 index 00000000000..a8639cc5a53 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1759e7063862ec1de7ce2b894067478599afb7c87b183e904c8d0199c93ed546 +size 46646 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 new file mode 100644 index 00000000000..9d24cf3869b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:420e10e0eb2c90d38e28262152dd2dee54cfb73f7b031b7a34acea17a8f784fa +size 46943 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 new file mode 100644 index 00000000000..38100458c32 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e639d6e0f93e15f3ea39a0e0d9561a8da485abcc97a92b7370d33f2b701c40bb +size 46602 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 new file mode 100644 index 00000000000..310b4fc6cb5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:264e12f555c641960f0fef0c1177715fa5d596f7a42e0796d49533b68cfc88b9 +size 46256 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 new file mode 100644 index 00000000000..d526c73fb5c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7faf10e196f9275fdafabb53cb354532b2c5aa73869d1fe48ac67b53bbb34531 +size 48681 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 new file mode 100644 index 00000000000..b5d33d09619 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15b0cf42d99a96ef4b52349301e4e90a5478150396a6890a66df1d33c3b6b89d +size 48558 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 new file mode 100644 index 00000000000..a5808a088fd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7debb36ffa6a60d7d30f9d59e247e6091c455860bdaae67502a52a5cec5e9c9 +size 48572 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 new file mode 100644 index 00000000000..522326aa213 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:106251ed2fb0a330fa98d4dabd8aebcb946fdbd098b18372c8a9c3b8df39086f +size 48554 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 new file mode 100644 index 00000000000..85e99637357 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9b8cb284cc78c03f4bd4f4364d898e656a07ebe228243fdded35c283a1205e0 +size 48492 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 new file mode 100644 index 00000000000..8e06f2bcad2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddb21e08e8047aada11330634ad9c137cdb8a3a349f457c066243394fcc23924 +size 48682 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 new file mode 100644 index 00000000000..3d55d29e233 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b797e153358ac8eb1e226ad537c23c61f4dd97ab8d5947b98bdd9b193f369b7 +size 48487 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 new file mode 100644 index 00000000000..da0c0430289 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3853ed9a82dfa5bdf5f0e4d79e7af04bc7e43531e691c4d5fb17a37f5bd4f5b4 +size 48249 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png deleted file mode 100644 index 87c4502f02f..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:938a3cd34ae844f67d6a3a1ecb51011b81f344496a4ab149ef975d38cec5c951 -size 54856 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png deleted file mode 100644 index 744255f3210..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce83905d9e3d5c85988992c2b38903ba71d9d13073986653b44115ee158d3bbc -size 54617 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png deleted file mode 100644 index e490c072c82..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0aa44a1ad39634129163b474ce8f61ca9978cc5c73f40af300573aa335787756 -size 31918 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png deleted file mode 100644 index 1c21d7d85bb..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3aabfa3d5bb5d590d076954d19584f5be87df7e27ba1ea644e7482c3e867dfc -size 54682 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png deleted file mode 100644 index f10a07c763f..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:792e82c77d4d1955bd956f1217290c3c2fecd3542f522c38f8315abaee9a1743 -size 54652 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png deleted file mode 100644 index 1d408c87581..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7a59fe77ba9a559798dffd6ae44be8e189888ceb971c4ac1cacb7e46847fcd04 -size 53590 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png deleted file mode 100644 index 7ac7b2180db..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2ab6f2174549f91c472b9bc464414903092a0c9618090a424b182735c32375b3 -size 53298 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png deleted file mode 100644 index 476cbc310bc..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f983ac4181f10e86b52754630eb5c643502a0ce5acec09c0183f6e8bef78deea -size 29553 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png deleted file mode 100644 index 05d298960c7..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:860908712015ef624d27b764d00c0e375ded901dc62f8f643bf85712488b08e3 -size 53344 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png deleted file mode 100644 index cfd28f20722..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4a714c9213faeb54c27e649ea854a42932a17cc181ee7cbd4788da987acd2c86 -size 53308 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png index 5a6c4983009..9381505a002 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1cb2f64719f8abda45e50b9cca1a2985630736c35e9cdb32b207696ec067edf -size 57683 +oid sha256:7836dc63d1181d2b057df6baf5fe3b8a05298aaa4e23a75a79790cafff1fd451 +size 54048 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png index 5a6c4983009..9381505a002 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1cb2f64719f8abda45e50b9cca1a2985630736c35e9cdb32b207696ec067edf -size 57683 +oid sha256:7836dc63d1181d2b057df6baf5fe3b8a05298aaa4e23a75a79790cafff1fd451 +size 54048 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png index c9c81afac2b..48046221dce 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40cdf166be2b920b5e6282835fa80654a734ad19bbefdc2c37d287447cb3b49a -size 56274 +oid sha256:cad88329f2179428e72e96499d91ebbfba0647c74fb271bbfffaea0f695d28fb +size 52595 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png index 50fb79702e2..c49b1ace09f 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8ff541c779c046143e109f80fe45c5742868b1b4b9d4d5a96371b336e2f1c56 -size 55837 +oid sha256:5861efb5c4453365fa215ec675e8c1289d86fa7756b42c57dcd9c7f138cd62bb +size 52067 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png index 50fb79702e2..c49b1ace09f 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8ff541c779c046143e109f80fe45c5742868b1b4b9d4d5a96371b336e2f1c56 -size 55837 +oid sha256:5861efb5c4453365fa215ec675e8c1289d86fa7756b42c57dcd9c7f138cd62bb +size 52067 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png index 9f66038e62b..67d26e583e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d18ec536792e9e3cea6a40e8a2901e131631be7795f92f64726b8b91e8c52e34 -size 54433 +oid sha256:af268c15499c557a3c6502f6daf732ede68f7fca1539a10a420809ca0fb212a3 +size 50664 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_10_en.png index c410722f8ee..3c0f9ab6ba4 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16d1a40f1236f2b0070c05851cfb5f1e73e420361d62aafb8ac5776e20b94db4 -size 42127 +oid sha256:f96978e1ea1f0ccf62d977e5d41e7036523304b6b42b27b82281756fb5546b39 +size 45057 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_11_en.png index 441eca91055..c410722f8ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a56d884dacc6f73de25255dc4c0960e15968a6b9b89cfab858571d5f6c5a16e6 -size 59133 +oid sha256:16d1a40f1236f2b0070c05851cfb5f1e73e420361d62aafb8ac5776e20b94db4 +size 42127 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png index 3d8dee84302..8bc1cbaf705 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7317fdf96ff21fe516286aca2720c89390eab57c15336a4fa82afd5473d29ec -size 15296 +oid sha256:87c022f59c9a89829bea26a112445f829dc65025279a90f20e76ee884d23f072 +size 59131 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_13_en.png new file mode 100644 index 00000000000..3d8dee84302 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7317fdf96ff21fe516286aca2720c89390eab57c15336a4fa82afd5473d29ec +size 15296 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png index 7374e09542d..bdb44e43f54 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afafd3155ea2199c3e0d566797337a1bb5c88ffdf8beac3274d91829ef91d44d -size 43807 +oid sha256:796869e88b376df6e40b6ee51d471cbb9ba19f308a5353a818eb182b6d7fbe47 +size 44214 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png index 3c0f9ab6ba4..7374e09542d 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f96978e1ea1f0ccf62d977e5d41e7036523304b6b42b27b82281756fb5546b39 -size 45057 +oid sha256:afafd3155ea2199c3e0d566797337a1bb5c88ffdf8beac3274d91829ef91d44d +size 43807 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_10_en.png index 5c76e11e081..43ceee5310a 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8dd44e2127936e2e01d99933a2f18e50ca760f614f6a3e1c3162e3387d4931ee -size 39759 +oid sha256:4f22b7825bfa438f55514a3d09696de34450c16f9d4481c4892be9c8dcb45191 +size 42486 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_11_en.png index 121db75d13d..5c76e11e081 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:399efdebf67b1c042dc105d6538ea4fb622314c883fd6d3899eebcedd3ff357e -size 57540 +oid sha256:8dd44e2127936e2e01d99933a2f18e50ca760f614f6a3e1c3162e3387d4931ee +size 39759 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png index 8e428f123f4..9417e25f90e 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c484008012b2d569c6d313823b4ea8c96521fb6a172d37ee1db11f6794dce62 -size 14806 +oid sha256:68eb8dbf6e7378bb088dd0063952b42fbf067e798ccf040fb4dc25e96f26cf40 +size 57537 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_13_en.png new file mode 100644 index 00000000000..8e428f123f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c484008012b2d569c6d313823b4ea8c96521fb6a172d37ee1db11f6794dce62 +size 14806 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png index 5d2b2341f45..bc098d16617 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ddfa90c2eaf6b493ebc9c2c5164888716918c7b453c49d7653a0cd4dd69af81 -size 42092 +oid sha256:bc77d365ea38cd50d5fac90efb15a610b52a069cbb799b8bf0d9924ea650ab79 +size 41408 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png index 43ceee5310a..5d2b2341f45 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f22b7825bfa438f55514a3d09696de34450c16f9d4481c4892be9c8dcb45191 -size 42486 +oid sha256:8ddfa90c2eaf6b493ebc9c2c5164888716918c7b453c49d7653a0cd4dd69af81 +size 42092 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 c13d3cb3ba5..3b932068cbf 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:4a6a54efb3bb7eeb97cfa06995a199eb83a7e630fb1b631d7992b464e0b04d20 -size 37954 +oid sha256:6a47cd94f3b8fcc211bdaea97559b6ca10af2b163a1c771a5a552bfd874000ea +size 38063 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 ef6e64aa892..5a14dfd78a7 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:d35962150a8dc5b7d774af160a994182afbe2d5538e204b2545c02287897cd99 -size 37711 +oid sha256:b466c2d1be208efbf340eaa6aedc2526d9c8403f7bebc53a00e32eac54c87411 +size 37804 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 4e534c90418..2e826bd1f39 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:a3711fb587d635b28a398bb5b475bee16d4abdbf691d494348fe11e270e0624d -size 38774 +oid sha256:b3aced48dd05c47fc20dd2394c49b3f2427992dcb425d1a7f32f45ca42d1bd3b +size 38907 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 8d27190ccb0..be4e78959af 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:41b91c8a1805bb24453c6f85091704e609a01f83a86ca17979d97de4c20e4cbe -size 38800 +oid sha256:37f855f15b734c7f756c860c7c4cb944ba62447edd5fe5bbe5683424ae55093c +size 38907 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en.png new file mode 100644 index 00000000000..cd12b08a008 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69fdae3edcd02635868f209a2c1ff21ce807ffb4c6d44dcf89fb0f54e1b9c1f3 +size 68396 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en.png new file mode 100644 index 00000000000..5d291def2f0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6da7fb8d713d5d8255593f2007edf068870dbd05225fe2c0562d197a1739b876 +size 67313 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Day_2_en.png index 139444ef1d3..4f17fe29035 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d964de0d0cc6b2210690c99f77bfd70d41173f71893a2c072565101a84982583 -size 66203 +oid sha256:dcdda50ce353819cfcd87063e2b7232d9b6da727f2d4cabb59868ac70037c5d3 +size 66381 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Day_4_en.png index 05e252015c4..879a8e10c79 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65a1d68b5cf2dc6ef6278f6bc2f420df0e53975f6e00492c924620c52e168dc9 -size 49507 +oid sha256:17e2ce7912116302ebfaf207172d40c28aa1bd38400c35aaa2738372c8032185 +size 49508 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Night_2_en.png index d9d0bab7bc0..58f7bfe9b01 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de1bc2159d3c9fe8ddfa2b1c078b66a40a839e45f9c33f8744aeaec58f5a238e -size 63800 +oid sha256:ee6b493921182340691ef43e5f4a8783831fb3bcd0daf4ca565458e46c8c914c +size 63884 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Night_4_en.png index 0c38b343ade..462111649b2 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0e182aba02a600b3be8d252efb3c3dbd31c9c607c8728d4bf6319b783d2d790 -size 46245 +oid sha256:4b21ccf4150c3b91ee3b3cb07e010438a978b7c8138fba56343a0b14208b5652 +size 46250 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_0_en.png new file mode 100644 index 00000000000..c0c23f4b396 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93b26b97920577fb83bd01e31e2fe9b03de7eafd02edf3946fc59d7d5ae847ba +size 30476 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_1_en.png new file mode 100644 index 00000000000..7b035db7764 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1c42f96f448cde729b0a28ff068439b629b53e64f29a2f722cbba752c2864f2 +size 29951 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_2_en.png new file mode 100644 index 00000000000..f564ba51ac5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3add6d1c206f509459fd64d717b6f1457863e333f2c0181982ac27f47f1b6b22 +size 30129 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_3_en.png new file mode 100644 index 00000000000..ac89c1f2906 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2f1c39e1e23e98d0f80ea492d68993c1d4ff7cabfa5ebcd96414742f2844752 +size 27434 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_4_en.png new file mode 100644 index 00000000000..1c10a34c535 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74403b531a8c2f563e6cf08f0de06392b6c608add907d4d7fee03d01bccb1f7d +size 33236 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_0_en.png new file mode 100644 index 00000000000..af97e0e7fcf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3eb1d959794baf1a25ffc0cef77a496c627cdb32879984b5c7e18692f0b072b +size 29618 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_1_en.png new file mode 100644 index 00000000000..282ea14c1b7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d7f1374c8b8e4ae2a045bcd7966378183de1a2b5830cd7cb82ef11bc44a6d1f +size 29175 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_2_en.png new file mode 100644 index 00000000000..6349b50c224 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de1a5b992070e12ecda9f9d26c8460898a1e2b19bab604d34cd45042b561fbaf +size 29171 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_3_en.png new file mode 100644 index 00000000000..f86157cd286 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbcd3623d21cd587912cd2444ef0f04ff6fd35bec811f4bc794654f7809584eb +size 25681 diff --git a/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_4_en.png new file mode 100644 index 00000000000..ce98d696ea0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.reportroom.impl_ReportRoomView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1230423b1bed62d800d545855b59d48df3083741d821c5caf1e1fc221b561fc3 +size 30788 diff --git a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_0_en.png index 8ea6e41b0d7..6bc15b8ef52 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad8e8e1de0e7c4b39760a7ad1336a520b87b2afec6c6919878345c9c369a9cd2 -size 97788 +oid sha256:14475290253a3a5adffc38bf1358d6398aee90b512dd9e444af88c71700a108a +size 8513 diff --git a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en.png index a636de0ff92..fdc89929c12 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a25327e6d4bd3075391e6786ada6dfee82de05e6941cc3eb840438b29cc21bd8 -size 99409 +oid sha256:18076ab6a11a67d3a89f0cbdccab96a69f40acd3c473fad28681a5d1901799bc +size 26607 diff --git a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en.png index 71266e3b18a..41288de9659 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d80e95603b9172d239d3f1bce0a9aa579728b95002ff6b0f5e2f47495aeb1b51 -size 97433 +oid sha256:8ed1e4fc0de314df634235c21afcc6050fed9c3c473b1f2fedf365ce9e07bc38 +size 23436 diff --git a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_0_en.png index a26c9f04fe3..cb4c370495d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e61f1e76d573f850f3699c893e884a48ca3e9ff79e061c7723c5a66074180e5 -size 82773 +oid sha256:ea910fb367e278a2a88c99f048639e84629564e4e9186d631aafa49920acdeca +size 8353 diff --git a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en.png index b0dd49b0eb8..fe70c0a848b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fec53729278f19bc00c336919a71daa1ee64a3061191d3cb52a2929d7bef1c11 -size 84440 +oid sha256:25e8c52cd120b0fde6c15f7a31e35f6952f9e1fd5689b741fe029a91fef2cf33 +size 24775 diff --git a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en.png index 06afd056e3a..7769e911c1e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8913b014d8f07a02b532e7a6d50270363f60c9f3cb59963ffa0c809a29eb4523 -size 83105 +oid sha256:51e05f6ead16d567da4c6b4f7e302696bd47e65705bfa0a3be16dc44ae15284b +size 21464 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en.png index ecd64c14636..b8802ba76dd 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a7470dbb61f069a133fbfd0de8eb1909f7c71d88cc26e42358d91c9cdde456f -size 13891 +oid sha256:dd65bc84aa624b3af6b055f07cac49b5779ac643602176be052770a85d1bf041 +size 13703 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en.png index 30d890117a4..cc04b1b6821 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a44efc28c8872981747255f8538e6bb60a804426e85d372a3b3bc17ad73937b -size 24688 +oid sha256:013e0f812dfa9dbb8114e06a502f244a02f5301e445ce39c9ab0d059c52f8af7 +size 24541 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en.png index 87e561ebdb4..e45a56abe0d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:130f2892cfebfc0b679669e8eab88706aa7fa35bf9e24d88c62448625aaf0904 +oid sha256:96292b1a98c2d9a7f034cbde25d03a4c1ee0debf6e8a7b5114eccf0d6c7b68bb size 11125 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en.png index 06c93027e38..8b053874031 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d58405ce69b7823d7f5c2da021b3f9cffc39335e1beabe543452e606179f9a8b -size 22409 +oid sha256:73cf6be0bd55e6c83888708176af33b9975fbe0721c4cce86799c547b9118593 +size 22408 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en.png index 47362fb9c9c..67a16d35b50 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e339b7d5286e4793b781a6edf8e7b1059d0b90d3998314057eaf0b40d9a7149 +oid sha256:be4cf1bfbe051f838a3469309bc2bbf29d7c3b07be92d52c7b77f94268a02c3c size 13060 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en.png index 117efe39505..a54a62e2ae7 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53b649b6aa79b687aa1a57d564e0fb7c2786dc62e5854a8cbba1e8a60704008c +oid sha256:2bd2bfdc6c1c597d6f9963eb657ab66f0d9c0ea42dc7d48cbb93d21b2933a8b0 size 42451 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en.png index d53f6a1ee4a..d88d38f9767 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53ad1edda2a1b0f3355f471a87a92ed9e4490af30a7f927d6b88d1bf54e8e4dd -size 37509 +oid sha256:a64c0a77ae970769a9ff0c1592fa291ebae3601eeb2adcf1059abf1fa8710f3c +size 37508 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en.png index 3d003192451..06f00382809 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dff355ad91d6e7d81dbd3a11822e57b28aca8dd87db79ad76e18d2295f9b7a7d -size 29660 +oid sha256:ac7b1872526ac55a505e3fa4ebf90922a56fb0bd9d4311dcd1077927935911d3 +size 29655 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en.png index 727decb1b67..22cd26e4061 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb74a50f50a99ef9142cde77604a2b9a80b466fc0b9079b4e2591cf7c3896165 -size 13046 +oid sha256:a28b2dd77b83951727f520bba1f551d271c0ae89217453614bd0ca652b41d762 +size 12919 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en.png index a118a76eac1..ed2c359eb06 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3eadf6c716c78a8913f36fa003e19ab863ec3413741e91212bad67b876aad9e1 -size 24906 +oid sha256:e2be3a7769ed5df69427b33025db1097ce9dd7ccf58fa1ca3d4eb7997543f824 +size 24812 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en.png index 80e14e8129a..b47b4250171 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c18aa05e130d7e5d2d33ea3eb78a096656dd8625719a2f16bb39406c5319ee76 +oid sha256:ff9a4ca8777f52c24fe86979790c7f2fedf8becce0c0a5c6d896bfef926a33f4 size 10745 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en.png index 7813ea21b82..38e0cf9d7ad 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f886766fdefc45c8738658d3851b973bc12f703576dd213c717e3990a7fedc9c -size 22832 +oid sha256:f3ea5702dbc2487125dd3ce6c382a10cd67dff580240cc81f510a4bc7d06bfbb +size 22833 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en.png index ec5bce7d580..54824070cf5 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93deeff0e991b3384f582e65e5729856b7bed4195a5a0329535c616bc5c15b9f -size 12665 +oid sha256:3ca09d0870d394875ec86e3e9b1077e64408cc86310ff50dc8943ff31762d9b8 +size 12666 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en.png index bdcd76b709f..4070ccfd7f1 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e815198898cfd091260650f301121e26a70d3943abc4ef2cc0bc95394395c2e2 +oid sha256:693bc737b9fcc36a745cb816bfbedfd1fc69793bbd45a1bbd8a4fcf4ca9080cf size 43080 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en.png index 0aa06f43bd6..e13d5c0981c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0da7a01de4fd1623c4acfd771f74e4a01b38e8227dffbe2c0076c56d617c0916 -size 37447 +oid sha256:89a681f309feb86724956b546a81c3fd2f8eaaebe4feed2b5a9ac64b2225ac4b +size 37446 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en.png index 87ec7712b68..dbfeb1c0e9b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec72db9bd920c237d4919fa9c09245efe02019a789d4c3c7cf1c9d3a9a76aa5b -size 29028 +oid sha256:7e536a9f3a30c6fc440073cc5f05f4af55ce2d3e68f00295cd8a7d1302c8ca17 +size 29029 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en.png deleted file mode 100644 index e7cbc171c59..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:055c81bf67eade1f203cf3e02a2b5096c89f73918b3f9123f0926d9af7a4175c -size 16744 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en.png deleted file mode 100644 index d9479c2777f..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4cf5881fe38c4d666a7c61d64d58ecc22fb82e50931857a79370c14301af444 -size 21293 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en.png deleted file mode 100644 index 0ad86e26839..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a61566b38636c7f66d4443ddf42330b2123eb4dbe5829df561e18c85a13dc59 -size 26245 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en.png deleted file mode 100644 index 6f43e3db481..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f83d60c7e1d963ccef24668c219bd9df36494dc8bd8f57af7aa63a3b73ee6882 -size 7545 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en.png deleted file mode 100644 index 34ded1ba23b..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:16685ff4d5d52c5cb15f87fd14178cf791a85f3295b9869f997f4dac9fda1373 -size 25819 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en.png deleted file mode 100644 index d220520bfc7..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b323796ccdf27e5c445aa02e3f7b73f65df961dd88f3ee480efcba14d8038281 -size 19426 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en.png deleted file mode 100644 index 7463908c410..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0b7570c4dc464557985e06a3d90fac595e5363f53aea02cce86dab76a58863d3 -size 15612 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en.png deleted file mode 100644 index 81b4f49cb3e..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:822fe5f5e803509db19834d72fe7d1960cf0c8e0d53255f1b25bdae590277fd3 -size 19991 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en.png deleted file mode 100644 index dac29e61550..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae41bb5c48e710ad31e37c78a1db6c928028ac52ac2d0a8e4574ebc75497f5b3 -size 24743 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en.png deleted file mode 100644 index 08349f1edb8..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba213b12d181b04695b9271670cb7d96ce83dfccc3c2031ee002ba1cf180cc8d -size 6408 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en.png deleted file mode 100644 index 1e509dbffd1..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dfb80116e111dcc2fff42ea4a6c26e2857eecfbfcdb6a43d09aaaaf63a990fca -size 24019 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en.png deleted file mode 100644 index 51ce6077b57..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f9def02bcb5784681dc26327791510a9a25f0772644cdae363d7a4b90e69067c -size 17479 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en.png index bdaefd7b5f1..d180111d1c1 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57c93d6c2373777214b59717831ae6d4198eb50d904c6f22332c631b7025e259 -size 32736 +oid sha256:b0652d08a2c59e59853b3ee0625a2cee62cf68e8542de16f26e7174f18af620e +size 11465 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en.png index 0c9549e7ec1..9627b16e6ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:726bf0c0beae74cfecd3c813335de763687642baf87c77d81d93ed9c9e536f0a -size 33028 +oid sha256:2350a560fcc78a3d1b22cd235746ac03b46eee2912184dbbe62683540dfee2b4 +size 11704 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en.png index 4fc5b7cc03a..d180111d1c1 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ae24aafcfa7302be6403c1f3a9df6d996b7800c77ab0a011333e77ea7f277f7 -size 22231 +oid sha256:b0652d08a2c59e59853b3ee0625a2cee62cf68e8542de16f26e7174f18af620e +size 11465 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en.png index 1fdf12a4c2c..70b26f1264e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a54b853c55e0fc1fa3cb9f037b02dad50ed50c9897ca2caa03dedfab34051a9 -size 31965 +oid sha256:4a64ec55a66dac97601880e5211153c5a25985fe0184186ab3c8d8dbebd9973d +size 10834 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en.png index c05799f8187..46b47ed0157 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d71db0947e09666739ca5e3c01a7c6b711a430d2a40e00cb55cd474cb42535c9 -size 32417 +oid sha256:9bee4a7a63485b54555a203b8e4fc557e504b3efacb91d1c7c082647848c8c45 +size 11051 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en.png index 5e90a88365a..70b26f1264e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1df67dd253e71fe912c82fb3d7d5d6e4be3efd2dd510cdbed410671c5272ddeb -size 21072 +oid sha256:4a64ec55a66dac97601880e5211153c5a25985fe0184186ab3c8d8dbebd9973d +size 10834 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_0_en.png index 9e8807670a1..4ecdbcf85d6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf7912cd69d2ad680d885462e19544ee56452bd5d538a8019a39199dbf93f124 -size 44374 +oid sha256:2fb84f6f240041ba3af8b90e32b308f68e715c813362f2cb9143fa745a0e1b32 +size 44214 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_1_en.png index 64ecba6ddec..0d245bca191 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f01dff35fb317a29f898879c6a7bda6b23ecc2daaaebe80abfc089b448f99a1 -size 11891 +oid sha256:dc12de5ab3944ab1e5ac3d352bbac286d44ed4a5e67cd040309a7a32b1cbd9d1 +size 45438 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_2_en.png index 64c0627f63c..9627b16e6ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5197977035f27da4e4fb4d732ac52006d72b517261567d2efd6738a39270f22 -size 12849 +oid sha256:2350a560fcc78a3d1b22cd235746ac03b46eee2912184dbbe62683540dfee2b4 +size 11704 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_3_en.png index 64ecba6ddec..507709b1b46 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f01dff35fb317a29f898879c6a7bda6b23ecc2daaaebe80abfc089b448f99a1 -size 11891 +oid sha256:b730a39ec0ac14e8557e738dd13847b5bb1291713b0d013cf50563a1e0904f7c +size 12660 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_4_en.png index 9cbd2e472bb..9627b16e6ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adb68fcd10b66e37f774560e59991bf6488043d4e1b66e6b1f19a648c90a6333 -size 7572 +oid sha256:2350a560fcc78a3d1b22cd235746ac03b46eee2912184dbbe62683540dfee2b4 +size 11704 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_5_en.png index c10cc18a378..92d9db2f11a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72dbe69b1952d3c2e00509bb1a5e0493baa30c0ef4e444f652f3fd7f81a8eccd -size 6528 +oid sha256:d175ae95fed4ac24e9b506678c4ee1e235c3c8405915498f7f97db0764e5470c +size 7571 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_6_en.png index 06aa5f723f4..fdb10220a2c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c2c9f8dc95c5ccf4836621ea4546fdad30091495b00d8207db8aeed9d0f5a1f -size 24543 +oid sha256:0db80941c8c981d3f66bc6cd9364f0f8fd5b1e7033f81da46cdffe478f97c748 +size 6528 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_7_en.png index 86a85de950c..942808435a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:878d81472018964e1217c36708e31469a62486e1a913da26fc5a209731c4f9a1 -size 11014 +oid sha256:ddb463eff5c3174663e6543726671708cef04bb11c9194b20373435e1441323a +size 24543 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_8_en.png index 3dafa23bb0e..3c8281a03c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd04abc2eec057ef2dbd02e06c3f0ecd8e66fed3f760f293af4bb86c91e031f6 -size 18284 +oid sha256:57ee28833b587ec86f93c685d716f0cce94b60e0752de3fc1a1a3d7ac05cbdb6 +size 11013 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_9_en.png new file mode 100644 index 00000000000..abe89bd9a25 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcc0479100b1fba092a109696b548da7a1da26583304ab7273bd454def8046b2 +size 18091 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_0_en.png index 17d7af8e536..bfa9afbfe9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b15475fbe36b0255b21a7492195e516dbf24298f6d3e40e164ba73897da24b10 -size 44429 +oid sha256:56e080464834b1d5e9cd8357e57cfaa78e38718b1d71e36310453d14af460e91 +size 44317 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_1_en.png index fddbe32336f..43665210e44 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a0aedac7a6fb0ed7d178e98bbe546a7176c111953f8d02c107e92d4fbdb6a9b -size 11179 +oid sha256:d24fb38f4cf8e0c8194abbbe1feca02b9515f4fe12db03f05730c2aab2a0c1e9 +size 45433 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_2_en.png index 7073c1fb272..46b47ed0157 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6138ec110a891f52f492d3377825708a7151d42d1f391a996ddc10a7f2624806 -size 12065 +oid sha256:9bee4a7a63485b54555a203b8e4fc557e504b3efacb91d1c7c082647848c8c45 +size 11051 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_3_en.png index fddbe32336f..dddb67bec90 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a0aedac7a6fb0ed7d178e98bbe546a7176c111953f8d02c107e92d4fbdb6a9b -size 11179 +oid sha256:871c608b761d8df820122d9d245ac037f71d227e7bc23f7fbe3402662ff05536 +size 11938 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_4_en.png index f0f400e37ae..46b47ed0157 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8638e7c07500bddc9c67480685d2548f0b50772977fee7565fb92ae72eaefcb -size 7504 +oid sha256:9bee4a7a63485b54555a203b8e4fc557e504b3efacb91d1c7c082647848c8c45 +size 11051 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_5_en.png index acb40617137..0fbe95ff702 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:000dbaa58af2f75293be0a3fed993447275582abaa33088b3f9c533ac4e0fec6 -size 6298 +oid sha256:d174f1f8e77c9cd02506b487e410a7bcbf4d571f048f739d4f10582c72e682d0 +size 7501 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_6_en.png index ab59f1d6292..228b02c0a9c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfcb60ce2e343530c5344d9dfb70a6a562fe2fae942054afa033e9052de998fa -size 24485 +oid sha256:000c36468af4793e4956a4a2a2be1867afefa13140219e2dd8b17b1f7670b6de +size 6299 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_7_en.png index 605d9d0907a..3473f077b51 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b81f55a04ecf3d4c3f266d5e0b67413be226933816195307e830d081a2bbc286 -size 10637 +oid sha256:a6bb04b4dc4dfb43adb8ae45670f20232e8a3ba50c409c50a86feb96dfb00d56 +size 24486 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_8_en.png index 516705b7ee8..39750c36e61 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93cd01b7fe046a90b49a20311941bb3a07e8d2bd71eb9335343f2c15713ce73e -size 17222 +oid sha256:5694d2ee8d939aa3cc5107b7601ff251481fc8226981208b603507d407dcb12a +size 10637 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_9_en.png new file mode 100644 index 00000000000..c01e5b8a0ad --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4b01a055e3e8f64b18d045717ee2d6c9795ef1cf2a923c0fe3c5ac26f6e9a62 +size 17091 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en.png index 4b06082200c..4d26116fc53 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f87139a0e17dbfb4d56bfcc41dc49924a0d185c356af5a5daaf60ff2465841d7 -size 29739 +oid sha256:2f23c7910852f16382a5186a085a06014a61e5297f259a20a450947ff11405f1 +size 29683 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en.png index 0c484052b18..3c9e3f4e092 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6754f38f6f193e2bd63758c0f184939e02f2fc0e3742669eb00ec6103c2a752e -size 28724 +oid sha256:9fe608e191fa311bdd26cf5e90eb55d25b79c55b536b26ed0a5d2e34343554b6 +size 28664 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en.png index c537936bbb0..197660c6c54 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29d32752f291cf258bfe69adb5c5968f776c39584fc8fdb496ecf81d3e78415e -size 22765 +oid sha256:446392e5f5f87e53ea6647472bb1f50c59301b919978fe97f0aeb2b02a06662f +size 22576 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en.png index dde921b1e10..d6ef2086059 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57e243970cb239da471ff3962429c72d50a11478fa49325b1ae4e994dfc8ded6 -size 22078 +oid sha256:19725ce0f5a535af906cd8db38e299ce34e6d11408217ecaf666870f9dadc79a +size 21874 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en.png index 333cfc97ce6..3ece1582c64 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c8157834cb3fb5b9ab3d525e0a77c804bda007b72772caeb34be53a62095c6c -size 13539 +oid sha256:121f177c3f1d0318a81507e799af7a47d2ad88639165a5a5bcdc960e99aff201 +size 13359 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en.png index 2d0248d8c26..f0d82f9d092 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:984a56a04997ae8461dc43e8b7d6abb68016838264815aa5b91c1f8f1e3a9faa -size 49952 +oid sha256:e5c71623f182b0e2aac51c276d3b61f039997c3dadf6c7bd9de6c4f2dfa352b8 +size 49837 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en.png index c2637ff6c97..9c9ce4e8034 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5375d20c8ae75f0c79ec5ff836eea360ed3af4ada1cd5f30db7bfabc3d696667 -size 66628 +oid sha256:326e774bb0420b4d55cca0014bcb8aac69f2351e58e756c7d63de3905f5bb6d0 +size 66470 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en.png index 7235f790ec7..39c11d95e78 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0d492629413bd4ff619cf46c291cb4597ededc825bfa9ddac09171b2a954d8e -size 60909 +oid sha256:ba30b7ffc1096fd5190fbf7e1881217dce20a5e0865800d49b26d82fd0aac0dd +size 60733 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en.png index 3721c262b71..402673d64c6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:594a74012b30e9f1dafe04bf8bdbfe4a9db301920be692f0c6d5b151638ea568 -size 60847 +oid sha256:0fa5719b287ebb77a79900ffdec8793d9a86f2a07920f32c50924872609b09d5 +size 60682 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en.png index ed65ead3134..35975f0208a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d083a4774130153cce24a64c1ea4bd3b9cbb918d486de0973eb62479f1e2612 -size 55770 +oid sha256:46cd3b9a7be00e4f41fbaab11e1cb6aa6528d6db578a84512f7abedf5d4b5492 +size 55610 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en.png index 8cf65f96b1a..85bd84bb102 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d40487761c37e8ffd1e9ab268f27c15db6b9f3e60e4e419e561d22ee2af49e91 +oid sha256:13c8b71ef7331e86d15200b8b1979688b21337237bcccd309ce70d866f9304f4 size 12932 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en.png index ff2a810a707..71757e0f8dd 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30d46002f26d76684d6c14eebebcf27247ea53e6adcaf7ecfdcac0072760c011 -size 57638 +oid sha256:2d992de8f70372d03ca2e701310ab261c1f5ddb5864810071a84f6b17ce5f6a4 +size 57531 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en.png index e103bccbf2a..901af608dd0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8b59e43f446c86f6aa78e1a327ee1de84410a2cfb8db6504e5b0995a71589bd -size 60056 +oid sha256:0b34ca16979d6e496ec13cec78250eace85e17616cfa162d131d2c8f797c6b1f +size 59946 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en.png index 5b62c02b761..4ada35a7158 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dd9cdc2340bebd48275cd0f9ef5abcfdcdd99f55586d7356be0d7b240813462 -size 51774 +oid sha256:3dbd48f7c11b0b5881eedf939654e50b37bfc349c28c3c0e93d5ca5bda0717da +size 51691 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en.png index abb4e70edf4..023f5af795a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35e233bddf6480d217ff63ce7af4d37433e6cb1bee90a26428ed2e57adfc1432 -size 64668 +oid sha256:83a9e031d5b928895778ac073d4374863a5e8a6b0ac9d99fee0a95879a0421e9 +size 64502 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en.png index 24eb44ee45c..e9fc088f1de 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28367b3884a7eeaba27b42ad23db9205a3ab656b87fc0c88f5fb4b6efbb73c41 -size 12796 +oid sha256:064f8dc06e6a027bb643ab0f050800f510deaf6a2624f35b4847384c68903394 +size 12663 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en.png index 73009c3a602..1b3fa517d23 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1eac5047f2c1fdc06aa104f494b36a6a1b1cb3194b5f8f3ebf85b7b270e8bc06 -size 48432 +oid sha256:9a00224f7aea8db03d01c0dbf244b90eb5d2d3f4289ced45cda874dc655fbac9 +size 48320 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en.png index 80d1a233dba..f5c9bf6755d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1e8f0c5245fb38612fe136c7f5e81b2171d9fc4cadc90170eaa70a6e13f7b64 -size 67238 +oid sha256:bbca019f7bb7555bcfea4645ad3ec00445ec55a54cc98611b1e0d7f99bebeea4 +size 67131 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en.png index 2d45057f5ac..06adaeeaa37 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c99600fc43dc54482914609441c5e6f2ebf3df3a22c93c95ec025d0df8cefee -size 61572 +oid sha256:aa22afe652bae97bbe9e3504610f520e6fd7337f2757f79c39e9e0f6115902fe +size 61461 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en.png index 894bc933f38..0d8b2edc9a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb8bd336ce57bf8906d7c30142e3f7e1823ed53c197125ffcbc9675ff635601d -size 61454 +oid sha256:bbb5c63809d556bcc60b11b9c23940e26bfac377ed4a5a0d97971e060365c6f3 +size 61346 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en.png index 7c138fda441..4daeb638d85 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c16447c9bef2aedc1a0164313c8d0fa4719a63797ca0946bc4eabfddc6eb457c -size 56071 +oid sha256:17a15796338eaa5a8b7a5c7e5252fe2d9c392f2d0dd78ce9e0976dfd68fa6ad5 +size 55975 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en.png index 0e2092abb66..31bb7dad85b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41801b45373848016f408b2a91a4261bc5af86f5c53c97a6932bb9393e9754f9 +oid sha256:562dc9be79006376346ab1b2635890615eecc381e82c15b16a9ebf1855741e24 size 12817 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en.png index 87391fb7005..8ae4205f0d6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ef1ce4c3617f44e0c601b7fcfa9e2967c37383b9d6729defebf543a421970ef -size 55914 +oid sha256:2023b53d3a7099f55bc36a738506c6fab2910f3c83ca24a1647b6ba35c29f351 +size 55807 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en.png index 214c794cf2b..796180be455 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5ce938a9ae304dc845cfd790a6d56ec862342b79ec23a38986eb3057b25201f -size 58271 +oid sha256:41c3b6a09bf3899a0a28ac6b9fc96b773687adeb40a4b11ab0f80dadc1139e1e +size 58176 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en.png index d3990081dc6..1509868c156 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e5baea91836057ecffa0f1414ce08418a955ce4c1fd77e6c284779192af65b4 -size 51051 +oid sha256:24389189d4955f584fb1ed4b390e6ec829622d457ee7f3205b0104f2bcd90bdd +size 50948 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en.png index 74fe5848e71..c5fdad0d7da 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f8427c6786075496146988aaa114639cca599d6a6d7c8c6efc753378eaddcbf -size 64348 +oid sha256:971a027f6aaaa21b7d62af3f08b50eb7cf8369199076226ea92e1b4b30f74342 +size 64234 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en.png index 9454d33fc60..bfe215fb51b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28cbc07493bc3a91f391b0256554171bf60cc02d25ab8d0d78d68168fe62f534 -size 14993 +oid sha256:37a0c2d8d87124b700269ade727cb51c90a85e5549333c44763dfdd29bbcecf8 +size 15007 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en.png index 844f5b8b3b5..8209f29fa40 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfa988a2f41961dab1fc0ec245188dd18c1cfb430ef24bea5993593215d5f8e5 -size 14734 +oid sha256:006ef95362f37664efb45e7e6378df14d247827652efbea15abfca65ba2bd32e +size 14732 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en.png index 434dfd6e1d0..300fd84fb57 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c6338a05bdb3cdd5f9bf9c394836cc23786b9e22b21359de8d6da84c6411a08 -size 39993 +oid sha256:397a452d16ac3e7b6b330d43997d0e187a0560653678c9d8a2a869ef1a5031ab +size 39990 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en.png index e166379cada..db5ca162002 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:023265d1589b636d203b74a0700ab222990537b23a57547da2da6b17e1df4726 -size 36229 +oid sha256:c3c7680af60804efeb1322173b388ea0035d2c302083d6a8703f456f780c14c7 +size 36226 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en.png index 2d0ff397ee5..098d1a5f261 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e13e690ce2552b88b7f9d4cedb6aba7b5570eec7945748550c47e4271ef8fef8 -size 46691 +oid sha256:2f29f42ca6bc511fda1b9f28714a72ececd7dfddbd5e095599c50d2731e5f8d8 +size 46689 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en.png index 111068fdde0..993acdaf6f5 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4fef72516624bbdcaeb338b0cf30c756754b2a3fbbd7bfdda4602e50aa004af -size 39872 +oid sha256:458432a863e836a4ba372a55195a361445f1b40343de35f73d6b9c046e559a09 +size 39869 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en.png index 15e12b11080..a45ad15064d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:460176a43c2bc6fe210e1ff560a79e0986f1f5f6c5c23d5c8c63c03f2d4b84d1 -size 36258 +oid sha256:d7f447f78ef722067bba12b38c11eb11375da44f1f89b23841244bdc5978e888 +size 36256 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en.png index 2dbd76f5a57..73087c8d5bb 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0df7c623da749f59347b46ffe9297d7398740e95382e2bcdb9c0f3826050da6 -size 37129 +oid sha256:50810036f7f7bd45d885a8b7698f2ec36d9fff5ed05a9535d9816db2b058c1fd +size 37127 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en.png index ae78cc5e7c2..8a259ed040f 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eac83791261e74129a8c4edfa11eb1c0be653bae7dbca8603d2c20b20ba8f42a -size 44237 +oid sha256:cd25f197a4c9b55de3d2b7a2c1d980bce15411fdc7e2baa0981aeec5974dec6e +size 44234 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en.png index 820a77980a2..72dbcd7e09c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e6bcbd0e53134bba8a2bbcc39bf870f3c6319b549983a952f50ed7713c90bfc -size 38779 +oid sha256:1012d38abcd0f6fbf786394015a2dbb838254db50787c2bfe1f7a323b2fea5f5 +size 38783 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en.png index 72a7b9cb93a..97cc6c3d80b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da3ae47e1e6922c1a251e85b1ca89fe57d10493d3c90b94089bcd61e507c118c -size 35113 +oid sha256:f5552d1f31c61ed051cda154d34702bd0820295658227b4dfb90d0a8dab2ec5a +size 35119 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en.png index 4c30a4ea12a..a74fc67a3bb 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f2d0bedefa832506abd130b32ee55a49a50ef201fd800eab93149ee06673315 -size 45091 +oid sha256:16f5ddab43cfedde191c0e1d0f8960a64cf35df5451b0eff3f0f0d997ca1f2d9 +size 45093 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en.png index 3285d110aec..2352dd636cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bbca59a14d847310716804ff82e21ed82e271730f128674deb3a57300a59be3 -size 38597 +oid sha256:3922657bf1a4e1a968adf7d81859f815fdf5c3f96ef2665b0bc1ff2517f2381d +size 38603 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en.png index f76340a6102..416bdffd899 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d16c36a9afe44e84fd960b01e5e5c73f7b905092753c35a47cd8681e88c21df3 -size 35073 +oid sha256:ed64b87eb0d1d55feba1ba53999758c5bf0c36159ec394041b2502b1d8155614 +size 35079 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en.png index 74ec8b77ec5..f7595ec90a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aeb6939c0f6a79e23ee8107273ea9ff8d6938daed979da6b4c83ba8c64678d6e -size 35350 +oid sha256:2ef251f40075ce6aff45df130925a6614490f1b112ab9319673f4f88b7947b6e +size 35356 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en.png index c07a586edda..2ad93a6ff5d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67bf22a9478e488c607cb29fcfd8669cf7ecaf4d11ddf973c4ee80013c7f3cba -size 42152 +oid sha256:9ac928ebabd4928553db085b3f7b21ba5474e13e1dddb02c5f7b961002135cdb +size 42158 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en.png index 9d3238f0a94..285138d624f 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53023f46260a8ce65cb13fc22022b48d8730234c68ab270969d3df5ea3792595 -size 40759 +oid sha256:202f6c6095f2e852b4f71fc1917a65854dbb0bd56b85dd6e3a80f7598021712f +size 40894 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en.png index 00cf1f20b9b..fa54fe8bd00 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8077f68c48890297ae599d97b6f813e01b7be492a9c927503d056f9818f6c77 -size 40491 +oid sha256:67a08774600869ff76540d518c0d5f80c38f1f155a65af56374ccf362d5109d4 +size 40618 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en.png index c1ac92f83ad..21bd18a2c04 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ba118648bb2587492b8381417b2ea7d125e715a1a047ae46feceb28f257af5c -size 56973 +oid sha256:bc1246fbe273fafe61dfacfe4131aeb166a81a4c4befc35fbeb68cf85c0f355f +size 57070 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en.png index d01d523dd70..59f4a409b2c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d4218aae68741cf79d5a82cfa507ae3b32b41d489fe2b857124b86da22ec2b2 -size 37658 +oid sha256:f632403bc5aad7990c8f2a81eec9977db7bbf0fd8db33bbda61af5d25329ac7f +size 37776 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en.png index ae8039074aa..b393d1eef17 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:524260d6f2607404731478d238ddee0bfc478665ac368806c7f5c79e77575b48 -size 36256 +oid sha256:60df11189f82a42a51fddfcece2d0fdd88c55d06f2a1ee1e2069bae74e9c1378 +size 36410 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en.png index b75f7f74407..286ddded304 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03d18008c9821362f0e3cb4528dd4edcb05abb340102c3792cf82d58dbcdbdbc -size 46527 +oid sha256:4cff66b4102160aeeb3db4bb5df1fef1428b5fadc7a12a62170d2335f506ad3b +size 46662 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en.png index d01d523dd70..59f4a409b2c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d4218aae68741cf79d5a82cfa507ae3b32b41d489fe2b857124b86da22ec2b2 -size 37658 +oid sha256:f632403bc5aad7990c8f2a81eec9977db7bbf0fd8db33bbda61af5d25329ac7f +size 37776 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en.png index b3cadbd101e..aa6320755cf 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7ea383d4b4601916dd6c91ef851e8caaa5f60c61da12acdc84696cc06b6b3b6 -size 36651 +oid sha256:554d8d41ed47b34e71f6fff1689ee2c9488580d012d4ad2b867814399899c6e7 +size 36812 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en.png index 2bb840bb1f9..16d4f004971 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:436f788ed2ad56a71e9dfa07b148124a50f5cd2433ec0bb29ec78f7878b87150 -size 39388 +oid sha256:f91c4e8c0f6920b434e4e7f5048cf73a77fbe68d3db2fb122b7e44ac0040be3e +size 39613 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en.png index 0c8da7db9f0..4115eb96883 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22d27b6b968014623823134af13e6ad472239f5ccbcef70d7161e14a6338c16d -size 39133 +oid sha256:036944c74e2a5150d54e9ad4d64fe6b624fe8e889e411c226816398c65110f55 +size 39321 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en.png index 7cdf8820325..b87b3c68508 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca1abbdb9f265f6372e172a23024c667df4ee16897dcf5ba3d86891dae255dcd -size 54515 +oid sha256:24df22e86f0fe16ba9d3ce944105c0e2450bed08c8a9556f0d3da0cec32067b5 +size 54642 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en.png index 47b3cd02d53..f4a6eb9f298 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd1b4e134aa359718b9020f2574f3f196d765c6d270a1bd66abf161559d6e44d -size 35720 +oid sha256:ca39d5d1f2a24584a695f7e3741a82109870a41a289b72882b9cf476e0526de6 +size 35948 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en.png index 590a83c5145..3dc45575199 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:959f935d5ca63b0787a31a0d266aeffe6ceb843626020292a3d4712894ee4bf7 -size 33781 +oid sha256:4381d361278286530bbf89b79c56ae37edb722ec0a829ad20f98ebe1205aa1b9 +size 33990 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en.png index 575908f1287..00930872a06 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9115b730d9fa662ef734e2504e01363dc11fa3bde23a383f9f739b0d31a4d836 -size 43408 +oid sha256:29863c9616f9ff6eedc59d9c8a78304e061f68448c51c217f8c43d7bb2b5bf69 +size 43568 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en.png index 47b3cd02d53..f4a6eb9f298 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd1b4e134aa359718b9020f2574f3f196d765c6d270a1bd66abf161559d6e44d -size 35720 +oid sha256:ca39d5d1f2a24584a695f7e3741a82109870a41a289b72882b9cf476e0526de6 +size 35948 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en.png index d35a7e6de03..b0418ba23b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b513c43ded972345fa14d766c1d2ef380a8acc498e42d7322545d831c744625f -size 34273 +oid sha256:dea918985f1cb9870d5da1e84ac2c7a43227f03aae4e4b7462bac5d6ba7343c2 +size 34480 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en.png index 9054ab58529..b271223fbb2 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0d5f98545b38207b0b61cc055112524b2b87c9969b41246a90d46554143905e -size 28628 +oid sha256:baa049800f4e7b6ee28c7a29d509c629fe3a18119add2fe7d3bc21d259e196c4 +size 28627 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en.png index ea9fcab7f4e..318f2a8d04a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c963f28503909ff8f965910922cefd82d4873904cee34bbbf4189b3a29b2fddb -size 30877 +oid sha256:9ee8ec155e26c40f305012f32adc07bb9698e21ec174c2bcba307376d56b0c0a +size 30893 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en.png index bd707d64d36..95724fd002a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8073fcc88a65c07810373c176f790ebc1f41953294a36b44a6d21bd102482d4c -size 28075 +oid sha256:ecc533896a45756b21e14302488735640dfd3c05c695153bf196f7c15779ac8a +size 28079 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en.png index 1b4e23cb3ba..1133a655e9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.securityandprivacy.editroomaddress_EditRoomAddressView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f3e187b8590ec247fa2640db033fb34b8f8eb8c5bfee6a4598514049cf87da9 -size 29909 +oid sha256:9091edee5c475dec3a1e8c4d43df4502a194b226156a333eabe73a2a66dba6f2 +size 29913 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 fe561de9dbc..40b2f015f3e 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:f883df9f8dd09fe8ac29cdeb0358e1b1f82fd0bf4f3b00778dcf2a436dc332a3 -size 42513 +oid sha256:43dd94d40380a5a667aee02d3ee002c137a68a7d10ba9d3ffe09015b505a6d44 +size 42594 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 2d1351f34b3..4c47f3d6234 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:ec59f34715f2bdcc1dc1ca9718f05366aebf7973a027b261633934680f85d9b4 -size 41133 +oid sha256:11af1897c0ebc05108efb6d79156af7a549788141afabe60e6b3a721e73462b8 +size 41450 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 5da528088f5..5d0e028c034 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:6cb196faae29f31e36f7e5a2f711e71755dbd76b0d7b2126cb79575628bef1f2 -size 40103 +oid sha256:44575c8ffe72df361eab4e6e7775b150cf901b309e7be59d0ceeddc27f9800b1 +size 40360 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 81da08c8ebb..6d90061fd0e 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:d17ebd1e51a73a8e213f792dc220125c77bc9143c9327bce9d25cb5573b63d89 -size 43278 +oid sha256:022987a206a0277729d65129a6efad6dfb0cdc8f06dc6e811f90307cba9f78be +size 43596 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 a61f6a87bdd..a2551c3c098 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:bc0c8d01736a19447d3e0c810fe594578b3cbacd5de063a7e314df8da3b81cb6 -size 41415 +oid sha256:dbf72b0000c25d5a8bc46e187b410b5e1d4dcdf49672b7e238221fd22af014ce +size 41705 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 e7bb5b3e8b7..6cbc4360463 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:0c36a541e25043a7c85d2aeab088005afeb84b36354e8231ac10dcbe95c9c278 -size 42216 +oid sha256:32b57de3b1128a3a17ddc1cff7c96d708f4ac4ef9efa6a6d3978b9cfda4b6a0b +size 42301 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 a9ae3fc4b4b..d21d1e40f0c 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:afc35cdc58f53f14f4584dbed43744435aacfa6d3ddb0b3b5f0e0ab7e9a7cc16 -size 42799 +oid sha256:3f40ee24252a856c4680dc0b62865e71108d54b326c81b651dcf9031c8d7ae95 +size 42835 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 new file mode 100644 index 00000000000..1f0daa85dc5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_16_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e610606229c8843fecfc699d798e530277ca7fbfb870198809a95417053fd594 +size 42018 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 new file mode 100644 index 00000000000..d3792f6a0a3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3cc4924fbbb2fea5937bf481b1322da14e1d2c8ea2cf4351fd6d0d222357738 +size 41304 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 new file mode 100644 index 00000000000..4e7871b133d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_18_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60571dc08867db9ad2f6528e95cc65df0eb57b9840bf8e96e6e1fbd802ad3a26 +size 38778 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 new file mode 100644 index 00000000000..fcb6f771775 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_19_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69d9aaf46305802861df8be0341abd73068376893bfd7aef43d7b8f3251350dd +size 38735 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 142b78cefc9..42c26d96487 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:79061fd2609f0e61df7af26919d68f22b78864ce031a6fa2f397af419601c367 -size 32253 +oid sha256:adaa2dedb41e4717fab78875fa5e11572ff768cebe3a8ca94a1871acc634d319 +size 37705 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 5d7829db3c1..7a25cb4dce5 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:879817d4155151a61cdf642b91ccc344f38bfb1edbf75129302fd0a115fc16d6 -size 34378 +oid sha256:1cbbcbeb51688c957181c186319c8ae3843368ea5e684f407bec67ec16f90371 +size 36219 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 228ea7493f0..a80e7db43c7 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:984798b0b880ee29f4f328e25ad531d56cd992f3fd3771b46d94fcbe1c1b9d8e -size 41626 +oid sha256:7f7ff91e97c256c96e261048d2f5c1fe50e7c1b6b752ec830ad63fe51cac44f5 +size 41939 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 56a3c5e8b65..6941fb2722e 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:fec1fc0c0932de4f00d7146120708380b64358123b97a227458bfba5117ef40b -size 40488 +oid sha256:ed84b2185eeecdcaf5d846b386f8d3b37f6ae6892d099950d8ec2c988aaf7ae8 +size 41111 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 28dac2ec2c5..1130fa23f4d 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:4a8c62eb5deae233cce822d39d85f2faad5152b84469c84ace57c042499d4f51 -size 38727 +oid sha256:7c823d58a58b9adbd3894c4d53754439da3533d72639f7181764ac1bad449bf5 +size 38417 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 cb1c1afa9e3..adf133ac794 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:6fc7dc287c5da18ee2a7f8fdadf91285f6e9ed7a8328515cbfd6f90b21642794 -size 42190 +oid sha256:3444349d7e07f7da2db5cd71b7334447a5d218445d626fdf8793ef224d07b731 +size 41821 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 434fb525f3d..0746cb3861b 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:58349ebc2291b8fe7daefd39db44983b6b10712b6d56984745cee8b4e1fbe96e -size 42429 +oid sha256:bf0c4686f5c3b8dc33a65eda243bab68a01fce9e5bb747957a421028f67c2f51 +size 42783 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 c530fc7ff03..440eb609ecc 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:9c8ed00bd704382cdb1b12507f54c7fb258b0b15f95cd376a816a457dda95d40 -size 41413 +oid sha256:b1c43dcb8fa94250922a5c55da1480295c843881f5f325583061c3a4efcc1c8d +size 41721 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 f4788aff681..33d3574fd31 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:a04aa114cb44b5aecfb9a5520bb1ba88d068c3722ba074858f0e1102f2d38766 -size 41436 +oid sha256:a19942e696011410011085fdb05573d5a9924c26a7cd5eb14d8ef47357520228 +size 41771 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 b83dad80c2c..8008eab180a 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:67ca921d06a4017b701ebcd3703eb58e7032a54a6e1b19fc02808fbaf3285a00 -size 43501 +oid sha256:5054000865a6eeed597c898c75cfdbf5dc1d0c8b49d301fac9dcd636e17f16b2 +size 43450 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 eb62c634e12..dd60c1ceac6 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:34237556029910293162d2ea98eeb9bf55f8393f2439d152d0afb7a7e40e511e -size 42032 +oid sha256:2989b72b62c6454e2770c24f488be188db70743fc98ec382f96f67a506fda909 +size 42207 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 3a8e618495e..a6b369d6d32 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:ad2042b9223b191b36cde6e0bfb1be25babcc24566ff80b3c23acdcfd5906385 -size 41016 +oid sha256:941447581bdae57977bc6021e586618765756b88fdfc0d5ce5f52aaefa7f6e71 +size 41209 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 f55cfdaba4c..63bf4ceab4c 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:4d1ea80b3990d355b42356ea6ed8419705a4d8174851869742b1372f5d815648 -size 43868 +oid sha256:42795ec59657adcab57a18c989be8a84434561448c5b242e655ea2568c3f536a +size 44046 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 e9f7c3b7e1c..a348a4ffb7c 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:3ca66de9f5bbedce4f33c86710cc6c63b96ff872ecf8514cf7dd39043fb6ba0c -size 42379 +oid sha256:713573ab48dc25c11ae0e5bfbe1e9909af6a1ba1115ffcfc37394171669c73f3 +size 42535 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 8916044198e..6a5be52a907 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:5bf22a62baea13b14d736ec6166401c73f9f8704f8436ba9ca631ba05e3425a6 -size 43179 +oid sha256:b67e13e498cc4b0709d74faa02f78692e717f3274c2f28ba77450819c2ddebb1 +size 43140 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 f03fe00b2c5..181ad2bfa3a 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:1627c9920140b37b99dd482ce8bca5ee12882cb90ff1968f66daf60837b7cf53 -size 43790 +oid sha256:f2925c650f266a5b39ec77489e2658a70761e8332cc759e418cf733b86572914 +size 43755 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 new file mode 100644 index 00000000000..184d471cc79 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_16_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1e43739e24423815b90dc6e5249400fd1049985811763422e9fc2aa9a74e610 +size 42875 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 new file mode 100644 index 00000000000..c9734faef59 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63c75fdca8960f015caaa11bbb970c6e7621e87afbe7761d23db875664c55cd1 +size 42360 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 new file mode 100644 index 00000000000..e60ef55c422 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_18_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cabfc7c5bf6f9b612e933cad211b8261b236cd981488cfa081e0226940626339 +size 39499 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 new file mode 100644 index 00000000000..2e3298b2ff6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_19_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf96db08ac0382185c34863b38603fe55eb3aab691fa3c42b6d73e4167ce8f82 +size 39383 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 90569a9e7e0..43582278ef2 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:47954db34506ba2560550d94cbe2cb1278866ab2843e34f92f8e1ae535bbd173 -size 33090 +oid sha256:49f36e08fb97b0530ef46c557d04d49fe8e8df9afdabcedec823fa6727bf15a4 +size 38670 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 35db5282958..d703fd166b0 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:30aa5d530772b143947ef620736128832dd47c8a2692a7bfbaee74ef66107f02 -size 35273 +oid sha256:1d1f4f05037d4ad192067d189dc9fddc860d55dceed95a98ae70b57ef3b74a48 +size 37097 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 4e678941ef6..483f10f66f2 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:82670330ea89664b305e223c23489e2746f645c5cbcdc33da8f2c629aeb9739f -size 42505 +oid sha256:3b63477c6dcd324babb989a5bfdccdec99dde345a134e267c13127869748accc +size 42670 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 bf9d831f5e1..d661dc86fdc 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:15c1291fd3c6547285a9f62b5c6c8516fbe714fd79392c9738531cbcf1d97997 -size 41450 +oid sha256:596e2613127ccb7ae3f2362727082a473ba617e8ad05a61fece2ada05b0b8c14 +size 42006 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 e177fad5e2b..1bdd63796d6 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:a4882133a2dd3460a7567e01de5803d502199c2b79b72d6f66b1a8c6df3021f6 -size 39498 +oid sha256:d254f940a7ef821c2a3a731e60fd23bccb61827bd00859d97139b9f93a5c8c3b +size 39075 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 42ea2bca5c2..ef1dcb04b00 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:787945a9d09f405aa8ae2efe88b40cf61b07e21b402e6d842723378ff7d0a001 -size 43242 +oid sha256:4277e9d618880a9d3be5d672ba7b7081af99f36f34f4678bb160b98cac9fefcf +size 42630 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 99bbb99d554..bd8f4f88300 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:1e3b4b5ab82004736abf58e29ba4db7f628e25c8344a782263cada5544ae3748 -size 43528 +oid sha256:aceffc8c16afc95f26a0aabb861aaba630ea0a9365ca1da3ad7a483ab6451282 +size 43740 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 8e8cef64dab..c26265b2018 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:6edfe665ed9121e32914884f32fb0801918b26574e04c2ddb07a0f2e66bef567 -size 42449 +oid sha256:011c18ce9dd7bc99bc9f09ce4f296aafc619d83ddc4f94f1465b49925591705a +size 42625 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 39b22edf338..0b06aa53b89 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:35a491ed5aba0d99f1db99019f3f22d09c74a4dce7693e95e1a356a0353df626 -size 42405 +oid sha256:b3971573f164e80b6e59bca91e32fdb1b5336086561e69148c22b1e7bb396935 +size 42601 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en.png deleted file mode 100644 index b8d5aef04ab..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6edb0ed0841360dcc889b874c890e4871e6f3081dca788b96a83e7f308675a0e -size 41437 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en.png deleted file mode 100644 index a93b056ebd7..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:266d8de099eeb551bc287647611ffa3a9cccfc98f0db70dfd4b1edb9a658857f -size 49761 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_en.png deleted file mode 100644 index 53b2660d98b..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9300cc18f5c3dd823760571d95e807d0305ba7989269ba42d2382c6e9f83f023 -size 41147 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBar_Night_0_en.png deleted file mode 100644 index 14111764aa0..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_DefaultRoomListTopBar_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2be7c170ab82578255aa92658dda00449ae4987dee552ce5ebdd1b1bb62394ea -size 49422 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png deleted file mode 100644 index 575c6f033d9..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd29991ad9e550e5f1e997bffd89bd26d68a97a11213819e6f646c594deb0a32 -size 37054 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png deleted file mode 100644 index c34aca9a8cf..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0abbd15a76d2b3b6a4d66e2ed7df83f1cff94ca03c38b7b2e4ca1e4075229ce -size 35731 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_3_en.png deleted file mode 100644 index afb4d8677dc..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e2e1602992580c5336ae1bab2789a0eca4d3a05a50299830278443e9d4140639 -size 18065 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png deleted file mode 100644 index 95d69eece57..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b00197e010e15b647faba1d6db9cf7e8a1bf7177814e25b07d7207554810107 -size 47084 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_5_en.png deleted file mode 100644 index 333bc47ff95..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e1f630cfee64876a7143dff8720de4166248607d2c1247c857ed35290d08ab07 -size 73216 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_3_en.png deleted file mode 100644 index ff857e8b980..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d00a6257b8cccad2668952edd6f6646fc8ed21ce7514ef41e7fff4c14a40da25 -size 17672 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png deleted file mode 100644 index 3728c0cf7ba..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c20373e670c4066fef2ca3980ef3d4080a34992a8735bef9a3a87ea095309852 -size 45540 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_5_en.png deleted file mode 100644 index d6d3773dd5f..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8dfdf6bf277bee38b0e63dc4da4c28c7cff394419486681c4ddf8cff20bfb34 -size 71739 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_29_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_29_en.png deleted file mode 100644 index 96d500520c1..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_29_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe7856a003131918100d8ce3da0e879f7c8d63de64e1b5889327738cb7a8f2f2 -size 23128 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_30_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_30_en.png deleted file mode 100644 index 5ee881449b2..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Day_30_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f64d2899886ab2e20bbd2dd0e0e2bd909f461751e60f4b6d93e506963c41ce03 -size 15832 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_29_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_29_en.png deleted file mode 100644 index 92876738723..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_29_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e45e0dc5de85736bdbb3221f26c1cae3af78dd63acf0208fd12e023d91fa70e7 -size 23183 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_30_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_30_en.png deleted file mode 100644 index 4ec7b36e3b1..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomSummaryRow_Night_30_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20a0dfc19f05e9727cf70cf7eaf83503c805423cc36edbefaa119c2692eaadd2 -size 15678 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Day_0_en.png deleted file mode 100644 index 7124ad7d501..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:37dd70368b6ab4bdef8ee33adbf6c662bd887e46ab1e959d70d583478707ea56 -size 14531 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Night_0_en.png deleted file mode 100644 index 0fc636b2ee2..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.filters_RoomListFiltersView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:77d7a204d2ad405bc620578d3670948e610757d43eafc5609b2d2bd7165dd449 -size 14302 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Day_1_en.png deleted file mode 100644 index 164a9bec81b..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d268304343069be162f6b42b9824d4396ed4de57441713c504d16a1b973e390 -size 19161 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Night_1_en.png deleted file mode 100644 index 237e0d111fa..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.search_RoomListSearchContent_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3164a91cb3a713f86c6d758faba231f75081acaac23a2768820b283ed367e4e -size 19286 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en.png deleted file mode 100644 index 84abc8eb8dc..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bcca776ead6fb79cdbab438c3c1f0267dd4560f04db2f0c02891dfcfa4cb744a -size 18967 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en.png deleted file mode 100644 index e40f7db29ec..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07df8414f35edbe82325919ae607253bf5b26642f2640bea71e4c8c7cad9d8ac -size 20767 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en.png deleted file mode 100644 index 5e01c6aa35e..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1db476cdd2e0dece3e490c44e245a16fc0fd0f4ef1c7fc8ea798542fac3c6cf0 -size 21026 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en.png deleted file mode 100644 index fb612df2a96..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d345c2b76a5ffe15aa4d793c9e27d0ab6935ee10cb27acabb2d94929b10544c -size 18259 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en.png deleted file mode 100644 index 69c9fbf0129..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0f6801f1ae3432f24b013f8e3d8bc002fc23223e49471ae1bb22f30c9468cc9f -size 19976 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en.png deleted file mode 100644 index ac1cf4eed39..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8349c065e9141738cb4b737ca0d67b3dbd91ee94935c249907c520e9303dafc5 -size 20177 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_0_en.png deleted file mode 100644 index 845f5acd9ee..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09b8dcff40d9231cbd5b81db4836081046dba2bab37d9b4e7bae49abf7debd22 -size 82933 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_10_en.png deleted file mode 100644 index e7e953fd689..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_10_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:edb52d5fc252ee719f6aec12faf98b8976eb9a61992dba510c06386999975ed2 -size 105988 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_1_en.png deleted file mode 100644 index 845f5acd9ee..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09b8dcff40d9231cbd5b81db4836081046dba2bab37d9b4e7bae49abf7debd22 -size 82933 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_2_en.png deleted file mode 100644 index d165a8c1f72..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e398afce8d17e333d7f33ed134dbc1de6651e525f72dff0980827df2609b5273 -size 83578 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_3_en.png deleted file mode 100644 index 60e909443d4..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0cd473c052b15a6e24b9b6739533cde3df2a32a43b35a4336ae3c8c1f6ec424b -size 62223 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_4_en.png deleted file mode 100644 index fb2fffde1d5..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0fae50917d9eefd54f29b79e8ccebba27dd5aee4058e0bcf54aef98faeb5a5ef -size 61973 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_5_en.png deleted file mode 100644 index 79c31a0f5ef..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:25a9019442b40f99ad0085c97bf20e30fdcd43e1b4ac64ffc7f59ae13c81f785 -size 60217 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_6_en.png deleted file mode 100644 index 96cd13c0356..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:85925c39a59d3ceca4da5234084a3218a33670a9e3fbd3121ef601e06fb93297 -size 100367 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_7_en.png deleted file mode 100644 index 95582e918bc..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0762f1f181107fad31c233bb1013cf82c6de2b94500e592609fdcc43e53a5807 -size 46550 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_8_en.png deleted file mode 100644 index a22032f9370..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3fef8280fdce4608a30e24063699ab99bcb55865b2df51b070e2dc0eafce23a6 -size 41271 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_0_en.png deleted file mode 100644 index 5dc5152a671..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dc64620fffb86bb5bcbb175795ebde2b84c18e6989f99528046b1ab14f3c9005 -size 89396 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_10_en.png deleted file mode 100644 index 3bb61830589..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_10_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1ee1b7d094db4cd00af34d1ef79c6e71ed61cec585612c538a0bc870c64b9966 -size 111814 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_1_en.png deleted file mode 100644 index 5dc5152a671..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dc64620fffb86bb5bcbb175795ebde2b84c18e6989f99528046b1ab14f3c9005 -size 89396 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_2_en.png deleted file mode 100644 index defce1a22ee..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:28ba0acc4ef26efdb4bc2f18c1643a82a180cfa429302da9df3ebce73f3f6552 -size 89287 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_3_en.png deleted file mode 100644 index f60bb7648d6..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:73adc32cbb8a372ff22cb7bc864ad09371deae3bd655081ca184aa6647e43943 -size 70656 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_4_en.png deleted file mode 100644 index 37bbd14e58d..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9388d2a63d46c890b462135a524a2bf7821e0dcbd2814de4523925ef6225fe60 -size 70413 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_5_en.png deleted file mode 100644 index bcb954969ed..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3cb135e79b92c2c6ad8e5b7a2beb4819cfc1e01d3000da6d400b7312205b7c7f -size 68644 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_6_en.png deleted file mode 100644 index 3543bb4fb27..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:49097acd61396b5190aa1820c61d826b04738dd56acb4cdfb5df5b04c486e961 -size 106227 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_7_en.png deleted file mode 100644 index 6ab0b8da397..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e339f50db6e8b06bdf5a26559827f92f5a9fd2f20bce4a45d4edf5d45e071b9 -size 53790 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_8_en.png deleted file mode 100644 index fefb0a37318..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9496a4dd417cb64c20a1d53fb27f9f567f0eefc8c0ad428a9c6c4041f66eddb0 -size 47845 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png new file mode 100644 index 00000000000..671153e6b56 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c023a4169536c12d13c7599c4d92e77269337e499b92da85df6501aa18b7818a +size 16929 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png new file mode 100644 index 00000000000..3af517ba6ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73a35d9dc688e2c992fc53ebafbf547f88fd96d5272ccf58fd1ac8a8fde55ab5 +size 20793 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png new file mode 100644 index 00000000000..82d780aea22 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7a86f06886332e666f2aae7db543bde1eee1dc0c193f4c5797c1289f9287707 +size 23764 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png new file mode 100644 index 00000000000..702f7b1797e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:752e41bf7050d0e21b644503f1fde9dc479e27563aa6257f2b259ca35d05d15b +size 23886 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png new file mode 100644 index 00000000000..27a01cf88b1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a3da0d5bba7824bc5a76b87bb2be17d17d7d2f0492f4332416929366efb7a61 +size 30927 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png new file mode 100644 index 00000000000..51a21383634 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cce848d6c16adfcbd2ef9eef5e65a035ccd4d0a3092953e51433fc134c286518 +size 29471 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png new file mode 100644 index 00000000000..e460132108f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9d8954f9462d07f7e60bad2bdfb350874b203b389433288b22366d54b4e4b1c +size 15707 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png new file mode 100644 index 00000000000..3ac9efeaf39 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:049ba622b24940e5bf6b2b008b4ba3752d67b744b816cb8af2909cda56dbf0ad +size 19298 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png new file mode 100644 index 00000000000..5953c6cfc98 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:958e47f8f5905562f78fcec67a47d0178b50bc0898d63c03deaeb5bb0ddedd8c +size 22307 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png new file mode 100644 index 00000000000..f61d40f5354 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ad01b142920b585301210bfde02c0cb942418872d10030de0e8422199169b71 +size 22410 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png new file mode 100644 index 00000000000..d3470ac368e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6723e18a9ec3f01a2909f97c5ad80b7ce682653ec16229e1129ac509c2decd0a +size 28912 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_3_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png new file mode 100644 index 00000000000..a5ba8dad457 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f7b32aed7f97cd97719c22400cbe7a4d14e9a4cf10d42b26b853cd81cb42f1e +size 27620 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_4_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en.png index 7366550a779..d02c579fdb2 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2159965425d13676b20c72d1fb1578a708a79b6386ea4ccfc8b8773885f24d40 -size 66188 +oid sha256:2e965d5398acd512547749bfdf62dd4fb7d0b5057e97e132583bb35884072260 +size 66205 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en.png index 7366550a779..d02c579fdb2 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2159965425d13676b20c72d1fb1578a708a79b6386ea4ccfc8b8773885f24d40 -size 66188 +oid sha256:2e965d5398acd512547749bfdf62dd4fb7d0b5057e97e132583bb35884072260 +size 66205 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en.png index 13ea28e2421..a80baddcba0 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fadd7226eeb9132a6848635011dd31057e5f9fafc959476500efa4fbe2e907b -size 66699 +oid sha256:b4876d94e3425249746c7d2e313160e23161f6d97fa7cefdb9cdc067098ef16a +size 66715 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en.png index f70930fbe96..140ccb8cd34 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:960b59b8ef1c33887e58b367333e5e02d8db33eefc9a4d4919cee81ebfb75dca -size 40032 +oid sha256:cf7efc840872b727a6eb7226f2d1133b16eb7edbbaf5be52a3da942b718c6cc1 +size 40051 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en.png index 170a6279193..bcf2df6cfcf 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9d9919426c607a37e2c42c9c1f11fca097c40accfe2e6b909eb583684aa973b +oid sha256:7a643cb467b7387849458ccd6f84c7c800aac6b1054ae53b751782b4ecd148d5 size 63709 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en.png index 170a6279193..bcf2df6cfcf 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9d9919426c607a37e2c42c9c1f11fca097c40accfe2e6b909eb583684aa973b +oid sha256:7a643cb467b7387849458ccd6f84c7c800aac6b1054ae53b751782b4ecd148d5 size 63709 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en.png index fa16bd96438..8879a9b5dbd 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1af579d0d2544971386704109c6332febdf4bc4e6fba79ebb17a7b8b3b0ebc83 -size 64253 +oid sha256:3396b3b1e11fabf41f6fb193e2620763f88f1ea8a3364df20b4b185661feae4b +size 64251 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en.png index f6e6d6966d8..6fe7d9c74a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b84100531df03f407833672b6ae71f0532925e5c9e65e282417c3af1fab8b390 -size 37412 +oid sha256:9a268e7609ddb0cd23b41d5931af896373ed82f2dabb6ef34841428ad7cb558d +size 37436 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en.png index a99b9983d4c..004d431a950 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a45c0e66b8d59b6de1098c045bbfb569f2b89cac7adcdeaf047f0e86bb59fc78 -size 31000 +oid sha256:c951ff5fbba4f82d02e3b5c34771d188777d1db316756e7bac96b2dbf028ee52 +size 30931 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en.png index ba8b461617a..e2be7b186fe 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0c54496802024e69121e25c1cd3511d75b1075311f1c423cd3af104f07b96af -size 41809 +oid sha256:94a8c329900ba25352404abfa6e485c876f229a33abb4771c0632ef2dc18637b +size 41786 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en.png index b8b51abc536..0ab79fb6845 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8da1db7454b4c44e819b11843463efba938742e978ed8766a0f21be6b98e5457 -size 41982 +oid sha256:a4730e96fc6edc15712a254bd9fec7d3a0b7f1da0e16304a951a8d62cea96398 +size 41962 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en.png index e692a54220b..2ad2d408926 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23880297a428a6f8e1a9159ca6303e7de95a2ab977c4c81545dd9451c3b1c446 -size 34954 +oid sha256:6f4eacfb8d56d34ae0d85734c6596e86d3ed3b9a664104ac82808c721af9e666 +size 34869 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en.png index 4d0bc5abb70..6d0b4ff7d5d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bd36481fb2c487f4f133ffc07327400a18a40c8bdd38faf123f7b68f7486148 -size 30168 +oid sha256:03f17ed51483a5033320e4e4855a3161c208b4a85effce4fd5ad4facaf99089c +size 30099 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en.png index 98f6f11dc1d..887f6fd662e 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:090e8fde4c17b3373b2263ef3971926f512ccc3e7106c50c6c6530d442f83c0e -size 40676 +oid sha256:2e4d51d755743a55f653ba7534f1794b13752f391bb47866106ec0b48e13f060 +size 40611 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en.png index f8935973c72..960223ada1b 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdd1b95451a1665f67510f85f719d331cd860af37aa715f099eff6e7eb4bd50a -size 40551 +oid sha256:4899bbf943966cacb39756ec7036579286dff12bcb9aa5b573e09a0929f5294d +size 40517 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en.png index 793c619c489..73f5f416738 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99af78be552d9e5a676bc24235f83357d53210d2c312f60435c8b28cd89b3827 -size 32532 +oid sha256:c836b91ac01dc49cd0d46137b28183d5bdb8cd3830728bb3e5e0e9af3d6a3420 +size 32473 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 9320e7331f0..51f0a43fd9f 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:89135aa932f161844b79ef452bb27e2f5bcd19aa9e7cb19224c719ab948798f5 -size 27769 +oid sha256:b638c18d6b0ec1a98611628702711ae6fba5e159096c2d6c47c7449f4a7b1765 +size 27789 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 0f0176fae18..be22dd4721d 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:c02ce7bd87c240ab7cbcabcee98c6ac16db46351185ea97a032d52b09b479e59 -size 27095 +oid sha256:f01961ffbddfee0ee9835e423846130120ef12fd9efbf41d1545f7d29a5e5f09 +size 27100 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 0f0176fae18..be22dd4721d 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:c02ce7bd87c240ab7cbcabcee98c6ac16db46351185ea97a032d52b09b479e59 -size 27095 +oid sha256:f01961ffbddfee0ee9835e423846130120ef12fd9efbf41d1545f7d29a5e5f09 +size 27100 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 93c6c2bcce9..31770ad19eb 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:65b6d9fdfdc3adb0b0ce567e1561c677f6562043a6e9e2a3b74fd7318b02ec0f -size 39500 +oid sha256:6243c5048c83b61c19ad23710488a18a6fb5c6f101e141ff411c5544b5ac8c64 +size 39529 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 e433e7f7fa5..99c296d0aba 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:e7f56cc2df34363c65ad46025d2a22189fbf050183c29ae76b30120f879a1a62 -size 26855 +oid sha256:d2c88a36bf2b4dcfae0ab02961c28cf5db55780bc4b68bcd7088e20493068d81 +size 26893 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 9f24d1adcfa..f3772fdf7f5 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:379cd0a7132bdbb92a6a17962da378b7b44113be9530fe75304be3318ea2c129 +oid sha256:a50c384d6d09f425be559157b8adf40da17103a19d98162cea9b4d59c4d820bd size 25128 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 9f24d1adcfa..f3772fdf7f5 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:379cd0a7132bdbb92a6a17962da378b7b44113be9530fe75304be3318ea2c129 +oid sha256:a50c384d6d09f425be559157b8adf40da17103a19d98162cea9b4d59c4d820bd size 25128 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 b8253e844ce..6fee730ed06 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:41d1e2bcc7baf2b813a6cce22d238afe8f68976b3d2c57388aa5b8e4f02de59e -size 37733 +oid sha256:c7f8676894fd8d662c92ad01a6ac5998ef2197f489a04f7a7a42400a22a52025 +size 37780 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png index dac97071cf6..1799aeace09 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1a55b39ae7912929b064dc31b62368a38db30a9c1d4dcc3cafde7291c54d501 -size 69035 +oid sha256:d49a60535fa260b6a920fe1765b0a4e6277a4a513eba5a35f2992f69576bf121 +size 63448 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png index 42ae3b31d32..9cf51a3a697 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eeefa70fdedbe04a2ff87608885296d1feac62d00127c55d4560b33be2b1fddd -size 58606 +oid sha256:45471081e7c479fec9279dcba2e8a65f283bde610c006f387581280a489dec1c +size 54422 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png index bbb93822e39..1fccc9e974d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41d5780c17995aa5016187671d7bd75060a243437d31828657fc9bb3724046d6 -size 68019 +oid sha256:b235d43a2d760c978c2fad9ca3cb6d9c84991563b70bb79852da00ddb1a04d6f +size 62362 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png index 097e3bd5962..a283c16bb07 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95760704db8f3a289dabf59d7b63edcc60ff7d558c74228d055cff17cabe6e8a -size 56742 +oid sha256:310bf9cce0143135a94b16704fca319db26607976c2d78bfc2896f85b67c62bb +size 52199 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png index 5129b34c5af..1edc1d26f49 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5481f4227f3f1cfcca8ff9653c13d43599b60408d6ddaaef854646fca856964c -size 35153 +oid sha256:e6548b4565f944b54d46d72cb0e3bd3dfa07769bcce370b01513f8fdcfde47de +size 34999 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png index b0dc4b044f7..7ea3f9da1ff 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:807cb5c65c62b5a628ad8719930aecebb03bab9d4d2565aa016a8bb4d23cef58 -size 34593 +oid sha256:483a7186c071c330632de27ac909d2fbe83e056b4b196a571632be4a657fae61 +size 34448 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 ec2a7733634..b47f61ffd21 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:2299427647ef9673a68b5235bc3503007874d0570324a17d3c3ee40a5e581af6 -size 42671 +oid sha256:0529088a7a492c6b89cc847b9b94758d5a7e2bad1f3c4957357f127682c24c3e +size 42678 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 7e94c40141f..dc86dd34da7 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:0a011821df73e73ac3ec15caef2c6cff228cf0e5567271164b655aedbd4755bf -size 40228 +oid sha256:0bed0b9a8c3f871af609ecf20d775104b4712ba944be9641f442e3f2228359d3 +size 40235 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png index 7cbb9c3fbe3..277f7990b21 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b71e77d68b21262c0dc0731a4bd4a62053ab94f5ae3ef7ac27b29154cfe7556e -size 56493 +oid sha256:3d5aec2ca7aa00c9a3b4a21e6589497b784d90676ffe94999efd245916de2f8d +size 54310 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png index 7cbb9c3fbe3..277f7990b21 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b71e77d68b21262c0dc0731a4bd4a62053ab94f5ae3ef7ac27b29154cfe7556e -size 56493 +oid sha256:3d5aec2ca7aa00c9a3b4a21e6589497b784d90676ffe94999efd245916de2f8d +size 54310 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png index 968c0911939..9ec14d711f0 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e8a2209d43ad778815d02edbcec99b3ced0acd0a672d792ce862e57d8284cba -size 50309 +oid sha256:56d784b2a9bd8c36845ad0d559ccb0fc38d93b281efa9f50590be375177ecc52 +size 48492 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 01d621ac7f7..67adf637d46 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:1220f0136cf5dd3558dcd08ff7f820fac4395b4b35fd5b144faa65ecf01ff620 -size 41305 +oid sha256:63a4ef5dd8981a98a5f22acc6bd14324421fb1f8653a95ba5d6b61cc18f70556 +size 41307 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 e69ddadd5a4..09e0036644b 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:b538d064bf3efda30bc981e24390e65918e3ae570f13b1329bcc17e55e4216c7 -size 39020 +oid sha256:8b88199e33cddbead264420a0d311cac735d3413a44f5da2596fbcfbcd0f5343 +size 39022 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png index cceb41304b0..b145bb8019d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52cc26ca7c7db86be00075dcdd39ac364339c0b462edc529c21f17187fb878c2 -size 54642 +oid sha256:fd0e7f92b0a307db669342b7880f14fbc2adc0b70a6cd7445771af95a9386f9e +size 52397 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png index cceb41304b0..b145bb8019d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52cc26ca7c7db86be00075dcdd39ac364339c0b462edc529c21f17187fb878c2 -size 54642 +oid sha256:fd0e7f92b0a307db669342b7880f14fbc2adc0b70a6cd7445771af95a9386f9e +size 52397 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png index 329f84b0d48..a72ef4b6731 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b44778596d0bc20352464b19d8ac13c1da3af8ba3bd139afb8559080e73d98a -size 47561 +oid sha256:16fc4591af9f0fc19aa2aa6d2e9d44cf87176797a8f9540605ec265744f9fb28 +size 45864 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 3b466b3aa05..02c00e06d4f 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:86dcedd7d1fb23ba10eb8b3e1c5d7f71331334cb98fb88bfa2a1fef0fffb3783 -size 44053 +oid sha256:6490f8a55b9e942384537441004193e483e48a30d8b0973273e7ece2cbaa5338 +size 44061 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 5d0f2b097c4..b3ed7af9f7a 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:bcdb036e2a7581e69d3b658d16bb26e98aa27156fbafd17e9da15fd103158299 -size 41819 +oid sha256:718353933652ec30a06b023798ba255bfca64f35562dd0412811ce68cb75bcd6 +size 41827 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png index 7cbb9c3fbe3..277f7990b21 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b71e77d68b21262c0dc0731a4bd4a62053ab94f5ae3ef7ac27b29154cfe7556e -size 56493 +oid sha256:3d5aec2ca7aa00c9a3b4a21e6589497b784d90676ffe94999efd245916de2f8d +size 54310 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png index 7cbb9c3fbe3..277f7990b21 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b71e77d68b21262c0dc0731a4bd4a62053ab94f5ae3ef7ac27b29154cfe7556e -size 56493 +oid sha256:3d5aec2ca7aa00c9a3b4a21e6589497b784d90676ffe94999efd245916de2f8d +size 54310 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png index 968c0911939..9ec14d711f0 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e8a2209d43ad778815d02edbcec99b3ced0acd0a672d792ce862e57d8284cba -size 50309 +oid sha256:56d784b2a9bd8c36845ad0d559ccb0fc38d93b281efa9f50590be375177ecc52 +size 48492 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 cd6dda7370c..d9c031190c6 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:64777bafec80f98b0b1f9b7e05ede31036032b97808d52911ddd9045d83dae58 -size 42794 +oid sha256:51bd324f466cdba9316c90aa5dfbcc9225af4e139c5adc808671fb65e370e0c9 +size 42797 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 d6a004275e9..b10a2c87fca 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:0d98ab21d3ae8099bd4f458c6422cf8d91a7405b45b1a41cc91fef78ccaf9475 -size 40658 +oid sha256:de5132949c86834c08fb49e8124b3326e64827297abe8ad9befb5073e2f06ab0 +size 40661 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png index cceb41304b0..b145bb8019d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52cc26ca7c7db86be00075dcdd39ac364339c0b462edc529c21f17187fb878c2 -size 54642 +oid sha256:fd0e7f92b0a307db669342b7880f14fbc2adc0b70a6cd7445771af95a9386f9e +size 52397 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png index cceb41304b0..b145bb8019d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52cc26ca7c7db86be00075dcdd39ac364339c0b462edc529c21f17187fb878c2 -size 54642 +oid sha256:fd0e7f92b0a307db669342b7880f14fbc2adc0b70a6cd7445771af95a9386f9e +size 52397 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png index 329f84b0d48..a72ef4b6731 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b44778596d0bc20352464b19d8ac13c1da3af8ba3bd139afb8559080e73d98a -size 47561 +oid sha256:16fc4591af9f0fc19aa2aa6d2e9d44cf87176797a8f9540605ec265744f9fb28 +size 45864 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png index c023c84990b..287773510d2 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e74c751eff5f2b168503e6f47bdab6fd57d9e88deabd469e1c39507734955d7 -size 55945 +oid sha256:fbad1b9aeb93d87860738982345be39484cd5fdc8cc6dd2772f5b21580d7f75f +size 55827 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png index 0b421d70580..df299655806 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4330b8ebfff0bff2e5bbcbb358141040bd6b786a07eeb821dc2ba2ff24e0dbd3 -size 54974 +oid sha256:5e7c5fdb23b2e6406e07cc518f03e6e49acae68f7b91dc2626ea7c34e1875a57 +size 54829 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en.png new file mode 100644 index 00000000000..46adaa89d96 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5de23f1ff2742d0a29297be486b6944dd64a3906cbf0902358473ed91fe8b422 +size 22882 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en.png new file mode 100644 index 00000000000..0ae1afb0c30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0365c048f02b698af5762c873a9e93ebe82712e7321d467c2f2bb1cbc105516b +size 22702 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSection_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSection_Day_0_en.png index 9625abff26c..0912c0fa579 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSection_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSection_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8f47f89b71826b87e2f6b1de8a325710f67da4c342a2fed10a253546f3b4658 -size 15303 +oid sha256:f4fdb9a9167e7088c7568b067ba2e02c5d5d953c5868f864c0c83c1bed485138 +size 15184 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSection_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSection_Night_0_en.png index 64841c0408b..ed871cda831 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSection_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileHeaderSection_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0bed8702030f9431a281c16cbf515821be63a49a5f25d7899eee2491f1804ca -size 15405 +oid sha256:28bb644a494d60947b4d66b859ce0cc678f31f93fee4f88aa28cb8e8d17dafd1 +size 15231 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png index 7abc6e9d3f2..f8f5af06643 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6312b78386b2fef7ba615af754688053c3ab9abcdb441fbb824307694e8af99f -size 28945 +oid sha256:b2f56793a5859517f951d4b595023e44983a2ecb574cbe5408f72655969b5ee1 +size 24085 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png index 35fec3118e0..5d1451f6622 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cc6276ebb7179bfd9c8fe56716f286d57967c1e4d75e7265bef04e990ad32fa -size 26945 +oid sha256:cc74b230023d824504a5ce250e0abc4d9e93fd3d692902568b76248442fbc599 +size 21988 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_2_en.png index 3d1c83858cb..a78a122bd46 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6697aa15bb8f4c8381c018a167947464b3153f98e0a3a0a3763c1ef87b6444d4 -size 24247 +oid sha256:204a2f11595cbd3fe467d6ced7d1999139d87f88bafe0fe80becba7deafcab3c +size 24127 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_3_en.png index d7361017949..817ecc3560f 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecdb50791124422670f3ddcf18143379b42a6e14e72bd8eb7625e2c54452c7c2 -size 36472 +oid sha256:19e7fa4a5c2f61cc878798d73ae12e6b7c47694c55698b383cba7c296b110a5d +size 36448 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_4_en.png index 184bca3fdc0..553aec17b1d 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f970162cb615188c0d4f9841d521ff8e8190d8f3343c96715b6ed311669e803b -size 30433 +oid sha256:666b6910502871fd340571cb6503b0e056454849411abebd667c2189d6891758 +size 27808 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_5_en.png index fdb53559c87..d078159df04 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:503932aeb8ee7ad412e7fa87e0794597aa6564998f280b88f66257431ab8550c -size 22473 +oid sha256:af9da2f03f67ab130babec49617cf919c116ab6a2ec8ea9545b44bdd6a299e45 +size 22414 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png index 4203c40a0b4..623e88860bc 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ff5e674b594c87a4ac6c1d1f296a1225d251e8a5882521ec02880dced28981a -size 26545 +oid sha256:4dcf82f9aa2ce550dff4bf0dc4b2786b52434641f61687a73dcba99c9421efa1 +size 22974 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png index 6d597104366..8ee163709a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd5a1deaa6f393b9cced7404a4a6ead9ffc346e8a08428fb9ae76a1a3244d400 -size 29916 +oid sha256:941b691799b124e724c350ff11d7cbd67be75859938fcb91c59e1f88216d8ee4 +size 25074 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 new file mode 100644 index 00000000000..191c4ba0bf7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea7b82e4b8df9e11e14a7be70368640ce04dd74319814a2ad99663901bebf9ba +size 34904 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_9_en.png new file mode 100644 index 00000000000..0e67124f430 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76b688c758b4485634a5b5e8b350982aec21dac454cc9af68920d80c44cf8143 +size 31647 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png index 20083453f5d..14cc9719ed8 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da14f6bed942b8a6e464e07ec6d29891749ca8093f5cda20cc7e304205cfce26 -size 28022 +oid sha256:86169e0e7694b00d1c6f924775377470f54b3cb343844c6bdd060ca0c499b8ae +size 23452 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png index 1306d4e00b3..e09867ca421 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a066ee27ec789680aca81ae3e43a67e7d96d6320f59da9b5e131d07ccc7b3dd3 -size 26046 +oid sha256:758b5c1df83fb5b241396268a73b019b28d0616e5f37bd501c42400683ffd583 +size 21384 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_2_en.png index ea22c51e8d0..c4fe92d160e 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9642867b66bdb1005802a5314e03ea33dd859aed63a882a974f71a3491fadb2 -size 23329 +oid sha256:3f0dbe3e9ded805f3cd4356abb5ce4c892a57cfdeff35a450607c493294f9080 +size 23261 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_3_en.png index cac048bfc08..3128baba4a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de705fbfc801cb60742ae5fb2369e21904745e6bfb1b1c4cdb991383a03dc6bf -size 33684 +oid sha256:dcffa0153086f13d3c306813b308021e9c737915284aa1bc6e6528d1c1d20e10 +size 33619 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_4_en.png index b7c99317274..72631875241 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:707670f3d2b717f8e5426a7f01baed95fa659e865e7b811a38ff4e03d184fdf2 -size 27884 +oid sha256:61aa562bfeeb701f22520827cc57a3019744e1036128e2c7486cada54a7f403f +size 25435 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_5_en.png index 06f67e89cc6..d768ce89c73 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5493e7c638101253d4a43b957a5b96975ab99baa76bca5eecb65332d3c0b801 -size 21830 +oid sha256:3fd9cc072584a210d41ae55c1c5c02d011b4c1afe94802ca4e8f807cdab069ec +size 21804 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png index 467ddef32c9..6b47c5752c1 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66be60cf12dc3ee2f5eb8c7341759504ed890058d93d2085c9df87ae04cf5f20 -size 24357 +oid sha256:8d889f6a0aa62800b92d2b259a1661e9d599ea9df0156a8ca1b55d859fe37a6c +size 21098 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png index 5234341ca36..e3492707187 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:138b17fb048934acb059487edc542ef007fbd4c80bcc958dddf1f933b66340b9 -size 28892 +oid sha256:b482ea511c9a60a35785ab480e6a495f5f45b0e7c96e72c95fbec08ec2824f48 +size 24289 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 new file mode 100644 index 00000000000..8e0f1c80948 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76825478ae3c24522f197c2040467338a1fafb596b46596f8a6bc299808d72b6 +size 33450 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_9_en.png new file mode 100644 index 00000000000..19e7038d2e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd81d819a37f2f5d4217ffeecb98bc7ca4c2a07ff043e8fc12a9f17682358044 +size 30601 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png index 06fb695f999..e3185fcbb61 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f18cc4f6c04a59e1a5d51a091efe66bfa7525e2fe229302174fe40fefa00cc28 -size 14026 +oid sha256:35c925ad33a54942643d723ccf8f8f258688c91779f2dac6141102cc11d0e48e +size 14022 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png index eaa3b34f512..d3bd7850dfe 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d82fea268b5a9271497a3b6518a88c757e6ba6f62f4a8990491351509383858a -size 13974 +oid sha256:8e697173ada83d646673ca72c3731ae73267766828842d90b627d9efb4e0c59a +size 13973 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png index 8928f958307..6e3fdb2bd3e 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f781e977784d8bfb1bd84947519f31e28106cdc036c6dedc406be92fdbbc0c54 -size 40077 +oid sha256:1da0edef645c5abc391f0425e3a1f8719c1f061f66517b131a81cdd59d2bd04a +size 40105 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en.png new file mode 100644 index 00000000000..53b828b23e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8634747fdeca8b754396dd32e02c36956017860118dbc33098aaa10fee358ec +size 28876 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en.png new file mode 100644 index 00000000000..88d6b6c4358 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66af8c7679f62fede7b09d6661f0b78d7264a2a415cdf5faf0928aa115adfc2b +size 24514 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en.png new file mode 100644 index 00000000000..03a91ddf873 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c89798843bd3bc2a02094e200ddfc4fcdad8e71554d64c3f0cf2bfc75e894dc +size 24514 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en.png new file mode 100644 index 00000000000..03a91ddf873 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c89798843bd3bc2a02094e200ddfc4fcdad8e71554d64c3f0cf2bfc75e894dc +size 24514 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png index f55850de1d2..6e3fdb2bd3e 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bea78fb1bb813bedce30e5b13257892bcc23a7d6eb1a404d24e764337568d6cd -size 41596 +oid sha256:1da0edef645c5abc391f0425e3a1f8719c1f061f66517b131a81cdd59d2bd04a +size 40105 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png index 3161e69ab72..6cd8aaacec8 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c238637a0a3107cfcd98230ac52fad7779d8c260b0b97bf30d231cd5493a944a -size 46453 +oid sha256:8ebb02ae3454f3cbb28e3aac886d088888251d25ba2c5e8fb7adde700f10383e +size 36323 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png index 21f135beca2..d7df699feb1 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d8163be5e84aa851df9666822a4d9bc7a40d3a99d6fa8498243c256b729d58f -size 44724 +oid sha256:682472baf2a1bc5f99cfadc3177615167f1f7531923ec34374f59e263b50d79f +size 37049 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png index 7e3f0d4fb78..7c87a4d5146 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a25f85925a38d02a65af4fb342949e9545d3e5e19012885f2c3aee4caa8b8f7d -size 31535 +oid sha256:f8aa70b0d5eace900fc8c58674d2f3cdcf409335a33e8cf3eea5f60fb495d009 +size 32299 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png index 77fe855b76f..6ec6fd79b06 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81c559a9661b3fdccc8deb444b897f9a45b33de0d4d58367ff021f92202a17b6 -size 21883 +oid sha256:88f616a97acfe8c98705843ebbe5222d8d7068c64345a63594bff10b27b56e0b +size 46485 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png index 1c316959ed7..888bb98b6a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3340a2d29e6c1d86f5ec5664179cae9501fcc95381311174d7d6b45b15af326 -size 24123 +oid sha256:79fe88714a2e3d9a738de0e457220170716adf88e85eeef8e3a460facacac8ee +size 46823 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png index 1c316959ed7..7217061a087 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3340a2d29e6c1d86f5ec5664179cae9501fcc95381311174d7d6b45b15af326 -size 24123 +oid sha256:0e42383bf5dda6a4511cb915c40f76fee54670158c848d4ee1b125c57eea290b +size 40166 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png new file mode 100644 index 00000000000..5d6ce8a8176 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef4fa260df2194e4deefd4e3d9d57779afcbbc6cd56ddb4d699afa6fbac94cd8 +size 40502 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 new file mode 100644 index 00000000000..7a8af23c304 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a599d61446db401ad156139758917b54f702d765d24f20fce246674e6dd05cc3 +size 31555 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png index 22a0da43533..848a72bbd51 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68b70ae5220e244acdfa3f1a2e36089c1994e8f05eeb6c344b4734858540e55c -size 38942 +oid sha256:4087f776842690767225ab62c5c1d7717cf039f4ec53494f834cbed9d1cc21c5 +size 38971 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en.png new file mode 100644 index 00000000000..8a110274ca7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:227c4049433b734d591eeea0cb5cca65ce27b3f4fe385eb224ec322148865f6b +size 28321 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en.png new file mode 100644 index 00000000000..f86af6507de --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0393e0549647a0fe6ebef2372d9e6885db7225d137e44d4057e3fe0a15b1aa8 +size 23895 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en.png new file mode 100644 index 00000000000..8ecf2388f58 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb51fe5a51969f768609a5e5eaf5baa95005b094bf1995c3b304f637bf58fbc7 +size 24251 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en.png new file mode 100644 index 00000000000..8ecf2388f58 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb51fe5a51969f768609a5e5eaf5baa95005b094bf1995c3b304f637bf58fbc7 +size 24251 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png index 4731f5a2979..848a72bbd51 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db8b048d11f766a8e3db28bac99f3e8dea34e6f37fba1eacd9e6f98f6127462b -size 40383 +oid sha256:4087f776842690767225ab62c5c1d7717cf039f4ec53494f834cbed9d1cc21c5 +size 38971 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png index 21dbb7d45de..8d074c872f4 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc4cbb2bea7749bda9f6c5647ff178717711bad5b1ee25621155bccb1e5f2328 -size 45528 +oid sha256:31d4e4cbf57e53a5bf5e63daf0b1926d39dc67d2859a436fe1eb548830897ee3 +size 35532 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png index 4a519862f33..142cf5e2fc2 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1098ef994ad9552aca2f9ad9efe7b1239133544e561e73eced6fa96d08eaa3b -size 43785 +oid sha256:67f80eb2f896018c000419a2d57f3eb198951dafb60fa5e6e57dadd70170e711 +size 36091 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png index 9768e763ee8..f2049210adb 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ff1487c8c63a476a570d271f7a0b00c8990e1f90d49a2cae5056a9cc7e51e0e -size 30765 +oid sha256:6b753111cd6ca0f030222d97ebbbeb5deb9f3b9d64d82dd8970116a3913be06f +size 31615 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png index 77082fb4bd6..8382812e130 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8565e90948aa18f04e1b868467a007d68fe3d18d534289d5c4d59d4b38915585 -size 21190 +oid sha256:fddc479ceef787049b0e4af3ce61f6efbf67f7dccd81c3d6c17fcc7cea9c1127 +size 45383 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png index 1c46f8eb533..a37c7355fdd 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6c7e8cdf40bdf018931635565ac649fed518140a047a71cf70cf63e9edf54d3 -size 23932 +oid sha256:16bcb5d1d66aa3b5bb8e574e67f47e87c4beee9cc8795c4ac24ea68c57d32dbb +size 45750 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png index 1c46f8eb533..07b3b6b8227 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6c7e8cdf40bdf018931635565ac649fed518140a047a71cf70cf63e9edf54d3 -size 23932 +oid sha256:8f99c623fc13c336ee3138741275a4fd42677d92d8357e37d6b9ae4cb75b22e1 +size 39450 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png new file mode 100644 index 00000000000..c5a310b668a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8be233702d5b478db8358053da7cfc3712028f1b0b31287b839cb2a2dab3866 +size 39831 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 new file mode 100644 index 00000000000..b0e8833345c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:093b5ba8be120cf9f93cd3448aac328c1a8ba191d45e48f514f12879e9d3d5ae +size 30702 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en.png new file mode 100644 index 00000000000..66b44846eb9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf4643bb5ac46e4e13725c6f333b23a9c5a8d3c54d8f2eecfae9811b7237d87c +size 31303 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en.png new file mode 100644 index 00000000000..7f95a4dd263 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6f9f5dceb1e65cc382ef9a2f52f771635d390779f45304b7a7caeeb9e30d51c +size 27879 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en.png new file mode 100644 index 00000000000..6e507eb825e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb1651647db82174f84e0e166f234e9e397df90fff77dd5f254fe1a18ed23d4e +size 25226 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_12_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_12_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_13_en.png new file mode 100644 index 00000000000..3212a9557bf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87343c744ceb706cc5ab7d1391f914b0020a21e5e12cb3d8d6af536c1f0f71f4 +size 4017 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en.png new file mode 100644 index 00000000000..98a0ff76e4a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebc36a702cc711e490f33912735a90f5cd85b1702c352931cd2f91d6598252bb +size 33166 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en.png new file mode 100644 index 00000000000..bf8a3e39279 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d9f27a4db5f7ddc46d1564b16c7efba385ba1c4015473639b34d65875e7f37d +size 23186 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en.png new file mode 100644 index 00000000000..16a8ed358fb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:266f19ba9cc406a67db3ce12b7cdac15452ac8f0945ceccf8e7b9b247a50d227 +size 20367 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en.png new file mode 100644 index 00000000000..6ec6fd79b06 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88f616a97acfe8c98705843ebbe5222d8d7068c64345a63594bff10b27b56e0b +size 46485 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en.png new file mode 100644 index 00000000000..888bb98b6a1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79fe88714a2e3d9a738de0e457220170716adf88e85eeef8e3a460facacac8ee +size 46823 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en.png new file mode 100644 index 00000000000..7217061a087 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e42383bf5dda6a4511cb915c40f76fee54670158c848d4ee1b125c57eea290b +size 40166 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en.png new file mode 100644 index 00000000000..8cb2aed49e7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7adcf41fe721cf1bdff61850881590bdcb32cff258343c24eb4e44df7dab734 +size 24621 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en.png new file mode 100644 index 00000000000..3a600054830 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:460e39d03c679b6575f37df00a7616ca37f4936c951018fc4bde4a6687175e45 +size 19185 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 new file mode 100644 index 00000000000..7a8af23c304 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a599d61446db401ad156139758917b54f702d765d24f20fce246674e6dd05cc3 +size 31555 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en.png new file mode 100644 index 00000000000..a01d276dd7d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fe86ca7a863bb66a6abe6febcc1ee4eec188d57a18f76dd973f3dad00911691 +size 30570 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en.png new file mode 100644 index 00000000000..7674d030026 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68fffe7c273430425bcf00ed18174c47023e114d9df8108f0681c61cb88ee6fc +size 27229 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en.png new file mode 100644 index 00000000000..4e0f525fec7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd887339bef515c0403052338579d5885d6f0f2c31aa3ce511521a97841f3dc4 +size 24531 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_12_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_12_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_13_en.png new file mode 100644 index 00000000000..850faebafaa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6ec71c6d10cfffbffb89364967f721762040da16b129da701eccb5568aa414e +size 3999 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en.png new file mode 100644 index 00000000000..122bfaee2f1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3129b8b649e5fb26cf559cce754a84788c186acee1e201989a3f405a8a4c71ac +size 32354 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en.png new file mode 100644 index 00000000000..5e7f4fa0b38 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90a6b06c59eb121a265f33299a97360e3a81c29daafc850c65f658cd4ffde99a +size 22695 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en.png new file mode 100644 index 00000000000..a8397081112 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84f577862d06ccb4f5a4c3ae9d95574dfc884f52ba08e121708c6833b417d130 +size 20033 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en.png new file mode 100644 index 00000000000..8382812e130 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fddc479ceef787049b0e4af3ce61f6efbf67f7dccd81c3d6c17fcc7cea9c1127 +size 45383 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en.png new file mode 100644 index 00000000000..a37c7355fdd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16bcb5d1d66aa3b5bb8e574e67f47e87c4beee9cc8795c4ac24ea68c57d32dbb +size 45750 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en.png new file mode 100644 index 00000000000..07b3b6b8227 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f99c623fc13c336ee3138741275a4fd42677d92d8357e37d6b9ae4cb75b22e1 +size 39450 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en.png new file mode 100644 index 00000000000..25f198a9e57 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b41f5b0620537d0e31e5bad7a6e763fc3efeb0cd75bf64ef3d283b14b5d5ba49 +size 24271 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en.png new file mode 100644 index 00000000000..656663db73e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de7e6aa8b417026b6a344955b478d823cc0118b450020d1cb81b6f0cbeaa1c4e +size 18606 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 new file mode 100644 index 00000000000..b0e8833345c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:093b5ba8be120cf9f93cd3448aac328c1a8ba191d45e48f514f12879e9d3d5ae +size 30702 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png deleted file mode 100644 index b8a509c4acd..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4945b44bec1854dd46110f4b5763c76bd194c37291c16a56f3c6781459c41f66 -size 34319 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png deleted file mode 100644 index 538c7a315cf..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae818c5880f0dcd878be8f7a6b231754175e765a40d3b29152f86e2a669294b3 -size 33692 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en.png deleted file mode 100644 index c13d05d4b38..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:104d1d386398aac789420babf33755e03d163f40ae596cd8d24f35da0363fac6 -size 30952 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png deleted file mode 100644 index 2f77b586a2b..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:145c85570217621cc576f305ed72e27205381c438e434205e07b7363cfd67f04 -size 30069 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png deleted file mode 100644 index 3161e69ab72..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c238637a0a3107cfcd98230ac52fad7779d8c260b0b97bf30d231cd5493a944a -size 46453 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png deleted file mode 100644 index 21f135beca2..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d8163be5e84aa851df9666822a4d9bc7a40d3a99d6fa8498243c256b729d58f -size 44724 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png deleted file mode 100644 index 6a6ac768f06..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:781d869dff205f99d5a9bc9986abbe489bef24551e6fb695280541741895a508 -size 24238 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png deleted file mode 100644 index 1227b0b07ca..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0cc2e51b4c09de738bc8380977541b9af379f0e21e06c4f1dd923fd36382e6bd -size 19247 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png deleted file mode 100644 index 7e3f0d4fb78..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a25f85925a38d02a65af4fb342949e9545d3e5e19012885f2c3aee4caa8b8f7d -size 31535 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png deleted file mode 100644 index b8a509c4acd..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4945b44bec1854dd46110f4b5763c76bd194c37291c16a56f3c6781459c41f66 -size 34319 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png deleted file mode 100644 index d69f43432cf..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a6c4f45cf28006db62e03512ecf936439c109d5aad556f8e57d9fdda1c11158b -size 29507 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png deleted file mode 100644 index 4479a394603..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cd68268fbc334f92f66413c8f63d9013468f6fa95d5b6264fdfe52e50637399e -size 26627 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png deleted file mode 100644 index 673f2c39000..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1ea8c46e3f364d562680ad78ac20e0f2f143ad6663d6996c4a68888c927ebcef -size 33275 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png deleted file mode 100644 index 64da5c67153..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:69f8bbc627a85c5bb4f385f54345a7b9bec8d4f8cae4174c337ec3e6600bbf64 -size 32074 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en.png deleted file mode 100644 index 36c7cdb8598..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:85b92d23ab2f690c3c15ded7f14c513371cd36482fbeda1478f62c82650f1829 -size 30274 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png deleted file mode 100644 index 49a47a746b9..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b848d50320f1494c9d0edbc21bddcc773de0cf9b1c36ce034235fd3fbbd77cf1 -size 29210 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png deleted file mode 100644 index 21dbb7d45de..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc4cbb2bea7749bda9f6c5647ff178717711bad5b1ee25621155bccb1e5f2328 -size 45528 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png deleted file mode 100644 index 4a519862f33..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c1098ef994ad9552aca2f9ad9efe7b1239133544e561e73eced6fa96d08eaa3b -size 43785 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png deleted file mode 100644 index 7ff37748eaa..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5931dfac0337cf856282d9c62983aa185fe208f2926868097238d5b95e222fe6 -size 23953 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png deleted file mode 100644 index 0a3366f74a2..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:68af84c43882c7512d1bb162820f9bbe23c80a3e6d11bc964c945b27dfe25688 -size 18699 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png deleted file mode 100644 index 9768e763ee8..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9ff1487c8c63a476a570d271f7a0b00c8990e1f90d49a2cae5056a9cc7e51e0e -size 30765 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png deleted file mode 100644 index 673f2c39000..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1ea8c46e3f364d562680ad78ac20e0f2f143ad6663d6996c4a68888c927ebcef -size 33275 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png deleted file mode 100644 index f3174f0bb96..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:965ae432e1a931f5e93311415a7cbd9103aa763326ad8ad4811a9aa28ce08e9e -size 28612 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png deleted file mode 100644 index 89268c76ed8..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee39cc4f450e906a2e11ddb13ce4913ca442fa9bca8c95a7a50a9e51fbe2b3f6 -size 25981 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png new file mode 100644 index 00000000000..d7219a131db --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:896715e04f98bd79679081093c3e46321d3e22f0b9e45bb620fe7c28c8e9bf73 +size 12672 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png new file mode 100644 index 00000000000..327cd1193a8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fcfb26e81944f1ccfe9144593dbcb5ac2fd6341ce99ac0b268a42e5c435cef7 +size 12287 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_0_en.png index 8d1adda551d..b5c96b8b9e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c4ac3560405d81ef71d6f7760a20a97c1ec72b94368bd9483140214286c8abf -size 8690 +oid sha256:36e770c5541901f71bc1ab36b8f5834eb5c153bd04a425f3bd66602b01127999 +size 8696 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_1_en.png index 8d1adda551d..b5c96b8b9e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c4ac3560405d81ef71d6f7760a20a97c1ec72b94368bd9483140214286c8abf -size 8690 +oid sha256:36e770c5541901f71bc1ab36b8f5834eb5c153bd04a425f3bd66602b01127999 +size 8696 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_2_en.png index 5b48d235ee8..8afc50222c3 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d35b0ceda263f3464de9dc0ece9513a3a99315c340ad6135882d394bcbf1408d -size 8752 +oid sha256:275a93f0e159a25edcdeaed39ade77625e49362576f390a29755bde3cc2aabcc +size 8756 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_3_en.png index 1f6e30f11b0..a3f9596e641 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1af55c651012be63341d343182328fed1469ece044ccb856e1a0bb0592e7bee -size 8838 +oid sha256:26530260df099d67a2e83eb6bd0a2a8721fd8b351332bed5732d03434e2b2ff8 +size 8844 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_4_en.png index 522b66ba16b..3394dfdfd74 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f9a34a83de4070df4843b840fc1965336c9aeea5bc5bb38ee711858c8862c9d -size 67586 +oid sha256:786a9213e15ce5f22c9fdb40bf965950ba49fc49cccd276795418d5fea3a1de0 +size 67588 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_5_en.png index 334543384a0..aa98c8b9b5c 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b637778795940cf95d95815bd603654f90e64d189e1a28310482a6186f1c074 -size 62442 +oid sha256:52e8f53b21042763ed8d70ea64e2c62ba91d77b8cdce93f8ab98aa55907e25cd +size 62450 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_0_en.png index ce61707708f..8f12c0d3be6 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd93e06fa2bf8830f949c4ec8ba0e9f8517631c296458b1844e3e63316ceca34 -size 8450 +oid sha256:535741bad671da1f9e40450929de940dac571c61476598a2a18846f3706303dd +size 8447 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_1_en.png index ce61707708f..8f12c0d3be6 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd93e06fa2bf8830f949c4ec8ba0e9f8517631c296458b1844e3e63316ceca34 -size 8450 +oid sha256:535741bad671da1f9e40450929de940dac571c61476598a2a18846f3706303dd +size 8447 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_2_en.png index e10d219861e..6098021d9c9 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f060da4dc65cf19efca327c9cb0abd1492ffc3400c40d3bfd119f88de415f77 -size 8488 +oid sha256:f5d1255a7153721ee534b0835427314550b25f8ada24b1c980be728b510953a1 +size 8486 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_3_en.png index 2936de38a73..f8e156e0cc9 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67b6eba0661ba9430c6f10e7b80b9e85cfd8e54d8ad81429afe16fa5c093a663 -size 8563 +oid sha256:0829175ad954b648a5ae8e0ab6e8a278d36238e61a9b9c9b33f25049883fe0b7 +size 8559 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_4_en.png index aea8cc80923..2c136ac0346 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e69c2178527ed63fcbfe8e4b26489cc7a503516c45febff8be2ca69fb060a5c8 -size 68685 +oid sha256:214d0520c2e4368c0168683c7bc4f79833292dd2613673196ce7fe6eb3eb5272 +size 68686 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_5_en.png index 2213edbb3f1..8672740a766 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.file_ViewFileView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9fa814e0d1b4c204f495d85a50f7cfea3b7f269127fa1978a9275a2cc17918a -size 62460 +oid sha256:06ab6982439f0e450b421ab4866a4282c76732a0f6b20d15fa76163d5a181b3b +size 62462 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png index 8761e14e801..dabb9f6e079 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8dc926ba5eb40d3eac7b25ff1aaecbe253625bd4088150feeb2f27aab0ab823c -size 9455 +oid sha256:294aad181c287d8c723311e4110f736866004f05f79f44531447b25cde1dd504 +size 9640 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png index bd55857b048..f54fde0d36d 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f55b0a9d592a6c6270fe7464a4be4c75d50142f9261d5e54bd4334dad6dd8d5f -size 9237 +oid sha256:df7252bdf7da5155b94ea29bf404f2bfdbde0909800b908f288341e90b3b17d5 +size 9386 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en.png index f12cb438d21..aa6c71936b5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a2a171ac0edac9cb734d418b7baf68dd294d9feb93f005cf445420a7538a8ed -size 7687 +oid sha256:49f6848dbff23f8c906894ee852253c051807c942f968ba3856e361aeae5c2ca +size 8683 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en.png index cd66fcf3c86..4661a04e609 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56508c5827cfda4c672f924e3e9280659b729b772455c65dbae76ba7815e18dd -size 7502 +oid sha256:d341142ffd67a41fee3b2f41146011caeee07b9ec3085d9c47131d1a5d68683a +size 8002 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Day_0_en.png new file mode 100644 index 00000000000..66519c4bf98 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19afe8458f9146aa4256e3ede12fc673e8605782db5522760ede31f1f81ddd9f +size 6969 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Night_0_en.png new file mode 100644 index 00000000000..876f53004f5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38a0beda65338d6486ab5e6c8afeea1b8871bce396d900a184769d9a2da495cd +size 7026 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Day_0_en.png index 9d132c42ab6..af747bd8cac 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9117e79573ac322172166be26d38c19640e93e4969da1b674d24f0b0311fa605 -size 6408 +oid sha256:60d8a652022f45141e8c1f0e0c93f6b748c863300947af218475010daba1c4ff +size 6413 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Night_0_en.png index 8b62cfa34ab..fd2c53c422d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b58b59809e44d16777dd743b62ea751198651e2ff3eb9364cfc78385d1df61cf -size 6451 +oid sha256:e35828ef24ee2aa6aea5446026dd933ff130c2a2fa03d20dea30306f9a22a688 +size 6474 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Day_0_en.png index b3b844d2aa4..663b8532911 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:396c5d4c0bbeefa607f84305333b7893365f419c64e40b82f02d06f535a174ee -size 6465 +oid sha256:d00a43e53ca9720e51aa6571249669e1fd27f84ae8b8d0d8ba5b08a894952698 +size 6476 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Night_0_en.png index 9e319bc452d..36393d3de05 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c26925681bd888998c5dafac638c163541fa2de4ced757d0535a86583447c91f -size 6432 +oid sha256:285b12c0c5695dbb6477564d60afa099300e35e32f29abbb3fa33c03e7c9e60a +size 6431 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Day_0_en.png index 768278c042e..fa5bc467ae1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a022f780c01ca2c2f7ce2b9990c7347c336c9f5cd40bfe1e8afb597c571f6948 -size 6081 +oid sha256:6df8ee3c6c8303c0b4626af59578ad9c3e432162c8adee963b15deffd2d8223e +size 6035 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Night_0_en.png index 6e7f0f12f47..331f7b3066c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4c2a60758bb05c4c152adc7a88027bc636aef0dfbd1731b7643b1aa81cf2d8b -size 5885 +oid sha256:a2e9aca91645e401a475e5eddc64f958cd24964cb2d87dd820c299646d5457c4 +size 5847 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_RoundedIconAtom_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_RoundedIconAtom_Day_0_en.png index 46213fb6b58..1184ddff0b0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_RoundedIconAtom_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_RoundedIconAtom_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67ff7c9588cc49baa0ffeb2620ac5c5b430db14b9f141cb91464c07d469e36f7 -size 4989 +oid sha256:49b57e4a662e7ef8a4add3d3559a10cc479d1a7438c208ee48507fc7a48d4383 +size 4967 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_RoundedIconAtom_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_RoundedIconAtom_Night_0_en.png index 6f3db773542..d20e3bc8ec1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_RoundedIconAtom_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_RoundedIconAtom_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c8fec3cfb705c222d102f241653bf6b2f8727946bce1595dc2aa7ac97473e9a -size 5107 +oid sha256:ccb6628c5e4279eab88ed59e4f290635c29f0c3435ae773986c10fbc25e4777b +size 5097 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en.png index efa4a70bf38..157f3e59e23 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:267bd9c09da1732222fc833e302b06c2107a2964524e8f98f5fdd7d2884543a5 -size 20808 +oid sha256:ff4fbcc142dac5a501eabb228009663eed497a2d30d8e29e0fe68b462a82997d +size 20369 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en.png index 311ce5e56b9..5338f9a2de3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35499aea0d8741002ee546553ac0ec8fee64abeb93db8913e585804ed28539fc -size 23451 +oid sha256:570d33be676b5a603f13c1beb364d41e8c19a6d1c501c3082cc197cd26344d4e +size 22546 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Day_0_en.png index dcf1fee93e7..6d68f88fbeb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a294fb3b53fe70fcb467e4fce0eca65cac7f4cd5d4d2b716a29640657a742fb -size 8012 +oid sha256:abf9eb7d942bd3dc77bc66fa3c4146b4d6d6d37a01175b3ba66e499492f9bd94 +size 8043 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Night_0_en.png index db61fa9c4fe..f0e7d1071d8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b4046958f40f92f1f98feeb638cec8d13f0c3cc1378c6983ae982a15a404845 -size 7957 +oid sha256:865fb15b9fa2b2d65204301a049eb508619b62932059f5adb54ba9dcfc5eb9c7 +size 7986 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en.png index 515255346da..b807f029dc0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7647c37f89ad8c258bb655777817e6db501a95c2a03354bee54d4728b7977e08 -size 17300 +oid sha256:d4e6027ed8a8c11bc294add70bb67986fc52c70a51a3410e35f6bab2a8a71a9a +size 17211 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en.png index c8d51382ffa..ece67b62f49 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fa12ea5b9ed4c01758712e46812462ab54c1621eaa1b7f0e03dbaf9f577fd81 -size 16677 +oid sha256:7201f6fb1368435a944b07a557a595509afa01d70cafa9ec0f4d8ff46db95482 +size 16521 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Day_0_en.png index ebbb0394e49..b0dfbeac58e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14340ba4aa7dc363800268c0ee149e6e296df72dedbd88d52da98cd7afe89d41 -size 11191 +oid sha256:e1a914e347b12af99ecc4783f03bee4491574d3475f9f7485f1055d84965f762 +size 11182 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Night_0_en.png index 65805965a5e..8a008409a4f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93682f1dec9f6d53b7fed92c3048968737bd6b9e42a58bad57cb04ceafc28af3 -size 10950 +oid sha256:c4a445ccc0c6e580eb0a2edf5f1c98b91d222e1dc3fa6d3d00088cf0114368f3 +size 10946 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en.png index da19681c0e6..ca528f64d83 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:273c0d2074133acfe00f08a5a9ac4ebfc9c0303fc482508b79884f8875d88415 -size 15516 +oid sha256:57cbf6294c2eed21f53d79adccceb2cc066c391a5ab1df76e39a1a5336318789 +size 15604 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en.png index c18a0244ab1..27411ac3b8e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a284db87dc2404a8f2d99d829b9cd9882eb46a2ec49e3d26a9323355279d96c -size 15100 +oid sha256:d28135979475db962fd91c63ed2a7f6d2cf24232b14755f961812a55c0ec4587 +size 15207 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en.png new file mode 100644 index 00000000000..20de46a8820 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bedf319e44da65199c96bb1a503d3a4c961e91eb5132553b50aa13cacfc5ceaf +size 12519 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en.png new file mode 100644 index 00000000000..2cb5df9203a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4ca72f4c73d4d9baca7cf8fbf6a7a0984bb9681d5fc252c7797634aeef05998 +size 12308 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPage_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPage_Day_0_en.png index f3637fd6ad3..20de46a8820 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPage_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPage_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f21517e6cefc67709164b03b6bf64765702532d9de4a4ca837b3b586a4125b10 -size 12458 +oid sha256:bedf319e44da65199c96bb1a503d3a4c961e91eb5132553b50aa13cacfc5ceaf +size 12519 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPage_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPage_Night_0_en.png index 24f77666b08..2cb5df9203a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPage_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_HeaderFooterPage_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c32ca21a6b99ea3b3aa05713247b2338af98f6830180dfab6d18902baa23d4ea -size 12274 +oid sha256:e4ca72f4c73d4d9baca7cf8fbf6a7a0984bb9681d5fc252c7797634aeef05998 +size 12308 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en.png new file mode 100644 index 00000000000..53b5d638de7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c7a7b64a83f68670c0c38eac2782ad5121fe18638c6a21f3b868d0cb4820c7c +size 59391 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_SpaceAvatar_Avatars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_SpaceAvatar_Avatars_en.png new file mode 100644 index 00000000000..30ccebfb08d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_SpaceAvatar_Avatars_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57545d6b9690b892859f7bf2d2aa92d3fdc1a3ae7b4849e39cf6df10bb0dbbb4 +size 8719 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_TextAvatar_Avatars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_TextAvatar_Avatars_en.png new file mode 100644 index 00000000000..48fa2bbb5ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_TextAvatar_Avatars_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:def7dc550efea75745153ad0cf71c7d92905205549965ba1f192ee3612cec9c4 +size 8041 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_TombstonedRoomAvatar_Avatars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_TombstonedRoomAvatar_Avatars_en.png new file mode 100644 index 00000000000..c812b2a7147 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_TombstonedRoomAvatar_Avatars_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50abd66f2c58d8cc18a96c73120a779882cd121049e473646518ff6841e25e30 +size 5106 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_UserAvatarColors_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_UserAvatarColors_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_UserAvatarColors_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_UserAvatarColors_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_UserAvatarColors_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_UserAvatarColors_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_UserAvatarColors_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_UserAvatarColors_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_11_en.png index 0cf3a95fce2..808d016ec81 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e44ac29c970ddab492a1b9550315a82f983b88474287165ba732e57951647a81 -size 19112 +oid sha256:5689b4b10c7225b8eb44b8573a9e7378b2d2673439b24a4d85351d9d7ef6273d +size 19162 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_14_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_14_en.png index 37040e69bc0..746e5221a6a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_14_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:498e1b23a60c563387bbe734fc4ffd3896824d128e1213b194b344091058ae8a -size 21480 +oid sha256:18b2140a2d22c4187d9f338c5d8516b65de738c4ae640b6870f49f0991860e66 +size 21485 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_17_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_17_en.png index eab29dd2b2e..e57ce0c2975 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_17_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:206f721ba79222702409f10e70e671e00d855e192387b2fc73dec93bc721765f -size 20933 +oid sha256:cd82839d55fcf1869595428b42b5a7add88199f5c877d6b5dc651c0ad5db2dfb +size 20942 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_20_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_20_en.png index 714308dfa45..922f4bad6d8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_20_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_20_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e48c08ee8ff50b9b03bf6af20dfce17db88865b0d1b6db9cbb210d79b8177168 -size 24574 +oid sha256:4c0edbcdb326e027f692feebfa253b23acca3b9a38aa9e2d32ec901afd3ee071 +size 24599 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_23_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_23_en.png index 113f1cc2728..f993d1f0bf7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_23_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_23_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64ba8cfebc5697bacedd498a508fe9f95491cb58128cbbbb39fc5a363b56c652 -size 16609 +oid sha256:f25815e696b4df2eab0d6553d8eee57ba2f845a6a959b94f958dbb92817f1696 +size 16611 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_26_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_26_en.png index 4e9d8edab88..257cc10c7cf 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_26_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_26_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38ea2bd258563803f089ed79d897b2832d7b8e3440025bd60b6f3156ec0536ee -size 20690 +oid sha256:c003933d1fc60b69f01e71d0457fafc1808ab1b77473638be45145ad89150747 +size 20697 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_29_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_29_en.png index 02297f29ee4..7d02982c4e4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_29_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_29_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cdac51735a02d6dd9f03a70937cf2a8c307bb8ef8636c6a136c389646cb2e67 -size 21150 +oid sha256:9ee35f103492b0b0fe4a2ad86f74f4d0283993ea63db039d034430bd4343e0bb +size 21160 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_2_en.png index 61a74b28d1a..aababf615fa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a38ee88772bb214f0b31c7338efc17e081dbe75b2123449305b6e62d406af1a1 -size 18359 +oid sha256:b030edd664a333f1ea2360ebcee1533c42f7abd3e96f54994afc760c5be4dc4e +size 18333 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_32_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_32_en.png index 4e6adf5ceaa..4152a5d16d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_32_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_32_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd5e60140ca1468932b81d2c708ce98066e1f4fcc0cd3a315986379ce1e66670 -size 21426 +oid sha256:191b2a9f3a8f449921f12acf72bfd58af0e85dec442dfcd81e04e342d5874e46 +size 21416 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_35_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_35_en.png index 1814093696e..b40107e0ffc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_35_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_35_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32df3f3250f88aa77fea3278630d9f19abd45950211d520fd985969e933c7207 -size 16291 +oid sha256:f5de3ed181520441851573e3ddf3e72c8c952189a9c7b418e3e566114c6f02a7 +size 16270 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_38_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_38_en.png index 21606b47a9d..d7014e2f205 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_38_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_38_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3672bb10f51f674dda4d06eced8f5a6e977bd10902d4de7bf9cc4e915d83bb7 -size 16836 +oid sha256:28003c233a356aa2409f60ed44daca0f4d3c1fb1a8689547d4e9f7f3a031163c +size 16812 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_41_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_41_en.png index 881a35582b9..1c64a0f698e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_41_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_41_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e56fff6052b5c1b3b61a2aa73cad2f03881d86d465dbc59465d63c4aeac3ef4 -size 16371 +oid sha256:8b25313908ca35b61f8e79ed5e05cecd7ab69050de96c027bd4b0945dc68c5c9 +size 16374 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png index 0565f214737..5e81d1fc12b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe30a0d96effe257973c893b6450a357d49f11e0f4743b2fdb16050fc15b3a8f -size 17549 +oid sha256:c2fcd26a3cb1486cf98d21df006334d997796d2e1329278bfb92097388cbe040 +size 17525 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png index ee7f8a89cc3..5fa7b054a7b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96518c010196d80e8696ffcaea032468bb4d1edfbe6b1187286da488600363bf -size 17891 +oid sha256:c7ad48821a6fced07afc3169a100c483a04067a86100b5cef1a10bfdea7727a3 +size 17875 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png index 83586a0c514..218c5643371 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dd0232c7847d5c0bb1047cfc6be5d4d43d2cc2e9347aff70a793103b511ad98 -size 21073 +oid sha256:19f08f3652a32b3b931c9ff1fa33f7d2c0b695279fc3acdbdb03767c98be57ac +size 21045 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png index 4a3262b6ac1..0c7a297bd31 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1570611e0a894a88644956558629113717217ce4b7a76c615264bc2fa776616 -size 19763 +oid sha256:68cf5fb4245120f669a4b7a6cc4c306334fd61272e5b5ed927a6c6a414060296 +size 19803 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png index b87579d7309..8001a93c5c8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68cb86e7966e17d3240de09ba69aa4af36e4b07c1b669b5003af8071d6eb1ee1 -size 13832 +oid sha256:496e055ab9bd30730cd8bf1b7f753a5f08017387b26afedd91339baedb19b40f +size 13831 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png index 06eb0f06a4e..86164b6cdaa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e196a6ccf55855e8cad09e9934e09c8564cad08b4dcbc418c4a2b8d467088c3 -size 22891 +oid sha256:9cff65483f3ba03ec504b5c5a4d4e219dfb7e928819ed5172f85ee8a4c7a16a3 +size 22868 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_5_en.png index 98c17002368..36795d6b65e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5214f4d8366a00fca152708c65a265d1a56312d929439f41db5c3920f063f7ad -size 30095 +oid sha256:6989cce071211332a3b12d7a58aeab6d6c1c4c3b744fb8d4186e9e812ed96073 +size 30067 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png index bc0c7cda4e6..9f6a98b8776 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:746b7181d7f9a3c25040624a2c20378b1eff1ce299840c6f4b4b9ca3f877bdfd -size 24976 +oid sha256:73d24101d6100c9c0a538cca0d5dd35fd83e6f7291c828963d23ca98378b7db3 +size 24950 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png index dd81493a761..cc0be0fbbed 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67a8223f107fa05f1c5d66efc85f68325b4bf835e371ad44553bd6d6edd4a201 -size 18491 +oid sha256:c816d7ccf93dbf21f45508d7905450f10c6af01b13b923ec7568eeb1f5e2bb21 +size 18466 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png index c1712835ff3..30698aa052f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be8c830118af26d3d39a80d5dfa3a750e6e3830a30b066ca91b7ed305a2ee482 -size 23624 +oid sha256:855337a22ffae7779ec645572f28c13e453ec19bac975e51c390b97648882aa4 +size 23631 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png index 0b8c15e54c4..0ad9637d5fe 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:672f829a7227921b7179e91c1ee3ef58ff2b7dab7f131b687c4fe0b9862d2a2c -size 19436 +oid sha256:bca7ca1e5153f33b81542ecaa5800bb14bafcae4ee5398e64485f080078552c7 +size 19442 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png index fc23c2c5add..496b7bc706a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f3a7f0a4944f35fd726d5090eab08140b57332e6f7d8eb475fc6bf9ef37bcdf -size 26033 +oid sha256:0ead78f918f3123488ec2d59d09babd8443eb10b88c8d15f6e4aafc8817c0de3 +size 26063 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png index b294ff8e75d..904325ecd09 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e88e6991bca9f0c99c28b6126b10135d0ba8de1faa7bc6174e6f66bc11b2ce03 -size 16794 +oid sha256:832084f232a204cd52913c85f7228422bca373feb63a36be419049591bb83a72 +size 16773 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png index 1698660c8eb..74cbc8dc97f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4979794c700bc8bab1bc767cc2387ce3be28b9d2c0b6da0696b237445ce7df95 -size 21225 +oid sha256:ce55aa9fd5c963d4e485bef493da6ac4b0cb434ef91f576fa1b4711617782dcc +size 21270 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png index 72c687d4395..045a15738af 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:491cb82462df122b0e8d21c5b70e8db8ac19c28aaee1289db6f4774e7d31d53f -size 19556 +oid sha256:6102197c0eda927f879482f07abc2a6fafe1e688b57855c30e69bc7564ee8301 +size 19538 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png index bd51d8c202c..5d0a47c6579 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bef7c3d043454a4faf858456c7fcc98d1a17a0846a891af3332e76c4e10b553 -size 17102 +oid sha256:ba3e873c2243c44c0b15393d26f7911f61965f4d1cc690f314df6c0b28739306 +size 17075 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_87_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_87_en.png new file mode 100644 index 00000000000..1f283638d5d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_87_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1104fc4c6acc07da2aa9fcc7e15a95a3ba1263f7bf976040af068724ec7caee6 +size 20708 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_88_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_88_en.png new file mode 100644 index 00000000000..562db54d92d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_88_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2249dc7c9627f50d8895208c79e22760aa2bc484939bfc648f2fbcffc20bbca +size 19239 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_89_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_89_en.png new file mode 100644 index 00000000000..70b32144d1b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_89_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47fc297f230f9ff3d6bbb0ee295ca17443313836fc2910735288a94bbb41e923 +size 24442 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_8_en.png index 18efb9a84bb..00472c5c5f4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42ac664be2f0ed05b1fc844947556b256c823c5bc1bce380cceb1fb50ecd751f -size 25028 +oid sha256:2fa4309841b5093d542e4d5496f3209fa70d4cafad35d69cbb72d7dc1292b1b1 +size 25055 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_90_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_90_en.png new file mode 100644 index 00000000000..79872e52442 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_90_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3667c472d9832e0d25996889ec0887685dfac049c043a6c8a448789a5f7c6909 +size 17200 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_91_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_91_en.png new file mode 100644 index 00000000000..b630ea26fc3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_91_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f96f824d47ed90dd4fee4cd64b807bc49dd273510d6cd2072b2bf5cdaf740dd +size 15943 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png new file mode 100644 index 00000000000..b459e2de73f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90c5321decf7bf505f94312611bbf76d35725f758ff2f32ebfd321f7c172449e +size 20382 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_CompositeAvatar_Avatars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_CompositeAvatar_Avatars_en.png deleted file mode 100644 index d14745b7836..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_CompositeAvatar_Avatars_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:374b810a6ef7011ec1270b7d4faddc8feacef10d167a5b2fc2c920d87f7d8e2c -size 27831 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en.png index cb09d01ce9f..afc6b91e2c7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e51f7c0e57fbdbeea27962cf3ebf58ead3a5689251eced57f2a37cb4373c9858 -size 7057 +oid sha256:867a27b40625fe6d0c214c3a4bea0a09baf298aebb53d3ff77e19b3dfe250414 +size 10393 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en.png index bef7df4b6fd..d291068fbe5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd6fd55c333b87c619189f4b0e1b1b0616f65982b322e03e271a59eaaf1a649b -size 6967 +oid sha256:df3a081fb057accffb4179c86f46cb05b033962c0339a96767b68610b9df9ecc +size 10313 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en.png index 7a6b59b2899..be39302a17e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6eb030d6d2f1c80cb7dea618c3a47db2f7db7b26ad19b3ba9313c0535ab7a761 -size 6533 +oid sha256:c43a92d9ca48521da56819a405585a96255b302a517832b7375870752489dacb +size 10516 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en.png index 0b7e398df04..7e813bdae98 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b955337a438e1c29cd75961dadc489d14f726f7524281f4368fac1c1cfcb58d6 -size 6512 +oid sha256:82e2d283d97af1db75bacef222976be6e4f646d5606e9f4aa95fa15b81bb0898 +size 10414 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_MainActionButton_Buttons_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_MainActionButton_Buttons_en.png index 31b2ae723a9..d0fe56b36a9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_MainActionButton_Buttons_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_MainActionButton_Buttons_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48b55e3d045eff0f2129394ea64bb5bc4962278a8feeb13323f5eed09d95596f -size 22300 +oid sha256:18a7b8395fe7791689d7cc028bd1b73d5b0a90bbcd10f5f8cc8e7542e36ba0bd +size 22136 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en.png index c9207657645..031080449d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24d042c0c4e9f9d01c5937d42cf16d05ad24e2d8cfd8026038126531b8304cd4 -size 30195 +oid sha256:4a35c5690e291b30e85491485dca6203d3541bae8aa6e680f53d5480b312b4fa +size 30859 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialog_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialog_Day_0_en.png index bce67de2238..08268d09c42 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialog_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialog_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:390153008f59d4915895498d3d6fa3354502f3f3a902b3ae5e53045aec27432d -size 18116 +oid sha256:6bf99a794b62bcac4356d40c621e541cc8b4819851be94ee4fb5f990821dad43 +size 18696 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialog_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialog_Night_0_en.png index 7de57649543..f0edea1109a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialog_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_ListDialog_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ad10053328a529721e5f6564e493e4d0ea1d9e4e098867f1a4daa938396d712 -size 16273 +oid sha256:9ac05090a322d3b3eb9bc6a8975a3f832e15099553186b9054036807b6d16124 +size 16719 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en.png new file mode 100644 index 00000000000..c937b35d4f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:869c9e0b2dd0ea776b99ab4e223c2e8c31f62a2ea9f7c2aa7c06423eb9bfa531 +size 18638 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en.png new file mode 100644 index 00000000000..3865c87243d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ca9146ee4758956ef5487e198352e836ed354a0c427f854780db2d3977621d9 +size 16738 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en.png new file mode 100644 index 00000000000..8c14f92fb06 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7874f4a2365bed4de566c8db579d8a456d298da8c913bbdabfd7b66ba2d99d9 +size 13342 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en.png new file mode 100644 index 00000000000..4712e3e1eab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:271f172f1c5cc924426afa2ae51f295c10cf51602600b46cac812a2973b6e68d +size 11694 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_MutipleSelectionListItemSelectedTrailingContent_Multiple_selection_List_item_-_selection_in_trailing_content_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_MutipleSelectionListItemSelectedTrailingContent_Multiple_selection_List_item_-_selection_in_trailing_content_List_items_en.png index a1159b8751e..d5fce4935b7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_MutipleSelectionListItemSelectedTrailingContent_Multiple_selection_List_item_-_selection_in_trailing_content_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_MutipleSelectionListItemSelectedTrailingContent_Multiple_selection_List_item_-_selection_in_trailing_content_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca5c467bf99999ed1c6e0ce74bf514038d06bb42d002ad3b70ed610e16d47882 -size 13368 +oid sha256:86026dc287d57a56a897eb7a86c52ddea30595d48ee2859b6e86a0791b6ad4e5 +size 13361 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_SingleSelectionListItemCustomFormattert_Single_selection_List_item_-_custom_formatter_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_SingleSelectionListItemCustomFormattert_Single_selection_List_item_-_custom_formatter_List_items_en.png index 7820dd8c144..0ff7e07e978 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_SingleSelectionListItemCustomFormattert_Single_selection_List_item_-_custom_formatter_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_SingleSelectionListItemCustomFormattert_Single_selection_List_item_-_custom_formatter_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c008e5dedfab58be89953dfe34da1b75961f61a538f785d5c56eff9c9778390 -size 17874 +oid sha256:430fdba6eaaa61187e351a73c09530e54962e1dbf1e455124f3f55c6e9abb1a8 +size 18259 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_SingleSelectionListItemSelectedInTrailingContent_Single_selection_List_item_-_selection_in_trailing_content_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_SingleSelectionListItemSelectedInTrailingContent_Single_selection_List_item_-_selection_in_trailing_content_List_items_en.png index 7711847d7cd..c6e24ab6aaa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_SingleSelectionListItemSelectedInTrailingContent_Single_selection_List_item_-_selection_in_trailing_content_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_SingleSelectionListItemSelectedInTrailingContent_Single_selection_List_item_-_selection_in_trailing_content_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d438d36942250c60ede8576302626c5153abd57b4978d7d562bd71b53bb0062 -size 15702 +oid sha256:efcb74c5572ab5eda93592a74680db56b2e265e7c9ac5e7f4a8ca29e12f83779 +size 15798 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItemEmpty_Text_field_List_item_-_empty_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItemEmpty_Text_field_List_item_-_empty_List_items_en.png index f232126cfb9..fac015d66dd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItemEmpty_Text_field_List_item_-_empty_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItemEmpty_Text_field_List_item_-_empty_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73978f15c708b9bb39f20aaaf2e1510caa796b8875bd1418d0ce1e6a99f60ad6 -size 8907 +oid sha256:798d2a617d7d5cdad84a3e39a81e83c9de1d5b84bfd35f54fd3a1e2271bdc411 +size 9277 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItemTextFieldValue_Text_field_List_item_-_textfieldvalue_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItemTextFieldValue_Text_field_List_item_-_textfieldvalue_List_items_en.png index c619f9a53ab..dd5b34fcd54 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItemTextFieldValue_Text_field_List_item_-_textfieldvalue_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItemTextFieldValue_Text_field_List_item_-_textfieldvalue_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b02a6c58c1a54f4e86b7b5db36c925ccc9a2012dc30372736216d489d2a8e33e -size 9819 +oid sha256:ddbf6a82c11d7dd55140c65677a5c96d53e09ad08c6a65313575a1d9b73744fa +size 10207 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItem_Text_field_List_item_-_text_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItem_Text_field_List_item_-_text_List_items_en.png index bc29b0cfa45..7b92100afd6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItem_Text_field_List_item_-_text_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.list_TextFieldListItem_Text_field_List_item_-_text_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f25810294b831c73b678d93a0352c4d2e84e946ee197680be40074145a4ba5ab -size 6104 +oid sha256:3427ed512414c9e6bdfb90db3caf29dacb8373878e56992eff260506d128c4bf +size 6564 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en.png index 782e3296fa6..f78e51c2bb6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce590684f178ab35d9a6d7ef36d98b39411b4ad9665d8367d527a6dbeee25191 -size 28806 +oid sha256:2681f08233461baaa5c73b5ea2469f62d8140b2e8672c69dda9aabae4b06763b +size 25071 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCheckbox_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCheckbox_Preferences_en.png index 51871d20969..0b9797572d8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCheckbox_Preferences_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCheckbox_Preferences_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc52445bf4196bb91d137b43e01d1ba547191a15ea4f61b09e2b7cbb323821df -size 64870 +oid sha256:a35098196001777bb3585a3367ad172786e5c0da7896ae1af7913df43900a918 +size 65667 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceDropdown_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceDropdown_Preferences_en.png index 2ec789f4483..c0f19893104 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceDropdown_Preferences_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceDropdown_Preferences_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca9a1d1bb7d4de0b8a5f5efd62cde248f0c9e07b88b2fbdbc598d922003a7f02 -size 31032 +oid sha256:fcf2f8879bdf6c1957b3654f201959846f5086606689107fef5c6f52e5b9e500 +size 31024 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Day_0_en.png index 71960c0346a..1686f961f89 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8db18d040380375d1f5e4b55990354c04d8a9be00775111aecfe32d09160925b -size 26318 +oid sha256:00b9249fcec16ceea7e9d9c17d180209fcf4523cec51661aaaf3d09b9e8904ed +size 22272 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Night_0_en.png index b1b6a52951a..3bb438a0dbb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24521601a033d7cf9ff165af3452b037db6bd7bb877c78adbc66a31ff81d24e9 -size 25486 +oid sha256:f2fa5ff5eef1ef2aedce9c48e38a19dfa2330e2fce70ce9d3b98a6ae2aeb60e0 +size 21567 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceSlide_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceSlide_Preferences_en.png index 7e76795ad08..9c3c44251df 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceSlide_Preferences_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceSlide_Preferences_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a035426d751113cfad1cb77522ad9c7cad37caccb310b013ebe6d9bf95e23ccd -size 23779 +oid sha256:10dc0842de8797bff611336c90358d602dfd5613a7e4beb7bb68a85065d1d360 +size 23730 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceSwitch_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceSwitch_Preferences_en.png index a6ae052e1a5..13864c32f2c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceSwitch_Preferences_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceSwitch_Preferences_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13e77179a17c1ae2587fd9a109787dd7ebea58837c318aeeed00bbeb28a70dca -size 36610 +oid sha256:d300a5570ff8ee77d141bc8a625ccf93266193ad8b32e0ce5894edcf68faa852 +size 35979 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextDark_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextDark_Preferences_en.png deleted file mode 100644 index e4c9c82c3cf..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextDark_Preferences_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b6432b9d7e491fd647898a8c1adfe65dff5141494ceb6801ca17dfd3c908f262 -size 34450 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextLight_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextLight_Preferences_en.png deleted file mode 100644 index dd33b70f5de..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextLight_Preferences_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:53a1978d02b8229ba18a75b0c8e05d9a9aa4934815a82b0da865100fd38bfb10 -size 34934 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeDark_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeDark_Preferences_en.png deleted file mode 100644 index e591a037b4d..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeDark_Preferences_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d072307f919575d403399dfe8b31ee13a9821495565bd9450768411cd837408 -size 36943 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeLight_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeLight_Preferences_en.png deleted file mode 100644 index 84e35bc1453..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeLight_Preferences_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7832924770983aecd3a81d01569ef74c6d3d2f92fb19efb23449ebf619152ad9 -size 37057 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Announcement_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Announcement_Day_0_en.png index 343694057f8..cd8b8fa3920 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Announcement_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Announcement_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:297a71fec7335294de0cdd9100e64454102711ff76e1df7a5f1e32e719310ad4 -size 27062 +oid sha256:c9f0bb344018e468ba59e29d355e5e5ff292e4bbf4198f0fdcf0e993ffd6cd51 +size 27057 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Announcement_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Announcement_Night_0_en.png index 87012bc30eb..b5681c3aab8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Announcement_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Announcement_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c986e1494b1c09606b12cf68270c54bda48034caea60dd911cac6cd976f4dc5 -size 26082 +oid sha256:a72cd475113e46f030a18413ec0973ae9357f54b40ac5508fa6edff1125d0466 +size 26094 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Badge_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Badge_Day_0_en.png index 768278c042e..fa5bc467ae1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Badge_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Badge_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a022f780c01ca2c2f7ce2b9990c7347c336c9f5cd40bfe1e8afb597c571f6948 -size 6081 +oid sha256:6df8ee3c6c8303c0b4626af59578ad9c3e432162c8adee963b15deffd2d8223e +size 6035 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Badge_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Badge_Night_0_en.png index 6e7f0f12f47..331f7b3066c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Badge_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Badge_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4c2a60758bb05c4c152adc7a88027bc636aef0dfbd1731b7643b1aa81cf2d8b -size 5885 +oid sha256:a2e9aca91645e401a475e5eddc64f958cd24964cb2d87dd820c299646d5457c4 +size 5847 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigCheckmark_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigCheckmark_Day_0_en.png index abe90d96b74..7c3b23587d2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigCheckmark_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigCheckmark_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:203f7ee722f1dcc9b16d0bf242d7ffb68fbbd1e850e38d09c759dd45b1baeb71 -size 8565 +oid sha256:6940c5b62364ff8a41b9600af5de950888d4050642a175d0bb13867e332fd360 +size 8573 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigCheckmark_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigCheckmark_Night_0_en.png index 7c8925a175a..53be504da64 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigCheckmark_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigCheckmark_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd5f27868d2eb8740fc84e1be39ed511fb89278adfd6232789a8abfcd7d6f40c -size 7214 +oid sha256:5e71365efdbcbc0f0ad59e4be7d806a46ca72469f6b7f73a7c5d698405f637da +size 7217 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png index 0d92b4ca6dd..96355854791 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bed157ced6cb695c6c92f15bc8b31b124fb943f86db580c9ab2db33d423b731 -size 12408 +oid sha256:1fed22f221c1087ffb066b950918924cfc4af1618bb7724c57fa27d4cfe50340 +size 14089 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png index 36f2db136ed..08a56eb13c3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:211c37c03a828d65c2d28622ebb7eee49d39941f80aca809698561c55ae12135 -size 12521 +oid sha256:b028b36b64fecede982569046613903a8df32e7f95243a0eb4c25e89205c141f +size 14199 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Day_0_en.png index 084aff9841b..cc57e03bb9a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77004b5d420cc5fe51f98b66033ed0d68cb9c308104fcdc281a0f06a6ece0fed -size 46964 +oid sha256:9d91be86dc5a346ccc9a9dcefbfd04fd229c5ae8367c8d94e6b99c7e4c48a34d +size 47007 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Night_0_en.png index 601c55c1b38..3c5397e73e2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3467dc3bb6ed59048297d12badde6ab1748c19ff755809d4a14cf47e6c81ed9b -size 50973 +oid sha256:d409e87405152caf5d2f277a513aa3535d5aa22d5308fe21ee3749ecc77579b6 +size 50933 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ExpandableBottomSheetLayout_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ExpandableBottomSheetLayout_en.png new file mode 100644 index 00000000000..f8f1c5da7c1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_ExpandableBottomSheetLayout_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f33d7b77bbda44a0d044a25b59dbc99a2289bd56884992e1a859743408595b15 +size 16489 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_1_en.png index cd257db2343..345e8ca6437 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1de76205c501b936eb87d2fae6dad8b4a1f89581ae912505cb877efd8be9ffa -size 12309 +oid sha256:56aabf6f359235c77b7bffa3d7e2c4ebf7b2c688eaf9e2d8a67da9a00433ebcb +size 12350 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_2_en.png index 81234de2608..110210ee92a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6f0733b08b3abf440efb9fe78b7ade728cc65ffc97697761b612b8fca017cf8 -size 12897 +oid sha256:058f539d2f7652342394adb681084147dcc2d4a1e24f9c8efd47781a0def4c33 +size 12925 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_4_en.png index d51d5098231..6e54ed1bffb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0ae9f53b675f2f754f7cb3ebf3fbe45ae7eae0c63bc8628425e0bf21ff95bcb -size 12485 +oid sha256:eb1c80dfc5926f3aa29ab5118f8c5a6a62ab13a1247e198e32f00509723cd4e3 +size 12550 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_5_en.png index b7c3ab2d68c..54ac4972d60 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a9d956826399b4a700a4f5d05eed66412f76f59a21a0995a85c69b3c528803a -size 13124 +oid sha256:61c83e013e73d55a6b3504f34dcdb0111744499172839a71c35ada67cece1184 +size 13162 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_6_en.png new file mode 100644 index 00000000000..0684f2bcaf4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d81aade2945be977896038addacdb9246d17bb1a57fb3bd0d43169e382ce9f03 +size 13126 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_1_en.png index 7246bfbf85b..1517ba7210b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2408b8d5d390737c5bf7a8410d609c727aaee6addaaa4d9e5b8fc85858835978 -size 12151 +oid sha256:316d70aa6507a9b0c5ab9833d750033df44f8197ca3f25d7f6acbfc496b68d59 +size 12184 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_2_en.png index 6ef037c10d6..b3ac22f3946 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cea402df09e13b10c31cf57149197af2b5bc36a1bb41a09f66d3bb2cd3617fb -size 12996 +oid sha256:d07c5029da082dcce7b86a202ea611e089ddb1e379d2511395f75dfabdedd5d3 +size 13034 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_4_en.png index 316ef83354e..aeeae19771b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8debba5d7b2f5866dafbb553732d5f99e3ecade1e3da873abdf2a82856f6b835 -size 12249 +oid sha256:d4871583b442845d3ae8e3dd98f4099fb7906f2ccccf25b61aad59efb8112c62 +size 12259 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_5_en.png index 0f1ab6cc74d..3b86fa73ada 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe20c0a4b18c1844df6d962e147a5e939cf0cba70657a67f3286c013942dd010 -size 13068 +oid sha256:0d2d9b68cff6f00bf12eed5df4123af7da908ae3e25e710e5ce305f36b404382 +size 13101 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_6_en.png new file mode 100644 index 00000000000..7e7a23ed38b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:610e68016011b14db56217ebcd27a86ab597c1668a3a90b09c3e39ac308f8764 +size 13054 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconMinimal_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconMinimal_Day_0_en.png index db9da7ae831..0f0769d9776 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconMinimal_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconMinimal_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c6bee86d5d0d45869a3c0737a74c03e82803f45c5e1604295c84d2cfe0b3ccf -size 7507 +oid sha256:a1638d8805c8029573b8cae9a71c3c2dddf569d5154969f49085def60ec74ec8 +size 7545 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconMinimal_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconMinimal_Night_0_en.png index dd637dbe45d..756c1aca442 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconMinimal_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconMinimal_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a90445d97819e255969aa9bf071321f4152f18875ba6cbd5e6b4532ca9c9f4c -size 7523 +oid sha256:19c89df3b96fad54d28fdd608cec70d2d8c71fb20901746abacd9bcc26fd5332 +size 7579 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_0_en.png index b3f9dda3811..dc6e722df7a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:174958b24ca5c9d1cb465444d8b869e03252c24d203d308b5bdb0681908d86cb -size 67766 +oid sha256:982b22e61e982d30ddf05f2f7e7df0de6f653e9563d8697d8d958deb272dbed1 +size 67002 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_1_en.png index 9f7de0ccccf..290b2c8ba58 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:378e2d8a4f8bacbd1b7937ffaf54078c95213d867d7a0ddb85c3ca2a367e06c5 -size 62938 +oid sha256:157aeb4a1d97133db7034252e5a3ecea2f0c0732f826a63ce6c37edc4c3c3f73 +size 67544 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_2_en.png index 447f7390bd1..91ad1bf4eb0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81ae404a24a37d8b86e61b1341d9fc4666d87dec99be6553a18037a22c3f8179 -size 72732 +oid sha256:af91b96c2163cbbbf01111f4d875dcb9662c07369cdb3c456cd86178e0173e6c +size 72302 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_3_en.png index 37fafa3eba8..90932cbd1c9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c87f241efd4cdc2c983674dd19fecc368b1aacc70f65360b69df7d7442341eba -size 74310 +oid sha256:5f5b7435cfcf0955e39ce197efaac36e6ff379777c969165aed5a069926a1d2f +size 73134 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_4_en.png index c9a507344e0..613b2d48fb2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f4b8fcee15f760864f66375a77dabf79150dabf2b7e589e2f855d94241fe4ac -size 79002 +oid sha256:c76d53e450b677ada343897ed16250b9cbb6a8e8add1b659db154dc6d954a613 +size 74765 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_5_en.png index cfb421873e9..06ce9e261fe 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91e9736a94f3ee5c82b7bef2a939503e878910abff0c930e0edc2d6f61631e19 -size 22134 +oid sha256:1b3edbf7ec81d79bf7201f32b3e8af53922dc060ebade16d5292c6fde766046f +size 42861 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_0_en.png index 3fee6d2856f..465ac95d7c5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb59beed103cd60897d2b837d09c441d9a7eab44daa3061863274556619e7b1e -size 64862 +oid sha256:c3893d921c761fe7c2b5df25e319d59f50e6fc621395906b6fe712521a436eae +size 64098 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_1_en.png index 0199f2ebf02..04b76daf144 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55f3de8981fc43f7c674200304a0da0b30ea0562c6bebf9ce2997b50fe680d1c -size 60295 +oid sha256:83811b6295d5714633e33bd5627e5001eded8306dfdc0e460f1f95cf2f30e1cb +size 64613 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_2_en.png index 399f07d303f..a63c57dc8aa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f935a0511a16dba0b2aa974334f84a50ceed06a82ecb13b911c8419aca4407fb -size 69558 +oid sha256:be70c02875df9a7cd94eaaa7d34ac2de72ebca1214515dda9725dcb923be8a69 +size 69369 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_3_en.png index 064d35c1b16..8f214157870 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0be6bcfe4f8b6df47a314c82411c1625c71605f257f5bc4cfb1ccfbf638f445 -size 71267 +oid sha256:edc05f93374f0b7a95b132f2f892dc193d514ff2c6de32846eee6e55210075f4 +size 70461 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_4_en.png index 9a76db86061..a0063834c8b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a3732d0ed75d211bf0827f5433acdaf64c3915b96c03343c76018fff7f88dc6 -size 76111 +oid sha256:1ee05334852ae63186189e2705d59c8732405b3f042e47a0146729f2bc000000 +size 71966 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_5_en.png index 627bb3b7aac..84d8ce1f85f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsCompound_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc64e2dd35081ef8dbec7bbeb0a904e2556fa2dcd753acf73c16af06e55b77a6 -size 21524 +oid sha256:d9b068e03c1a383d98e71baef9759e33d4fe22b3cf8a664202b463b8b7be1d39 +size 40886 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Day_0_en.png index ca94a884e3a..65bc8d2f203 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:560c9159e78e9da940da58a4297aca1ac647218fab0e03c532016ac96a3a560d -size 21524 +oid sha256:b5e45da502ee63629d7a0ca130ce92bdc33e2c93baf49f50594fc8831771c305 +size 13760 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Night_0_en.png index 6793e1a4051..13c6908f5a9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0bb37fb7f6dbce206288431c59e3cc1b31d1a5a25d73b2f2321d7d46a459da5 -size 20631 +oid sha256:4fb44869e4f1d3233d316e814b5d88a94177b8bdae9eb19186ae937641570ec2 +size 13355 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png index 37b234801f4..5555e6bb022 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea4f3ed733159e0413383d8994943b5b7b3100160728138b7014a9567f665021 -size 30632 +oid sha256:3ecb9e2af8ad8221225e71463a1da8589ff1a6c9ba393f636c2dbec08fae513b +size 30991 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png index f9577523847..897b8d763e9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:657c14f3e42751aefbac5fc0b7d94462d2b90da1707ab7ce7997f5db1b29c6d2 -size 31013 +oid sha256:f7f456872a423c0edeb87e5a9fbc7e73896f3cb75609a64c4acb959ffd77e72d +size 31404 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 d5639c42b15..01e6f56033e 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:0171a8269216d3d1d43e9f78d9616473e357127db7d1452f29aed8b2614ca350 -size 33478 +oid sha256:134b6c98eb0fa9513a50d1c272901b2881a7b1b1b4303b299f36d6dff832e9dd +size 33504 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en.png index 2206653e60e..f528f58de43 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15e3d8f6a686349045f5101490b18b9323ce114a9b2ffba492495c88e50d9be4 -size 24415 +oid sha256:a78c77da5eb9c5706bbaf306fba85e311757990b81ad5d5714a4a8ccf72adc92 +size 24411 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en.png index 74503b27068..2dc142ad759 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c12fbe16c8103e201ed682eb3dd01c680ce2c97fa2166dbd04ceb1627db08fda -size 23957 +oid sha256:0328504a77a8d9c1f5500b2d2b8f9504352c238cdded9fc82f6bac2c995293df +size 23940 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DropdownMenuItem_Menus_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DropdownMenuItem_Menus_en.png index 7f7f322c3a9..7f13d187d3b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DropdownMenuItem_Menus_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DropdownMenuItem_Menus_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abc6e2350ebc5ecfff7534b1febcf2cd58d37a2d90e7c594be91a595a95fbdca -size 23832 +oid sha256:0f806f3b279ec62dea1a9179a035953eab59f648fb3a5b278bf17011b2d90e03 +size 23943 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconColorButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconColorButton_Day_0_en.png new file mode 100644 index 00000000000..f81bdb10273 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconColorButton_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6489b0d11a7e9d29836d09e9f5507850d1a978c6810ec19861823a17c6c9d94 +size 12550 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconColorButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconColorButton_Night_0_en.png new file mode 100644 index 00000000000..7858b1f1b91 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconColorButton_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:673097fbe509f075a9c7e48f72766e5153cbcfb49643eb1f49636d5d27431e0f +size 12147 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconToggleButton_Toggles_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconToggleButton_Toggles_en.png index 9e1b6c5638f..857de41abd9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconToggleButton_Toggles_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_IconToggleButton_Toggles_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e18c366b09ccb25e0d8a00f43ecbf5afef9d2ba5ab6b3e406c945ff75a2e666 -size 11055 +oid sha256:9b4dd0b39488e2a13f7efb8c3369f81abd8b5d77093d4ecacfa14bdadc31987b +size 10983 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineBothIcons_List_item_(1_line)_-_Both_Icons_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineBothIcons_List_item_(1_line)_-_Both_Icons_List_items_en.png index b23cf2ad926..559bf7fb1c9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineBothIcons_List_item_(1_line)_-_Both_Icons_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineBothIcons_List_item_(1_line)_-_Both_Icons_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:994c387ed14b17e4aecca6d357876733c3d956b210b7c451db8197b627c56112 -size 16573 +oid sha256:21d389ff0e3456ba514c1408b86f211a3c9d4760dcc4dbe2c1feddd7805b74f4 +size 16672 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingCheckbox_List_item_(1_line)_-_Leading_Checkbox_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingCheckbox_List_item_(1_line)_-_Leading_Checkbox_List_items_en.png index 0cc0061903c..b474eb20204 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingCheckbox_List_item_(1_line)_-_Leading_Checkbox_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingCheckbox_List_item_(1_line)_-_Leading_Checkbox_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:931481a1fb63f4dc01f5ec120b655d6acf3b03a6feece8a8fbe0daeaebcb61cc -size 10678 +oid sha256:561127e23c0bce4281725a639c95242e120a0ff7f289ec8012294dc627af7cf4 +size 10681 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingIcon_List_item_(1_line)_-_Leading_Icon_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingIcon_List_item_(1_line)_-_Leading_Icon_List_items_en.png index a5a9027842c..9bba3d30817 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingIcon_List_item_(1_line)_-_Leading_Icon_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingIcon_List_item_(1_line)_-_Leading_Icon_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8157da65ee527b1daa34f8ef10de9c8e0bd9fd4c3a12a500a2309c43634c6f29 -size 13555 +oid sha256:cc2e8a3c8f8ef64890c66b19ac35d208eb80f84dfc6576a6d47df873b1c85b85 +size 13565 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingRadioButton_List_item_(1_line)_-_Leading_RadioButton_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingRadioButton_List_item_(1_line)_-_Leading_RadioButton_List_items_en.png index af8ca242226..350ddc194a6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingRadioButton_List_item_(1_line)_-_Leading_RadioButton_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingRadioButton_List_item_(1_line)_-_Leading_RadioButton_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef70bc4aaafa730bac6584014bc4fa6291fd5290711497bef6334e969e6959b4 -size 13250 +oid sha256:4fcbf70ce869c505e85f21458bce6e24c2241c0ab7b850681ef4c244dd42c94e +size 13135 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingSwitch_List_item_(1_line)_-_Leading_Switch_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingSwitch_List_item_(1_line)_-_Leading_Switch_List_items_en.png index 5e2672fe544..3198e70baf9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingSwitch_List_item_(1_line)_-_Leading_Switch_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineLeadingSwitch_List_item_(1_line)_-_Leading_Switch_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c105ade197c90cbdd773da3cc8082792ae305704a76e211c230f3a4fcc7d5abd -size 17909 +oid sha256:79cfec30a9109ed025bfdbf41bfafb75ec6716724f12c5f14f8b322156579e6e +size 17625 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingCheckBox_List_item_(1_line)_-_Trailing_Checkbox_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingCheckBox_List_item_(1_line)_-_Trailing_Checkbox_List_items_en.png index 4383d7cce50..5a46ccad857 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingCheckBox_List_item_(1_line)_-_Trailing_Checkbox_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingCheckBox_List_item_(1_line)_-_Trailing_Checkbox_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e36b943b5ab7ff1750d63b6ec856d341a04e249c48698d6fb606dce1cdae50c -size 10606 +oid sha256:5d922db570b982ca36125a043ba1f57e573760800066f6bfc3e61fb8d17dc8c4 +size 10611 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingIcon_List_item_(1_line)_-_Trailing_Icon_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingIcon_List_item_(1_line)_-_Trailing_Icon_List_items_en.png index 6faf127c0c2..e4f9ad3ed3a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingIcon_List_item_(1_line)_-_Trailing_Icon_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingIcon_List_item_(1_line)_-_Trailing_Icon_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52e6de1b25d89d02fa65a8ff6ca2ecec3697c5c2b3375f83dc10d021970cc5cc -size 13399 +oid sha256:ce52afd561501e9c480e81d8859ac65b31b0347c0792099d30b648df1cf9b240 +size 13273 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingRadioButton_List_item_(1_line)_-_Trailing_RadioButton_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingRadioButton_List_item_(1_line)_-_Trailing_RadioButton_List_items_en.png index e5a64ea8543..9df59f89712 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingRadioButton_List_item_(1_line)_-_Trailing_RadioButton_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingRadioButton_List_item_(1_line)_-_Trailing_RadioButton_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58f68cea7244703ee8e22a5f3ff33ec6d0959c2d695ef7a460cc675637025228 -size 13234 +oid sha256:b0ff7f9ff66f346e86e82756e403811f136a9813f22589c3cc32c1d60a71ed7f +size 13106 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingSwitch_List_item_(1_line)_-_Trailing_Switch_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingSwitch_List_item_(1_line)_-_Trailing_Switch_List_items_en.png index 497202438ef..382a773aac5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingSwitch_List_item_(1_line)_-_Trailing_Switch_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemSingleLineTrailingSwitch_List_item_(1_line)_-_Trailing_Switch_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:386b3da0825689ae023ef738d557711aee454cc73aa53872951091e0c33b780c -size 17644 +oid sha256:3dac42d73f49f877fcb6fb3c665603375d6eaca8869442868e734a15e76796da +size 17388 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesBothIcons_List_item_(3_lines)_-_Both_Icons_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesBothIcons_List_item_(3_lines)_-_Both_Icons_List_items_en.png index 6880dc5c94a..8176b202d5b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesBothIcons_List_item_(3_lines)_-_Both_Icons_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesBothIcons_List_item_(3_lines)_-_Both_Icons_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a14b583319cce4c2488148f24b2c383480d09bd38930d13a7f1e80fa28dfd246 -size 47313 +oid sha256:40e9eaf9547c920e9a33e01fad2b17fa9dd995538ed7aebd11464c92ac587719 +size 47500 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingCheckbox_List_item_(3_lines)_-_Leading_Checkbox_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingCheckbox_List_item_(3_lines)_-_Leading_Checkbox_List_items_en.png index 5ff5abefbf1..bb3acbc43b0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingCheckbox_List_item_(3_lines)_-_Leading_Checkbox_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingCheckbox_List_item_(3_lines)_-_Leading_Checkbox_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9013763c103b76796b2aaedf8fd4f470ca4372ecca8d115b59ff5f00b08f4f3 -size 41697 +oid sha256:c1548c6f1689a996a63124ffda33b3f7605de5768a4bd6f65c7b1ab469715ddf +size 41723 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingIcon_List_item_(3_lines)_-_Leading_Icon_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingIcon_List_item_(3_lines)_-_Leading_Icon_List_items_en.png index 904ecb6396b..0d065d01569 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingIcon_List_item_(3_lines)_-_Leading_Icon_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingIcon_List_item_(3_lines)_-_Leading_Icon_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b424c57dfb95bd4d2befe0e1b569bf417920a10afd7bd5bba6f5d544ae20876 -size 44500 +oid sha256:b3a7cb4a62dd27bf07258b96a181732b78df8157d29a7299d8ebfdde06434742 +size 44553 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingRadioButton_List_item_(3_lines)_-_Leading_RadioButton_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingRadioButton_List_item_(3_lines)_-_Leading_RadioButton_List_items_en.png index 50af415a4f9..3ac83a76c9a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingRadioButton_List_item_(3_lines)_-_Leading_RadioButton_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingRadioButton_List_item_(3_lines)_-_Leading_RadioButton_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7561472314ea7f56f80086897d2547e840c04425e317d62f3acb2a7959ba44e -size 44022 +oid sha256:87578f4f5f6c145f884034120d37ca72738d94570250b81920e9367e65f63710 +size 43932 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingSwitch_List_item_(3_lines)_-_Leading_Switch_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingSwitch_List_item_(3_lines)_-_Leading_Switch_List_items_en.png index a788aac61a4..be6be536003 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingSwitch_List_item_(3_lines)_-_Leading_Switch_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesLeadingSwitch_List_item_(3_lines)_-_Leading_Switch_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02fd327d87c9b3e7d4e07a665a5242432c26cd0eaedd05a645c9f44296007dcd -size 48227 +oid sha256:82c64c6bf6da748cf9b29081392296f0aeb7d9711dab834e1af62e20dbe2f8da +size 48069 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingCheckBox_List_item_(3_lines)_-_Trailing_Checkbox_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingCheckBox_List_item_(3_lines)_-_Trailing_Checkbox_List_items_en.png index 37b4420bd52..326b590e603 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingCheckBox_List_item_(3_lines)_-_Trailing_Checkbox_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingCheckBox_List_item_(3_lines)_-_Trailing_Checkbox_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3caa1ea352417833395592c2d35b0c176ee991748796850b26fefb4c22dcd6da -size 42048 +oid sha256:251cdade6428df13fb3ef77d557e4919068d60c60d81279b5e29ec6b2d92b054 +size 42064 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingIcon_List_item_(3_lines)_-_Trailing_Icon_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingIcon_List_item_(3_lines)_-_Trailing_Icon_List_items_en.png index 778124f01e2..88510eb9c0a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingIcon_List_item_(3_lines)_-_Trailing_Icon_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingIcon_List_item_(3_lines)_-_Trailing_Icon_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39d787d556d73b53e91118aff0259f52e1d0bd9a67294a7773e3ce0ca42ed687 -size 44758 +oid sha256:2234c6599f8dfbba6367a54e7aa2a9523d88b7da0c629a410581b4f6c4229932 +size 44589 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingRadioButton_List_item_(3_lines)_-_Trailing_RadioButton_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingRadioButton_List_item_(3_lines)_-_Trailing_RadioButton_List_items_en.png index 12c26dee05e..c00f37c4f0b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingRadioButton_List_item_(3_lines)_-_Trailing_RadioButton_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingRadioButton_List_item_(3_lines)_-_Trailing_RadioButton_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8d7dff8473263b10d5e717d4f0de8f0ab7f0ff9678d599823895ef3c28b8e5a -size 44285 +oid sha256:f0d5855a3c319d430945515824a337076d237dc63613181cb0cc5717776293c5 +size 44241 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingSwitch_List_item_(3_lines)_-_Trailing_Switch_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingSwitch_List_item_(3_lines)_-_Trailing_Switch_List_items_en.png index b342cad9e99..dc537aaf344 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingSwitch_List_item_(3_lines)_-_Trailing_Switch_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemThreeLinesTrailingSwitch_List_item_(3_lines)_-_Trailing_Switch_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b262c93bf96b3f1cd160ce84203489c315494d2292f3985f514b0feab280bf0 -size 48035 +oid sha256:64c674cc76cba8bdb27b416a68ae9d4ab170a17f41bc06df2e88e981b7128c69 +size 47919 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesBothIcons_List_item_(2_lines)_-_Both_Icons_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesBothIcons_List_item_(2_lines)_-_Both_Icons_List_items_en.png index f5773792d70..e6abb0f8c2f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesBothIcons_List_item_(2_lines)_-_Both_Icons_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesBothIcons_List_item_(2_lines)_-_Both_Icons_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af7b60236fa5b56d4d0267c19f2702f2f11c6a3463ac39a086b2ce3349e65290 -size 33968 +oid sha256:aaa66427225716d5e9a4b0c5ab702be447f10db93350420cc2271a43a32343ad +size 34105 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingCheckboxError_List_item_(2_lines)_-_Leading_Checkbox_-_Error_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingCheckboxError_List_item_(2_lines)_-_Leading_Checkbox_-_Error_List_items_en.png index 57762d5e58d..e09d4d86ee3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingCheckboxError_List_item_(2_lines)_-_Leading_Checkbox_-_Error_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingCheckboxError_List_item_(2_lines)_-_Leading_Checkbox_-_Error_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4d25a2d81ae68068e3c69c82178a3aedf122eea767167ac743929ddbe739aae -size 29612 +oid sha256:203a6626faf4df511645babde9e8eef76698924547c5b051e45b7a2104dc342c +size 29646 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingCheckbox_List_item_(2_lines)_-_Leading_Checkbox_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingCheckbox_List_item_(2_lines)_-_Leading_Checkbox_List_items_en.png index 4f20d64d40b..2ae474e5eaa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingCheckbox_List_item_(2_lines)_-_Leading_Checkbox_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingCheckbox_List_item_(2_lines)_-_Leading_Checkbox_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e273356ac0c1c3ba1033f31bfb04d33c82457467f680011804f6ba9b027bf620 -size 29523 +oid sha256:ad8304ad2b7f854bcd37bde6695e18fe9c6cb50cc579751101536c17113fc4b1 +size 29561 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingIcon_List_item_(2_lines)_-_Leading_Icon_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingIcon_List_item_(2_lines)_-_Leading_Icon_List_items_en.png index c5fca049fb9..3d70f6e783e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingIcon_List_item_(2_lines)_-_Leading_Icon_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingIcon_List_item_(2_lines)_-_Leading_Icon_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0671c62081c3151696ed9275f76c63b536d852d47b09016655653e7f3278266 -size 33773 +oid sha256:5cd56a68af5795c6fa82b410282680ee53eb8ab0c729f8d3321f6fe98614c0ad +size 33623 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingRadioButtonError_List_item_(2_lines)_-_Leading_RadioButton_-_Error_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingRadioButtonError_List_item_(2_lines)_-_Leading_RadioButton_-_Error_List_items_en.png index ec3a5d234e4..f888d2c0400 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingRadioButtonError_List_item_(2_lines)_-_Leading_RadioButton_-_Error_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingRadioButtonError_List_item_(2_lines)_-_Leading_RadioButton_-_Error_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b904675e4c1735ad6bb566ca8ca014532d005b21c77500f64475afe293baba74 -size 32213 +oid sha256:2c48bc9aa3aab2e897485a98c7d49291b7b3075b777f6e20503be6b06bb9256d +size 32128 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingRadioButton_List_item_(2_lines)_-_Leading_RadioButton_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingRadioButton_List_item_(2_lines)_-_Leading_RadioButton_List_items_en.png index 705b82af06d..b3ef2b68f99 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingRadioButton_List_item_(2_lines)_-_Leading_RadioButton_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingRadioButton_List_item_(2_lines)_-_Leading_RadioButton_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d21d8a2fa0ae1fee321079cc0cd752c24a2e4396cf512a8ad7164b63477b4de9 -size 32024 +oid sha256:bbfaae2c8c7c3d66d605d4bdad3e7d054ace167977aeefa6faabfa0d20efb8cb +size 31947 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingSwitchError_List_item_(2_lines)_-_Leading_Switch_-_Error_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingSwitchError_List_item_(2_lines)_-_Leading_Switch_-_Error_List_items_en.png index 9147c837dbd..4e51b798acb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingSwitchError_List_item_(2_lines)_-_Leading_Switch_-_Error_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingSwitchError_List_item_(2_lines)_-_Leading_Switch_-_Error_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ae4d1c58af99419e6f15d16e50b06d45ec8a0611137bd840f872cbb5eb347ae -size 36455 +oid sha256:a67d10e2a98b6d474ea176b94ee6a5985d3f7ebe0b74502eaa7246a8f618f1e9 +size 36282 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingSwitch_List_item_(2_lines)_-_Leading_Switch_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingSwitch_List_item_(2_lines)_-_Leading_Switch_List_items_en.png index 7283b06523f..e8a6d444f88 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingSwitch_List_item_(2_lines)_-_Leading_Switch_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesLeadingSwitch_List_item_(2_lines)_-_Leading_Switch_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee41a7e7b529a976df2c5f3e152e6b090e15291abc6a24c5117c050d070e80da -size 36072 +oid sha256:6f9f2041e2443931cddced74cf71852407db51d0846f503cc24ec852e6b2c54b +size 35947 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingCheckBoxError_List_item_(2_lines)_-_Trailing_Checkbox_-_Error_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingCheckBoxError_List_item_(2_lines)_-_Trailing_Checkbox_-_Error_List_items_en.png index b6fffa2da5e..7b2c4a05221 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingCheckBoxError_List_item_(2_lines)_-_Trailing_Checkbox_-_Error_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingCheckBoxError_List_item_(2_lines)_-_Trailing_Checkbox_-_Error_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d57a9dc6d67f26bdda0d3192ada45f41945fda7e204763538e42cada37003ea2 -size 29906 +oid sha256:08473c64d289e750a99b9a0eedb46a0e4d3e2dc4a6f1acfb39f2fc19ba8c4217 +size 29935 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingCheckBox_List_item_(2_lines)_-_Trailing_Checkbox_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingCheckBox_List_item_(2_lines)_-_Trailing_Checkbox_List_items_en.png index 47ae7db4cd2..7c0f6964494 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingCheckBox_List_item_(2_lines)_-_Trailing_Checkbox_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingCheckBox_List_item_(2_lines)_-_Trailing_Checkbox_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8ae9d48a57cd796bbb2a8d962b1094d42e07b0726232958fd45575dce5d6e42 -size 29830 +oid sha256:47a9c25c523166f10d0acf4498e9a4fd64f4f2ef125b49d2970e9e5f92a3cac1 +size 29853 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingIcon_List_item_(2_lines)_-_Trailing_Icon_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingIcon_List_item_(2_lines)_-_Trailing_Icon_List_items_en.png index 17ea1db9bea..f416d70a990 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingIcon_List_item_(2_lines)_-_Trailing_Icon_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingIcon_List_item_(2_lines)_-_Trailing_Icon_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7669ba797d31ba6a05da97afd39b408cae4bd297d026f7163ddfa77c98d9e776 -size 34073 +oid sha256:161e8085bcffa3d8b82340759224aed56099527106283c4c5b6230d92389aef3 +size 34112 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingRadioButtonError_List_item_(2_lines)_-_Trailing_RadioButton_-_Error_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingRadioButtonError_List_item_(2_lines)_-_Trailing_RadioButton_-_Error_List_items_en.png index 3cb56b25f7c..a24670b5dd3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingRadioButtonError_List_item_(2_lines)_-_Trailing_RadioButton_-_Error_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingRadioButtonError_List_item_(2_lines)_-_Trailing_RadioButton_-_Error_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64bf18409a011ad7871cec5e25034862add9e4ac2d2ac0051bd15090f11f29cc -size 32388 +oid sha256:ad0c8b79a03f82e5d3b1da8764acf7cb0a33c89b093e2b17f3f202d3ab5734c9 +size 32351 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingRadioButton_List_item_(2_lines)_-_Trailing_RadioButton_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingRadioButton_List_item_(2_lines)_-_Trailing_RadioButton_List_items_en.png index 82b6cc9a95d..5e950137a3d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingRadioButton_List_item_(2_lines)_-_Trailing_RadioButton_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingRadioButton_List_item_(2_lines)_-_Trailing_RadioButton_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:428e23a4e8074a009345b262b4b1652a4c98216353a78603f141251d6c182d43 -size 32183 +oid sha256:8ae2240ed721eee53d14afea75d8cc718ba32d428e0fba31c9e124d88f42b8e9 +size 32146 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingSwitchError_List_item_(2_lines)_-_Trailing_Switch_-_Error_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingSwitchError_List_item_(2_lines)_-_Trailing_Switch_-_Error_List_items_en.png index 08c05d41d3d..76dff084637 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingSwitchError_List_item_(2_lines)_-_Trailing_Switch_-_Error_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingSwitchError_List_item_(2_lines)_-_Trailing_Switch_-_Error_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a3871cd50b8a6aea809402de7f7a41ed3afae0da36988c4fbc3cf4639cb4b3a -size 36443 +oid sha256:5b678e9204d4b1c248add757b1562a706710c12f997458644418d34db0e02423 +size 36299 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingSwitch_List_item_(2_lines)_-_Trailing_Switch_List_items_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingSwitch_List_item_(2_lines)_-_Trailing_Switch_List_items_en.png index 7400a5ecf14..c36610fcaa7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingSwitch_List_item_(2_lines)_-_Trailing_Switch_List_items_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListItemTwoLinesTrailingSwitch_List_item_(2_lines)_-_Trailing_Switch_List_items_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6103e4db65702d046e12a460ed355f724f3fd1d07f7cba9c3d9c702d9947b3f5 -size 36030 +oid sha256:d40308160ebd69910d279499a08b88238b0288bb97ddd139e37ab4044123e573 +size 35935 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en.png index 78b3c64932e..57064572732 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ce878bbcc295d612a1e1690b9606faf6b060442ac8b8f6abe7fbcc100ab551d -size 25376 +oid sha256:494270714b162766fa38dde5f74eac67386e1dd896f041df16452b5f8601b8fa +size 25382 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_NavigationBar_App_Bars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_NavigationBar_App_Bars_en.png new file mode 100644 index 00000000000..f25072200f1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_NavigationBar_App_Bars_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0554fae92f638dcb6f0701c36f67df8126402a74a8864daf8f821cd5d3ffd29c +size 16367 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveNoneQuery_Search_views_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveNoneQuery_Search_views_en.png index d517ece48d1..78b2174e58e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveNoneQuery_Search_views_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveNoneQuery_Search_views_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a69f4b7add1abaadd967c96693d14dfff62f73705a9edeb3d1a9c32b68a01ce +oid sha256:5117f03b47d430e9a9373ad0fd90534288010745500e21e007b5b3836f10df49 size 7500 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithContent_Search_views_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithContent_Search_views_en.png index 4ede7b23516..9d790317051 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithContent_Search_views_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithContent_Search_views_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18d0edd9ffe6749b524a454bfb74373bbef52b1d0fe6e503b73523f2b84f8bea -size 23704 +oid sha256:40416d4e67f10d0399af1de88e8a4785a645475021c97c51e4888ec51746bd76 +size 23707 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en.png index 2b0f5e477df..0b416da9f67 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e37ec1481f04822c9784f3610444e36f9e2189c41aad878bb83b4570a033ac07 -size 9107 +oid sha256:25390b86c8ee47707f99e43bf566ca91a289781be2969e2aaf8b5e1ad0f36c96 +size 9113 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithQueryNoBackButton_Search_views_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithQueryNoBackButton_Search_views_en.png index 16509084c8e..d78df1fa158 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithQueryNoBackButton_Search_views_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithQueryNoBackButton_Search_views_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:014273f9262a31f01596e883dd8779b2bf8ac4ac2f609abbe582814b8eabe07b -size 6837 +oid sha256:342c3d1af1d44b912e7516de3a6aa4a25affb25bc02d0c36d0c93c90f97a96b8 +size 6839 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithQuery_Search_views_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithQuery_Search_views_en.png index b4a50169ec7..05f6e3ac884 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithQuery_Search_views_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarActiveWithQuery_Search_views_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:584f8c7eaf99a6b9bf3ff6faf65a188d23b27ce2f1888fb4c78ab166b77dddd5 -size 7169 +oid sha256:da44fbadab421c9a055fd835ccc1ec67966ec3e52940b00b5ab607f85d050684 +size 7173 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarInactive_Search_views_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarInactive_Search_views_en.png index 80de6614815..e26e65980aa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarInactive_Search_views_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_SearchBarInactive_Search_views_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db6ec7c40c6178cf8b4170dc369f14524453cd2c8101f21b70bdc80dc9aa3e34 -size 13711 +oid sha256:a1cb2f4f264b98879819fc05ad09e93fe144a9d4ff8afc546c1d6856e581e129 +size 13438 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TextFieldsDark_TextFields_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TextFieldsDark_TextFields_en.png index f0177c8a61e..c0c139308d8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TextFieldsDark_TextFields_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TextFieldsDark_TextFields_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:165bbcda11115c490250a86ac45d84277362a8c0fc0931473f1dc729b5eaa70d -size 42804 +oid sha256:56ad29105d2d04531a1f230a4285a624cec4bdae5fce56a032f481ce69c760c2 +size 43296 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TextFieldsLight_TextFields_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TextFieldsLight_TextFields_en.png index 806bbe951c4..3811bfcaab3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TextFieldsLight_TextFields_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TextFieldsLight_TextFields_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86b121d1b8c7ea4b2401ca6ea36897b630f41b5fa9961c5bcb8746a142b8c26f -size 44373 +oid sha256:e9ab677aabb026f6719f75c907897f8c7c50d284f5337ac4b4b228fb585b8e56 +size 45003 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TopAppBarStr_App_Bars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TopAppBarStr_App_Bars_en.png new file mode 100644 index 00000000000..df3420b26d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_TopAppBarStr_App_Bars_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86b77c62b3c95dc10f14f2d511ef2d0cc95337b4c6677efc6f263ad99714c573 +size 13301 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme_ColorAliases_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme_ColorAliases_Night_0_en.png index e85bdfdf43c..73f665c052d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme_ColorAliases_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme_ColorAliases_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8b117d33e87b6f1dfb545429f6949f5362c42be957bbae7e987db5202e1b2f2 -size 66324 +oid sha256:eaf36257945e77a17ce3412e5d52a45eff114fb0b53e8158186ba2192aef5f71 +size 66325 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en.png index bd8972bb349..1a8c34f8302 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc5b7cad43115ba59798f8b0e31b0d5bc4b9efdedf8a2eacef7ad7d6eb156d6 -size 12926 +oid sha256:d27794acb3ee527509e2267fea260ad2d56b50b399c374a6070cee5c8c2d22de +size 12908 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_3_en.png index bd8972bb349..1a8c34f8302 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc5b7cad43115ba59798f8b0e31b0d5bc4b9efdedf8a2eacef7ad7d6eb156d6 -size 12926 +oid sha256:d27794acb3ee527509e2267fea260ad2d56b50b399c374a6070cee5c8c2d22de +size 12908 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_4_en.png index 76f86a3a7ea..bddcae2a040 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87c6e31b7c3b722eaa9415db45cfbe09811ff14329585e96277357a5317dd00d -size 3810 +oid sha256:b6d0b5186415672f3c7ac110f20718de8824b9fedc77ddc515d4d91f980fa7e2 +size 4023 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_5_en.png index 203c4073ce9..8853c499e8e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90be97208ca18b76aaf61fe840ead88f58a9aaa44e60fe8e66f2e43ea54cd0cf -size 5050 +oid sha256:2a8500db17f25995f8714d748e83eef38d7dbce6495b14d76317e827f62be550 +size 5054 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_8_en.png index cec4fbb0368..ba54b5d1c34 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dba4dab7966a7a9e2b21661b4e2600411b18f3380d1560c90c6b493c10a9c42 -size 4139 +oid sha256:7db58a299efd465cbae02cece5acdc0746389513fa2498cd09a3be7068eb6713 +size 4142 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en.png index 70b39385e6e..8aaafe7d132 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7cea6cba34ea64294fd4718fe79fac6e82c90cf552bfac3acc553b4e7684ddb -size 12860 +oid sha256:5a3feb1ae5105c3f716aeba67af6661df221119b458b9152704ce0b727aa448b +size 12864 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_3_en.png index 70b39385e6e..8aaafe7d132 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7cea6cba34ea64294fd4718fe79fac6e82c90cf552bfac3acc553b4e7684ddb -size 12860 +oid sha256:5a3feb1ae5105c3f716aeba67af6661df221119b458b9152704ce0b727aa448b +size 12864 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_4_en.png index 910431eb612..0b9fdd36799 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:087ef9cc26ca086e794cc61acf4afb27b52448913fb2100891a37cb4ba0cdc21 -size 3878 +oid sha256:282cc41554c3626a2fb678b6ecbb66ac3c15700635c0dbbf1005db9d318a25f2 +size 4093 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_5_en.png index a2f56d47a87..29723573358 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f43bd2190c64eb273303df7204e6289203247482f726e345f401a985ab527f91 +oid sha256:f56b4b05cba27ead484779b3f266ceeec0abcf26f104ccb08e068d3603fe7d9b size 4962 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_8_en.png index c56e2e01c11..c7efa50728e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7afd6955b08824f772d4de165bb131933a5e928706bff46929cd9bd4b33412ce -size 4192 +oid sha256:cc03e1c417f898871289f8b7972b4b2ef4ae0c59b57c2fffbdbc4bae65a68d91 +size 4200 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png index 63108d9433d..6c684f1ec5b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c3f8d8596a343d874f047bdad91e52206308066b6837271a3083b31f139ef49 -size 13526 +oid sha256:81e5612049a59be0e4792c7e6ae3683505e12161d3a01c4fb4a54ea2d8eb6c09 +size 13543 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png index 90de3282625..9305d875f7a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a86e5acab8589a935f1368134181edd47046096ed04a53c2d81e2ced733af0a9 -size 12116 +oid sha256:72611586e6f1a469436bbff5bcad36766b6b163b4199beb0d928c0a1aaa67d8b +size 12113 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 a9f0fcbacbd..e932cf8d47b 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:20fa65558d90a4f9a893586fa8abff69dd2b55d37b6f7841f6c0d850e05bdb67 -size 112162 +oid sha256:8ab12d48e310e5d725deabbee73eef00cabb0ca8a13b08ba5d8536740c53bfee +size 112159 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 new file mode 100644 index 00000000000..d302243c6a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c12017903de87b23d0d07470b5e0c6defa98fd31dceb90c97bef5332ecb844c0 +size 26141 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 new file mode 100644 index 00000000000..ea589f63795 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:719910b555845f680fd691797d9c1d455868031b86a1b720857fb2a58fa0db6e +size 24321 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 new file mode 100644 index 00000000000..0545fe37ec3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09f47e482b73dc913e6bf432a97609c6b998613b235aee699ca5a71d46a362b4 +size 25045 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 new file mode 100644 index 00000000000..dda1da4e98a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80137719fadb06ff5fe6f28f8cc9f86af243de0f57e6b00cb28758f5ea4adc28 +size 23296 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_1_en.png index 9de6354dcb9..e528eae0568 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4aa6994d79512b53deb30000d354401bbb0e5c7b716ff9ce78edc0a5ec1a700 -size 8663 +oid sha256:a7e27785d6c6c8fee1ca9e0a5a3d68373ef73cb7c8cabc811a4dd1c0dec27105 +size 8615 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_2_en.png index b4b7bf5bed2..e528eae0568 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3744f13869dbb38959f4dd0887dec02c0a94d9d080e1f89d3891c9ff9d61b20 -size 32753 +oid sha256:a7e27785d6c6c8fee1ca9e0a5a3d68373ef73cb7c8cabc811a4dd1c0dec27105 +size 8615 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_1_en.png index 49e829c8b78..b9bf089cc7d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3155928c36bc44d440cfa4abc88dcd50247cac0ae8769c34a3bcfb407da77a0d -size 8906 +oid sha256:95e047cfbfd7ec5f585a80bf77b9456679715a5f18dbec54ce44940767716b00 +size 8859 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_2_en.png index 4eeb76bd3eb..b9bf089cc7d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25e8d307a6be5516f3cf7311dec41fb131a2be3e4b66c54d5024cb3a9f0b5d53 -size 32465 +oid sha256:95e047cfbfd7ec5f585a80bf77b9456679715a5f18dbec54ce44940767716b00 +size 8859 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 397c0314432..e8b27fb6c53 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:936ed4d3c244fbe9a96fe050ae3bdf723e8147b889b8b6a6382f3eeaec565c1b -size 30432 +oid sha256:582ddce1d2b4e5eeb748e82ae7e42bcae50137ccf0caa974eb183f6ba5e1bc82 +size 30431 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnsavedAvatar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnsavedAvatar_Day_0_en.png index 661829afbf6..31eecc07a50 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnsavedAvatar_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnsavedAvatar_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f238177f5bf6e2964aa7f1d661af45c9bf09507d1a7b34dc749603e69056af1 -size 36330 +oid sha256:82c54bb785e7c2c301109f25b59e527193e1c786fdfda1252b82702c685cfd7d +size 68516 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnsavedAvatar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnsavedAvatar_Night_0_en.png index 7cdf21906e1..2811748c8fa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnsavedAvatar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnsavedAvatar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ec7b0c98849e856978f3f5601680fa9a72597340aeca40fb03e5c232c3d651f -size 36002 +oid sha256:ca5be5bfe66000d7730a9405ffb2992644f638fb58b28ac3f647c0c242ed8c4d +size 68007 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_11_en.png index 25c49f963d0..be54b6e25d4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73919b99c5a5699aeede26b8e9e14fae8a23ccbf0f7933158a215a0f96de8a25 -size 9742 +oid sha256:d59be1ddffb1e12e2e722dddb093b4e996742dd54023cf140377976ba3a2729a +size 9777 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_3_en.png index ee46744a0b7..491bd5f7b0a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:795a462bf0f34c66d9bfc87ca70b5efa2df6c20f73525f2320f61d0c08978056 -size 6499 +oid sha256:6cd50e8b230af275047116327b0df459bf0f1b2a40538c948962a076cb804a15 +size 6683 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en.png index b1e7386915f..f101b8a3b0f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:444e50fa6d7667b9295aaccbc059318e1564a3e4f7d83a360ca28bf750a4ed1b -size 6157 +oid sha256:c64d85a120267324f9f236eb177786eaa04f6a8632abbd8eb4069a9c24cfcc97 +size 17751 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_7_en.png index bdc3bb3f7ef..42783c850df 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cdac36229c6f622d7acfa3e2131b6610a70fdf929cfc164143232df06015ef0 -size 6906 +oid sha256:95098a1e7897c4ec5c495128619e53aadf92f8963a9afdf9e9b858f5e5c8be39 +size 6908 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_11_en.png index 2b7662775c4..eb074db4421 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58776fc7227c819dff17d7f2857381e022d9926906024b8102efd1b0f82029bd -size 9579 +oid sha256:6992d0760944b9bb5dc05081e8aad27b0cc9aa7cbc3452e5b3344e57d94cf5a0 +size 9604 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_3_en.png index 92f95135584..5ff2a113868 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de0c54d3b8ba37a84f2bf75d2a413a34ec7904501fad7115fdcb35518caf4bc7 -size 6456 +oid sha256:090e715462d0aff317071a8004f22b2506acdd2082f037682fed347d5fc17f77 +size 6630 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en.png index 4c1860381a4..cc91d9f3aab 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb8ea0027d6e4eb841d570c008468eaaacb2c841cfb7fa70d84c1f9e50dd9f36 -size 6147 +oid sha256:9d3261765aae0bf2aa049267678e51192286dfe830747c202431d043189a1307 +size 17512 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_7_en.png index 5cfb9081c20..24c08fb94c3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d4b7d2feef85f66b8772c7722918abad562d6673b657357e19fb094f5d04d2 -size 6853 +oid sha256:4b51baee2fc5ba9fa48eb9185a2cd9e9c895ea65da7e1fe310dca27a40bbac35 +size 6860 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 137a7d8afed..1081fef24da 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:8070f1089c8d151b74558046ca70d3c92525d80109dcc082ac05be5678b7b6e0 -size 31230 +oid sha256:4566bd5cbc9ce35873cf227cb67801982ee3271837aaaf36972c15d0e5af3379 +size 31297 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 7a2ce52d926..d28c5e9f4f4 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:2dc3ac99e6894376a01f3f9cdbe8efcfd43233ada646944634489620535d326e -size 29603 +oid sha256:db1ea9cc19e29c68aa2227e28f6f0b3ea70e468ca1a18d767f101b99f6790561 +size 29652 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 be45c32ec9a..7fc75a1ee53 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:4cded4f64be360fbd6ba607f9303e17154da24220712cf2e8da2d495b50bda26 -size 38103 +oid sha256:77c5e15dc1fab1d33b966ec656064629f3b79a2539f770149525d3918b9a6d28 +size 38116 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 440c3309cf2..7ef2e4121fa 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:de4cec2f60dda00375c6583fb2926cc0fdfa02d4673bd3d99bbe0ca3a2193952 -size 36454 +oid sha256:28555a9e4ad68098acd694b40bc9e071641bbf8cba8ef1da6d7d91f10b5d0d07 +size 36475 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png index 02a746e1d8b..b6ed5508ce2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b2613f0382fcc71f248e92a2ab007c20b88178f10f20e91a98f9112ac5dd3b2 -size 8226 +oid sha256:c2fb56957eb5a9273f37c3f9b973feaa5fa439eac92ee9b0f9398d6afb2cdd81 +size 8373 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png index 9ccb8d78d2c..28851d43a20 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfc1ae0e3cb25ee9e6092c2eda4b5e8d03ec3fae37e8d4d4d591e3d697042201 -size 12845 +oid sha256:a144c54d3a135763f6ffa6c8779e99d4051756cad37fe3dadae6a7007e44964a +size 12984 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png index aea3483115c..87c031fc16b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51c50604b17ac864c548a33141aba6bf9c1c791790a11fe692493a9fd3d91fda -size 35598 +oid sha256:0e0d844ba1da660a17a341aac67cced91a4c6f46fcd895df0e5a8d260299a1cc +size 35740 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png index d79c6a8d25c..732cb7c319f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37f2c5ddc5dbf12e9006865a8e0348949e8811e9e3031721d67dcc0b878d00ba -size 8011 +oid sha256:729a3e601744721ecefeb6ecc7cca8df59ab29a81d03df775f4abb08c7546fa2 +size 8142 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png index 04df2c1c11b..1716664651a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9e21c6ef5aeb835cee91a073ccd23a9fae1f3a1e6bc5c5dfdbe80ce9da62c51 -size 12496 +oid sha256:3a44ec2a88fa4d43320e279e4ef00c6c3fbe7641ff619a6589a60b1651d49915 +size 12603 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png index 11c187832fe..a254714f66b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04c3a63bd1d6b1c217946fd3ac359ebbedd33d39da663b489397a4a81744c75d -size 34365 +oid sha256:e1d45184a3bed370b47f3b559b2a9927a108ef26c595a98d1b567a57539966be +size 34492 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png index 56d561434cc..936ecc3ee07 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fe856f45857f420a42562f0618473857eccd3f9a16c75df43157b881d473fd4 -size 8892 +oid sha256:2d48c56c30f1c1043c398849fcc9e66ae3184f186a89b6eb446f107ec5c60727 +size 8901 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png index 7c0ae7e7de2..d2b3e9262aa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc84a1980024331be4906d1df8702ba16cfee553d18c3556d6c08597cc5c1a05 -size 13243 +oid sha256:e997992ed8a574e9a15df43292514a285441a11da994b32002046d2508c3bbec +size 13251 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png index 60abaf54cf9..b0271d9d74a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:538d13a4aba7f8d7e503852eed6ae2fe35c1a54002c33abe7ef763052d4c7ab8 -size 36254 +oid sha256:661cdb45ccec5bc9c7cd7e5d185fe38abe7911f6010552270bc0c34dbfc068ae +size 36260 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png index 412d151f90f..c51247b8648 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:674d480011c737b98adc471bca330182efc6eb31b1671e6bdd6aa54bfeaada9d -size 8616 +oid sha256:84c93351d1c0ff82ccbb5a566f644bd0483c836081f888cc370880bb55cdd576 +size 8638 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png index 89ea4bcb8b9..463d5b9d17e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ffaef3911f91499e4905e0d041c8ff31a6343c2b71ea79f0b4ab6d6c89800d5 -size 12795 +oid sha256:f36e420501a83e7b1292c139c4bb694d1d42ead1f2d8cbf52fe48c1753c8e25c +size 12816 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png index 31cbcbc59b9..1db3b737b22 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2f88af6da9f62f2fe0fb17883387574eaeca5d1f65f651d970e13c22ca8079b -size 34947 +oid sha256:5f727a995d7863b68675c2daf4cabb988b75c440bf063b1d77765daaa4f502f1 +size 34976 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png index a027c893039..42704f56473 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15d3fa6a95cda6bca06ad79d3f4862db05e38111cdcac47c1cdd3aa204bc1f97 -size 4210 +oid sha256:871cede89b11408db3873b7512f204a30c10461e5182ed1dfc0ccbf459e5ae86 +size 509431 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png index 503f2bb229e..bdf2a68f058 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abaae9e0c6bf9d7dec701e9a51592e89408668e0a2b8325731efdfdc73978acd -size 3667 +oid sha256:c2f098a79f6aa516c3624481c573b730de1d978616068ca2e9994850922a1172 +size 508910 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png index f5cb7d82cdf..dd1be5c2bad 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00159ed8d968d53970e4a3b7f82ab542fb5eabfc4513dd68d0eef05f0615373e -size 7290 +oid sha256:0d4f9eb0aa4caf7300e1d46c012caa420d75359b31d4312c3c074153476f8589 +size 508761 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png index 6665c107c11..ace50774343 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efb12b63fde67256b255503d00848d64480e142c54619329b75aeed451a3dd17 -size 6375 +oid sha256:af4c41c3e23c6669f42ee6413e9d7dee7b166fd4ccd8d189eb4df96601fb7554 +size 508550 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png index d58f4c31555..82127ffa239 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f6835bd79d18d202c1d21b00a1afa4fa2c7cbfaf8f586a1dd1f48afdd5f69e5 -size 7644 +oid sha256:c3e88b94e81ee3ae2cf7c091e6fbe9b7668b88fc675ec41342b1c27d817013cf +size 509402 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png index dc363be75bc..1dc78b8c9c1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:452afc2e04191eb82de772597ee97987eda5667ff56ecb684bb3b9e0bef90435 -size 6737 +oid sha256:1e9b94a224bd3b8ac9b3722a5817b315497ea48c1847b3e5fc796482070c5844 +size 508924 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png index 9807f8bd54e..0581a5f6518 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ee8d9e829efc0723d62411e2069a8dac90b18069d1d8cd5dc5ec6a5b9899a14 -size 21572 +oid sha256:b5f93cf8983f5e2a34fd777ae386f09aa5a54dddc29f5d3eda3d5ccb71f267ff +size 528450 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en.png index 07edef2141f..4d065a57160 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbcf086763463eaa1dbf9cb52620c430f7a7982f01d3abcd039ebd307544f8e7 -size 72986 +oid sha256:c1a7fb899410f0fc591994dfcfda047d2bdacd120658793bcc142be60eb39324 +size 73010 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png index ca39d4cceaa..c1cf3654504 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff96b22724d7f82b3003a73a560da0a34c9c196757b4336706b5823bbfa32589 -size 32398 +oid sha256:9e06c4fdecb6b6965f974d7f424e4fe2cb1b0d338dded694fddd8526f5d6d1b9 +size 32544 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 cc888a4d576..4d767762f2e 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:492b5ae698dc52672d8d0a4599c9cd9a5b6f414e8a0a6f42c91e765e5a5b221d -size 40921 +oid sha256:1bb913c523ce939db43f4ca48855bbb74bb5f68f0bfbc4389153d63fe8b515ff +size 40934 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png index 5c067f246f2..788b2a0eb31 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcb5bc041286ba863ae982b2ad03873a76e48ed6ebd5d35c82dea269d86363a7 -size 21189 +oid sha256:8d4e5c1cc6d8e8d5d7b7b5d25ed70efb840f9ca18336d57d0a471753e9674dba +size 524899 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en.png index 700079a0db3..10bc833804e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:267cc528f2fdaba66bfad4f8c8622087b76c2e3409f5fda8ce25009039278a22 -size 64356 +oid sha256:71036d96190e9ecc73c92ad26d93d99d6bb2b13606996a40804e738249d8df1e +size 64387 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png index 2149a46ba7c..19185a8e591 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f5831741183467f1d05517097f2617aee405a9d6752cdf8a8e193e5851376a3 -size 30904 +oid sha256:2c8ea57ab755298a5c7f8d2c78bcf7e9b059e5509dbd3ab6ef198f94d1007df4 +size 31013 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 158c1812401..2bcfa031f6a 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:de98370531bc9342539bbf98b6f3534b72e327a94e34b1c6d827e2330291340c -size 39235 +oid sha256:8c8de4a38149e730572b49a99e296e57a307c5f48bf2d97cccede39fc0199424 +size 39256 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 10f75384f6f..de9c746ea08 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:8d8842663702441ce586c7e2141c0cdf47032a26b6015e592abb5682e3cd2c60 -size 25152 +oid sha256:280c717aaea92a570a45de4fdb7310952946a9b7c8c52011899445a1563131cc +size 25462 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 876fab066dc..7f09771c8c5 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:6520b0faf9c22ee1d9b1a088b29fc07e3d8004db5a342cd3bff189d844aace6e -size 24649 +oid sha256:05067a9993096d7d779182b117b87cccbef0ce0992d8123bb5184d35f22de1bf +size 24933 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png index 50f49e2d0bd..b074f416874 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2907ba5f75a40e30a37ad4d302797cea4e3d2b77cb1c66a9432319242b8f1995 -size 12188 +oid sha256:2953eb53475bec7b2e840c53d8c7805e55cf3ef906e56b5814636c27900571f4 +size 12185 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png index 37bc2056260..581e458c985 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59e981e51595a368745c92f355793b2c0301a9e4929430733876a02f3ac75e2e -size 11881 +oid sha256:ca2e22b393029a9255bc07c6e417ee70d3209c2773928dbbf20282e77c627e09 +size 11870 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 c497a8cb269..d2c7fc2e404 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:ddb495eaf8113f0be1ba572697083bd5b8ccd4c308780478b6c4691dd0f8d922 -size 8019 +oid sha256:5af95c3019b00ac54031978e4b1be995ce03f652723846863771c6850488d4a8 +size 8022 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 257beda2948..ce39d18a074 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:22e4744300d62e550a9c545420621fe6aa8db1674a876731be31a12fbc426cbc -size 7320 +oid sha256:24b74dd10a9c73adbd32725f7c5f32ea5c8790fc5c492f318d284c2b499049ec +size 7321 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 d6bb46d5c23..5c9644ad808 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:773785bb234c0719c0f27ee91a970c63d7f971518dc0c83298a4b6adb9acc4cf -size 8066 +oid sha256:b3d9df5a468c4a1d93c91e940ff6195dedc97257fe13870e4632faf11f3ca2fa +size 8069 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 64ec4ae5d4f..e84e7c9d865 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:5424e7ce4a5259aab297a7911741352f0fba01e5fdac972768441db9edf41171 -size 7675 +oid sha256:5cea70146259c8be492873dedb297077dfac1df155d527666955abc05b8744c4 +size 7684 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_12_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_1_en.png new file mode 100644 index 00000000000..1b3302c89cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:615107c5e6d654779b1a8fb0ac3e5511f03ac0eac2a5c3ae0424972b92401ca2 +size 5244 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_2_en.png new file mode 100644 index 00000000000..cfe67410f0d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:783bfa52d591b0ae98ac55cf5da8c4a7d7a28275e2714ec7278d4a7947009f4d +size 6187 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_3_en.png new file mode 100644 index 00000000000..a7883780457 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3e94d893ac1bc9fc13806556332f6639469abc620a7617a730a8bbc9153f826 +size 7070 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_12_en.png rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_1_en.png new file mode 100644 index 00000000000..cba2bf39b20 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a360e21538876df4d8aa1b4a3e95e4982df6307a69df4d887416cbbd76b8cd99 +size 5250 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_2_en.png new file mode 100644 index 00000000000..cf3bd3a0ea3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e374bcd5e119bf944eeaf28876b7e902c1bbd2e6bbf84e0f30ace9732675b9ae +size 6142 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_3_en.png new file mode 100644 index 00000000000..47cb88a8137 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bd12380891a5af74a827ce14393bb5f79d9019eec7653e10ecc2923cec3c2a9 +size 6945 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 4bd792bedf9..b55294cf851 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:b3599a3a6fb43f98d928ce71c3ef7a8b8102d558c31ea1f735a948e337738f95 -size 13533 +oid sha256:0ed1979a1216e09690e6ac3fc826d018d7d70ec86cfdf7300226a4ec0989667d +size 13532 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 48a87ab6aee..bbbecb9fdb4 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:412476e819f688b5314651fd3d59439fc0e4f53ee777e3a070b82fd19d51700e -size 13272 +oid sha256:5a77a6032c61758c55c26f916ea0351800115210d38bdc3040827cc5b7f977e1 +size 13271 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 0ab3d99837b..7dbb416bdc2 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:1e4ef6ed6fe4c858ac4c67bf2bc5d428f4d4734cf8b01244a3bc963a815a540a -size 389328 +oid sha256:69e9c9327893e0d8823fbbdcf4c8491fddfebfff2ebc7a8b1d286c1e08393525 +size 389330 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 81afb4ea1cd..4a6573b425f 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:67ca752577251e9e57e7680ae1bbdef3324c84ec6a1037aa9f7c228bb8206f4c -size 388634 +oid sha256:5662ae11a7256dc5531b4788e878e1c398c44798bcd94400b30396139a7b4089 +size 388663 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 b66dd0d5064..8cb39b9087e 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:96941aab9596583187e4a089bd448252be551e3ef6b2fb7550c3bad5c7ba60fb -size 37905 +oid sha256:4a76e053e9c965a5196a12af5ec5328b5b5c2f4d628deb680c2060feff828749 +size 37922 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 2b16d56f044..f796a811999 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:add442c1cabc79cde42e65775b22413c92918bba93d96bb72a9ff214b9ae7fac -size 31126 +oid sha256:70efa4c5843b1da2ac887475599be75a700aee16469fb5fb2c222a37a3b821e0 +size 31165 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 a227321f6b1..c1734088d7f 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:0c288f75fcccb93e074afd8178219887ce8a541d8e444df18ca041647114d340 -size 24491 +oid sha256:28006561c0ff11c97ed9e16a090a008700066f3740eec35b2a418c4ed2cdcab1 +size 133897 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 new file mode 100644 index 00000000000..f701e5bac85 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_16_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2a48623119e17e47184b6925f9fcb4c5ed9625765e2e6a8537041ec9253ed4d +size 113570 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 73e4bde51ea..b6987ac6752 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:248ad0bbfc8c8a56975c2fc6ddfa5275ff4f3ad39b9a76126c8d4bdd0c566e88 -size 389354 +oid sha256:09be544b2b4f1128557da415f075ecc88a3db42458006558c8fd716f533b1dc5 +size 389357 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 ef7f299a264..a1521a7b1b2 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:ecdd1219635a61be5473773464d3796ea6a8f17ac2c384e131265e9557dadf42 -size 95129 +oid sha256:7124b7a6e17de6ac3772b43d25407db7ff7a0124ddf9263d091c2016a66b9198 +size 95143 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 adf626696dc..b1d677139ac 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:c89d437ccf40ac25227d19d1b775ae0992cad7970784e29586438a60f6bf950b -size 396206 +oid sha256:273a842e3821d4467c1c03aa5510b2680e6fe4ab82f19b41fab577e972fbba5e +size 396189 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 cf56e5a13bc..9cf4813cc00 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:9ac33be436135d5022024108ec403ea3e53995d8e42dcc52bbd3d8041e5e2975 -size 25053 +oid sha256:4ca520b7761afc1a88c83e947f0bd1c8a19b65430c78735bace8038d4680077f +size 131596 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 75b5e2302f3..487ecf30fa6 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:4d0288375c9e746d4cbb9b270c1c7f5e97633d76027186bd9568555edeaf8700 -size 5411 +oid sha256:34e2486d2d6eb25df20978ca2a9f83ed7dc35aae7a506f9077ece1aac292f799 +size 112101 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 2bfbd92c3b7..2d79de708b5 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:c756b50710b1ed10eea01d8f97d8cca79a3b3440c559e1f593c790b55e5f6556 -size 14194 +oid sha256:ccefa0617f544b53e8d150bcadae23b8050638a640355c10dac6ffe081a94d05 +size 123336 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 be7ed165cf0..288dac72db3 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:2c5a6e30127fe88d1d55da6daebff64981e88bbf7a37c712c99f831849e56172 -size 14374 +oid sha256:d994a11e59b06122a5ad7c254b5a3c27f9d032b1c0a050dffc7e867c34deb050 +size 14361 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 654e1d891ed..43c4783b624 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:1ddd7d087dac24b5f2861b59c946eb89046b9bcb1a709dca423b8893f55a81f9 -size 26217 +oid sha256:b6f583b3861c9d20508c886db1dfc9f7fc981673b7b7c5737f075ebb5e4f0dc9 +size 136609 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 f230ae125e2..d32210f468e 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:8a0e84ee0e17fa30222ffb5fe58f353ca847b251a8da7088a58faa467f1f6742 -size 26258 +oid sha256:909837b6fc20a48624952f7ef99ef0638a507cef08d5dab8285aa527f0b3d1a1 +size 136717 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png deleted file mode 100644 index 785f56af7f5..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:154a56d848cc735efc25274052ce92b1a20bc8f80f75e91d0d4cfc7d488cd246 -size 5874 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png deleted file mode 100644 index 3f13dad18f5..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9ff42261f4d991650578ceadaa13be0b1924c1af8f71cf10832f53c7d958827d -size 9317 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png deleted file mode 100644 index c41c1c79218..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2dd3462935091cfe022f0e6fc71b082eed7dab4769def13398a81a90f871b61a -size 5807 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png deleted file mode 100644 index 17ecfab0903..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e6158b2d0447199de84156fbfdc7bd9aa1af7ff2092b498db254ded465605b76 -size 8208 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_0_en.png index 10adfc84001..a6ef6d7db5f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ac4a8ce6b6f7d9179be2d51c44c766ab71bbb7a8e9e9fcdb4d62b1c9f660dda -size 13292 +oid sha256:910a2b442c7a083381629718738edd84614e059f14f7639f1d23e740dedf2717 +size 13152 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_1_en.png index cc1f51b3c97..79539cc2ba7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05712f7aee243827568680e2e911640bb058f2a3b5500740054d72eb47bb97c1 +oid sha256:e8fbaa10be1d09931f7e74502a7badc15f331040da89d4dbdce97211ac7bbba1 size 11085 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png index 0e22f34d88c..c29a8568fa7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0db3f0cdb53eb761bf3742ce94f925472a7e1b48b306d2cd81baa2c42bee6a14 -size 31142 +oid sha256:ab0c3b105b12305c073989fb7686f4e012a33651928219d2c6300f5fc165d420 +size 30981 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png index 1eb2ec85b44..6d265d08927 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6139f1037a5c09425121ddddac37ab0d006f3441467329715fd53dd3d27f24b -size 29200 +oid sha256:d1a17ed95a976a813b541eb373e2eb1f50388f6a994da7b65a586d31be8e0d23 +size 29199 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png index f0656f971f4..734ee090dbf 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1034df9a9d3404f00658b1bb0e4f939f5e9cb00d8966761d9089bb3cb9bab86a +oid sha256:6a6a80bd4372c69ee97a857bf2a03bd90fe011b3703649b171bbc168f08c92be size 33643 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png index 6411138939b..e8bfc990c6c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5aa120484502f3ed14e33a7460b19bd090ac421bbb383e75127bbdf3f54f5ca3 -size 28818 +oid sha256:d1c2ae860a7453897346416f0a55363a121c899dcb16756e525d9f82b5aea7cd +size 28667 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_0_en.png index 321d4b94ef7..2e88d12fc0e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:415665f1025fddf1141d9b46292b4e9f05c14bec32371e32b8fdadde5fd0d183 -size 12598 +oid sha256:39cf02b9ba4e1b4272fd70fce37f927d5b8c99e0ba8f61cde2ffb312ef42abeb +size 12500 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_1_en.png index 55763a27416..187f6011a14 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f41a72ee1adb7fb72720c2c4323a1049b5a2c77e88372aec4b51c1e32251f6f3 -size 10796 +oid sha256:f99464f3f16b7304ed1a6a99b864a30ad874d4ada5d9960ef41dc8557c9f4061 +size 10794 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png index 451df5a6bdc..02d2f734a13 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:646bf7110d66643fda586c2ecd54b78f09111d0e74a531a7c1fa08e968b1abc3 -size 30217 +oid sha256:3fb42641a724ad82ed94660327179c7a6782073ca39d9904a1133dd9462397a4 +size 30124 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png index 7a29490bb0d..4ef4a973579 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a479ad5ccdfd947730465010cf21503c0e7a22857537cd7d8f727a45beec58cd +oid sha256:594841dd922deceeee60da8bdd7228f9c685f2aa700a2ccbe0dd03bd4b9e4b98 size 28739 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png index a1820a31329..5ce058ef3e8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df25628681cdf2bee33230069ae84210c3dda185d1e009fe646cb1cd30bf1ed5 +oid sha256:79224876e9c3e44f30f08c1e1d4050d8c909d8f290c4267f29eea15ae4f7f2d2 size 33186 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png index f19ea6f743b..1f53e949ea3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c44fadbdeb27169f88a2678ab8b6db9ac7a9f6b21871a436d14728eec398daf -size 27883 +oid sha256:be403e61390a076aa830470dee1095888f05990a5ae6bb99a2498af121656828 +size 27789 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_ComposerOptionsButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_ComposerOptionsButton_Day_0_en.png deleted file mode 100644 index 60d16dd429e..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_ComposerOptionsButton_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:66b9db2c2f0af1d0aebffdd74ceb8177aff237bc8f7d84deb13501f8aaed5872 -size 4583 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_ComposerOptionsButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_ComposerOptionsButton_Night_0_en.png deleted file mode 100644 index e5341ba23c8..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_ComposerOptionsButton_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c89e2c023db9d402fe28ed322173938df1c6b5d6dff18a4ffddfe768b9b992d7 -size 4587 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_DismissTextFormattingButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_DismissTextFormattingButton_Day_0_en.png deleted file mode 100644 index e42669d4526..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_DismissTextFormattingButton_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4dd9d99e40d9991117971f912f45cfe0457917b77de9e90300e80768d8a2037b -size 4840 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_DismissTextFormattingButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_DismissTextFormattingButton_Night_0_en.png deleted file mode 100644 index 7a6a922b788..00000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_DismissTextFormattingButton_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9ab53f98e76b04d78497fc1b77607eb65b6141a98d7895ac0e428abede8b148f -size 4814 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_FormattingOption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_FormattingOption_Day_0_en.png index 1e799d259d7..1a330b04841 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_FormattingOption_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_FormattingOption_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f38013c0ddfdd236760df2a0d52e9755459b69ca6a68f7dca660f3809a83e93d -size 5166 +oid sha256:995c805c496e4acfd98f28971b17fdc13374a5beb9669bff745311cd739b054c +size 5169 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_FormattingOption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_FormattingOption_Night_0_en.png index 09728a23dba..3ca8dd84f4e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_FormattingOption_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_FormattingOption_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63b7ca45ea1b3857edbcffa207f15ccf51bebd54df2ceda37a0b7907e6a18df7 -size 5103 +oid sha256:1f11d84c6abb51261bc8d2763f52901605ed72cb336b9d42a265e11eaa85fc8f +size 5111 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png index 92093d1e759..7d6e57a4c98 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78251d1ca6ebd636376dc200e06d3b033b5d9e9df3e32e452f9882663d06ffec -size 8676 +oid sha256:6642e9af74b4b404e89d08e68ace09cbbfe91b21173989c8c7921a033ff53c4d +size 8671 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png index afebd5ed8f0..5118946d2f1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8e2a46f08630a184d0b6e182e31712aa959f2976b348b7d4b4a8f50f22bf2ae -size 8473 +oid sha256:66393493e210dd6726b31ff4d32cf5ee4a54b24a268f2105e49ea10bad35d689 +size 8464 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_TextFormatting_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_TextFormatting_Day_0_en.png index afbd86cd827..3a8e7963640 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_TextFormatting_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_TextFormatting_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0a6b1d4996735e79f05074f20103243d803cc502bae1635432a1eea52cfe57d -size 5925 +oid sha256:eb92edfcd06dab3a516cd0ab35e92ce4db50306ef121a1b9aa76d9b7c8b1276f +size 5933 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_TextFormatting_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_TextFormatting_Night_0_en.png index 6794c173527..6fa23c30944 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_TextFormatting_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_TextFormatting_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87da38c6b739dfd8d407ae950bf9328a7b54a6872144ac914ef54166c5db5fda -size 5773 +oid sha256:926a3ebbf2eca735988c8bb24f93361e9c03335b5f40d0d15a9f3470a336e1b5 +size 5772 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Day_0_en.png index 8068aa97793..48127f148f6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b09cd6f506ee333f96b7b86f73996cf822702a9fd27f05dbb39fedf802199920 -size 4294 +oid sha256:fb679142bb81b37a49d030b5268018cef90f6a537711871cfd1de437e90332c9 +size 4313 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Night_0_en.png index ec7839d4a39..7643b87f9bd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de72f81a640935c1d396195131ddb3c49da0ab0d5a116681c2cd2bacb2ccec85 -size 4253 +oid sha256:a52ae5c2c8a8de0b92eb3f1655aacf9fbf5d6ed2dead662580a34cb60fd5ecb3 +size 4269 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 5f6cb5caad7..78aa5cff8da 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:f541f0a05b49272ef3e379a9a629c71458b37435928338a3a58ac852b38eb803 -size 48802 +oid sha256:73a225eeb9c217ada941b5add96dd2f832fb33b702f86eae5e1f5ecbee5a4bb2 +size 51625 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 81f4997ffcf..20eb03d5d41 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:a542b3be557dddc3aba16bda8bb7db1bf0078e6bb3abcc42a11c723e8dba3504 -size 46683 +oid sha256:ab365d585b76ab0723c9998cc13f755640b7f086b99b55804a38e103b8d42416 +size 49440 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en.png index c8aedb734a9..4d12b16d6af 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bcb98d543eba3d43a58b9d17f753c65ccbae2df6f6ebf444c2eccae482a7466 -size 18474 +oid sha256:90218dca0be48c641761178aba13fc46f54cdecd7e050146416e9a49afa439da +size 18504 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en.png index 2053e6fa752..806455861aa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e77770bc5d582ad53fc1ad7d3d86b8df85e2c0f8fb997145a1c695ca14cf1e93 -size 17258 +oid sha256:3ca61ffc0ee37f992c11b9039adb22aac23ff7af6b4f796a0bc7d87a23ab98da +size 17297 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_0_en.png index 8268a757350..429f278cedd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d4d42730ec3015b56f13fcce98c4be694133b2101272e8f9173a5f938aef5ec +oid sha256:16816388aa16f525b69ed1d4254cd91294bb60281a2b67df095e1ac4fad8c544 size 5455 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_0_en.png index 6ebfe7bf200..67c6d2862f7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a7f8b640efe8ff0fb996883593ec89fe65c34a12ee500475633e58c344d5668 +oid sha256:7dcd6a858d13ddbd22b6bdfa47aee0c0a98af75d3840eff9767e52a144268539 size 5307 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 f6f752ccf95..a533d93146e 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:6e78e412582b829f3504227b4f3b8d0180ea1f9f71ded30213e0c1153ba4e20c -size 52481 +oid sha256:4f9279dc01e9fdec69ee0819e2b4b7e0f00dec7e1b62c353052530b6bb66d4f0 +size 52621 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 049fd8c1ffe..ca9dd3997a4 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:ff436720455e8951c96578613fab9980de42459334fa1d578c20a4c633c9f6a5 -size 50909 +oid sha256:0c3d263dfa02c775db1f370ac6ada2d3b31f3ce252f605e05f7121367b609bc0 +size 50915 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 d2b38ae3dfe..5ee6bdd7106 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:3f5eef2eb386333da113037b088fdd0a565b0816bee70f2f7decca88893a3164 -size 53457 +oid sha256:72c662eb1b7c5d85c65d1f7cc25bf484964f0ce31f414406fe8fbf90f2e9173f +size 53784 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 3ebb4b2f18a..6f8df5e1e77 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:5d1aeebf8addea8474ae7e97fa1fa064d7b5f416aab2ba385135b12ddd44b5d5 -size 51767 +oid sha256:d6bef68b39ff88a4020f30c0ef8313eccbe2d360723894414cdabd9b89ee9c84 +size 51768 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 c575f5f5bc8..07d884cde4e 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:1aca6febd470707f5fd0c6919f4ad55cf7b0b7b9afe5c34db63f0acfa059120d -size 47506 +oid sha256:a28794895dd4d6a6389aa2c23e756861e6f07ff13b60c67f273fdf027dd8eb09 +size 47515 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 af28c0055ff..4033cabff3a 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:c908b06d3d00253a8d2fd0231bbdd6bb5b0a7574c4599814356ab3bef42192cb -size 45974 +oid sha256:3d4214277841ebc15eda9dae5367cec2d1127eadecc7b2fa97ad9d845888cd2d +size 46042 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 a3a8fad8b23..f8f15e94f6c 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:149e24482adb4b4ddf1e54fe39ab56315e73598f0dfec4acff479c89509070f9 -size 52144 +oid sha256:5fca1c7a63f89d4ae3c63b473a7352f83abda12bde4ef7698fb4d80218ba0cc8 +size 52411 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 cd34c866fbd..1c797b94a28 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:e9fb48855ec77da81d4cba8f428fbfa69039020a81b6ccea764413e42210abb2 -size 50860 +oid sha256:5bf7e9b141aef2236b935503a409eae7dc06f824d09a83fb97370450920f6d54 +size 50841 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 new file mode 100644 index 00000000000..9d16559ee8e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f1643e255457fcf5511d807235137a5afcf457740a02ad9632e26b06b224f89 +size 63488 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 new file mode 100644 index 00000000000..c7be9c1635e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a01bfddaadf8d165d638980d05ebf02076c9ddeb0160e7dff40171fc433d3c05 +size 61161 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 f6f752ccf95..a533d93146e 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:6e78e412582b829f3504227b4f3b8d0180ea1f9f71ded30213e0c1153ba4e20c -size 52481 +oid sha256:4f9279dc01e9fdec69ee0819e2b4b7e0f00dec7e1b62c353052530b6bb66d4f0 +size 52621 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 049fd8c1ffe..ca9dd3997a4 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:ff436720455e8951c96578613fab9980de42459334fa1d578c20a4c633c9f6a5 -size 50909 +oid sha256:0c3d263dfa02c775db1f370ac6ada2d3b31f3ce252f605e05f7121367b609bc0 +size 50915 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 new file mode 100644 index 00000000000..2fec8ddfe95 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a596e77c10b67cd0295b0676c051d712205839b52bf46f831ecaa24d275dbec +size 64470 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 new file mode 100644 index 00000000000..a74efac0ff8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a8686422da867abcd6dc0d33e021ecd59499509d85ff8d722ea2a53facc6ccd +size 61934 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 f2a7b25e4fc..d65c2360472 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:fc2b0061acf3b93e9c27f14f27c5422b034f1d82a579e31b34ce6c09cc255751 -size 53468 +oid sha256:d552bcec8eb096ae396e545744af9018bcae277597a1098e0477e87e4620e30c +size 53806 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 b8ed083d9f8..838cd73d508 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:a5438d8a7c75ebb8b90bc7cb0086c64399546163393752ec0ef96796903cb391 -size 50962 +oid sha256:54b501a2dc902ba27a701fc8539fa2d2eb64511d2f058b4ff189365df63d5665 +size 51093 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en.png index 540f1b6ec4f..a3bdbf9b773 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d966380d44d4fdfa87c17135cb540aec7b5e5d9dd433ddb9945d088b46eb76a -size 13485 +oid sha256:12f0ca8fdeddd40c744cb0ece103a98bfa2f38450d46458fcc6fc06751360ed5 +size 13606 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en.png index 74852be96b2..42bb0815dc4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:808978903883f938d08c594c9a87f97774f9a0c11847088a623e199809882adb -size 11916 +oid sha256:e7c5e43207805ef6241140c17405258d2d6615ca80180cc6880457326b6b8446 +size 12011 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en.png index ae450922c07..ebc1200cfe4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce993fc311dd889c5a709941cec6224af603f3e9db62c02eda36adcb43d9c8d2 -size 14697 +oid sha256:2928dcfc8005ecf9b4666ab6e4c53f79641eb6946271db8a994a40edb40c963d +size 15180 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en.png index d4209f72ff3..ea6127c7719 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e256f644dd8cbb96d0d97c0f41e036f41f137cd09e322da9807ad7271943a6a -size 12999 +oid sha256:69d4d152932d0ff43f179f739810a63afad8124dd5ea706d73ab399cc00871cb +size 13472 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en.png index bd2bfdf6b66..6973fd68c7b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0602f269f3d3879db0a6b18ff56de7dd3fcecf79ccee1c25e2aa763dab9e1e3d -size 16958 +oid sha256:ac58ade49e587fe1be47055a6c0147dba40ee4e3ded1a3ae67ae9ccecf4c54b0 +size 17424 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en.png index e244a6fd2a5..6f592e08274 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ef3f08243ed2dde9dbe4eb7422d68c33a4ae1443b7b75dd3661d245f58b539a -size 15088 +oid sha256:e9d00a99b3bfad8572a86562f1e40b4ac06f241efcd778398d1f7593ff3f5c43 +size 15620 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 new file mode 100644 index 00000000000..e9d269d47f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfcb5206ea0fc6b89d6ac460d0d78bd8b70cc3e960d4ab0397bd6470e7dd23b8 +size 73608 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 new file mode 100644 index 00000000000..5f86ec269b3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdf8092eb79caa45a8f911589af848756218fc7ca8e3c32c11b7b650a4b9564a +size 60143 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 new file mode 100644 index 00000000000..c4715f2010e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dd4dbbaef196264c23e4188b34c617d2e72bacaa9f206a18fe2a73ba17adf63 +size 73006 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 new file mode 100644 index 00000000000..c4da3da864e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c8920e6be0d01f0038d315a6421fe051b9f205477bb313e8ae8afada023fea1 +size 81122 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 new file mode 100644 index 00000000000..c7f1a6c6197 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3e2cfa763cf8f73872d108e3c749ced3dc730b9ba2902b1050d2fdddb036c56 +size 62658 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 new file mode 100644 index 00000000000..419029e254d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8b3b3fbdc951585ace8942e9307581cb7793ffaa4c8fe56a52b6063bd5db271 +size 61540 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 new file mode 100644 index 00000000000..71c7b2817b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68bdae9e2b82ba19d7d7de98fc682ffe7eb96777ba86b7fa99f2acc0eb33f7a5 +size 68023 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 new file mode 100644 index 00000000000..e5c3663080d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d799e7c48c310dd2a9e42bf8765016da88ee69f51555992d2f80f467e8f07be0 +size 90598 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 new file mode 100644 index 00000000000..dfefb57abd0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed11b4abb6a6a1a790248026fd9b322d1d00f3988957a9a549573d859dd6fa92 +size 61017 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 new file mode 100644 index 00000000000..837f7d577d3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:851fe33ec8cac57d5ade446cec96dc5ec5790c8ce28d8bb47b111b46b9e30abd +size 62175 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 new file mode 100644 index 00000000000..d5dc1d06c06 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1838d95b241ebe2b83e732569efe344f1bde77b2cff068d0bb40412306c1f2c3 +size 68840 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 new file mode 100644 index 00000000000..5d956beeebb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ab214a116f303ef96e58d29edb757ccd4a36100d701c230a5f60d5850ebf0ce +size 60562 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 new file mode 100644 index 00000000000..ae795c68bf0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b38a8b5fd2c82843b4da6d0c4c11400f569d8e38a8c4dcde03056c5e81a53c0f +size 71405 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 new file mode 100644 index 00000000000..58444fb9565 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:996e5014e75e88bded0ef2742733b60f55b003486e3b2a418298e39dfc595add +size 58295 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 new file mode 100644 index 00000000000..7f86914fce1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eab616b950daa6169f06930d411e1ed75ef1cc2f425d692abb8fb479c658d9bc +size 70819 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 new file mode 100644 index 00000000000..1778478c3b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61b85adda6a18eebcccd149318e6c0da23c791cf4df8405f41e31891bfa9febc +size 78647 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 new file mode 100644 index 00000000000..201a5202f23 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:950da1e8c251d711c7ec460234981ac52f6e6bf7528fc904518b93fb0ce35b09 +size 60708 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 new file mode 100644 index 00000000000..d397948301d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d4756b79b40777acad34655ab49ff9811f20b5196c6f2cbe31207e876290906 +size 59737 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 new file mode 100644 index 00000000000..abf0911da58 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c7e798594d983cc0e544e3eb70ea1654a78a866a7bc64ef620ac6a823b8a691 +size 66011 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 new file mode 100644 index 00000000000..3fd4cb75c10 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f65f939395ba3c0736d59d9cdfcd55f1a9f7f9c0e1fcec8ff5da441277313a0e +size 87547 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 new file mode 100644 index 00000000000..829941d6c5c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a4fee3963b352bfc49d8ef7e18c0179f304064758daafc8324cdc650458b522 +size 59171 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 new file mode 100644 index 00000000000..2d3b89b31e7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b075203bb02a9a68b70e24d4aa3b7975c50bbbc2087026f28910427f431ec62 +size 60251 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 new file mode 100644 index 00000000000..5e2596ff575 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bc27af141adfea18c39b86bcdf046392e765e7411453b6143958d5ede017411 +size 66622 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 new file mode 100644 index 00000000000..102acf57ed3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03291891705787b0211dfd7375d755dc1abc985eb88d48bd978555032bea7a4e +size 58782 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 26cdb69f7b1..3d64aa3d290 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:a315b0a33c2034828d8943f64ad88f3a2e3b4a39db17e4885a80d387be0e1a06 -size 76291 +oid sha256:bb24ef3a0c22045fc9e9ec390dd1785779924f5bd2a10f9bbc88d1a70b106191 +size 76305 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 f3152152bd7..372645812d3 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:49c600c54b3ba8eb98af152a9867f7a1d8740c76f51f4b0d85af95fb5f716e34 -size 59323 +oid sha256:1974abd76e0a3f8cdf1beefe26209d2bffb5212520f9a4e2655b80925337d1e7 +size 59835 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 6afb4ba7c2a..4ebd5027f7c 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:0fac85629625d4e47c266e54105dde49b28036726718afde8e5dff7cb4505b3c -size 74264 +oid sha256:2aaf080dad05b68af3ed2ea9a4d6cd2f2e9bb991da02683a2e195cb01002cb18 +size 74482 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 948e5367228..9f5cdd792cf 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:1783e2299851c1a82a57b5434cc5488c94967622f886c267fe5346c42f9c52cf -size 85709 +oid sha256:e1185e1fe56eb661dac60465841979a542055436aa5064663641164cef3a6dfd +size 85660 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 288474acc6d..c122a69c83d 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:a8b5f3892fa9bddde1217f328ba39b9aa99080c2dfd68e50221361e9529c0dfa -size 62559 +oid sha256:ec74265bb93b5699a79918986ac601c3c2b2eb04b8aac0c72e50e57432ff9c27 +size 62992 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 733a723c6a3..3c8023bc834 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:58eaadc8d90b16008dd83040c4bf6d3bfc9fc8eb6bcb272bedf47317977dd14b -size 60814 +oid sha256:54d61d2749c5bf4194658359c580616412da386454cf7d06cd7546b6555f52ad +size 62033 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 6bd49bfd243..c6c0b542f24 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:509f8ee34c3d526f3c96578dbe00c9dd63e2178bea8ea7edc8467cd12cf8f622 -size 69346 +oid sha256:a23bf0e7a2a2874121ba56dd144316df816432f1989fa8b9d05278e067175b75 +size 69928 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 dcf27b2bb2b..cecb90d4c8f 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:c666853bb32e576090f3d314097dbe7d2daa85bad949ce9c3ad152847c743a0e -size 59726 +oid sha256:2958634d33600d8e1b2b49cd264e0dd3bc54ba4a4fa5d34b2fef5d195a0d30f1 +size 104699 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 3732d0b8f22..aef349c929f 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:19b169ca0a32a4aec13012495fc2d83ff53ab2d096e9d7630d2e84317302c67a -size 60548 +oid sha256:0834f567e0de98e55bc7607f5361637c00131c15f249594fd265ca4dafb4966c +size 61049 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 48b8b9f4610..b1da52d68ad 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:590a064309be74697bd0ec036c2112be286d16fbb944246f9091b3ffce93dc8e -size 62766 +oid sha256:bbf214dfd61e9da88625243ebbca73c68718d15dbb96a11ff6765e8135ceac72 +size 63119 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 b9461d92c88..e175b2e143e 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:e36d27009e23535f39ea20206af67a95368d0cc674d53e9085b111e48a6beea9 -size 69949 +oid sha256:a34b3acb890a44a29b58d27e91851e6329d2ef4cadbef5beebcbd21f0444e93e +size 70432 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 ccc32f4a1e3..7ef00f0247e 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:405f65309e783c2c099f45e6be528c05fbfddebd37accd9352a9208588b6e178 -size 60015 +oid sha256:fbcdc99699e06603d7e2e285d8eec6103b68dc829f45d29f5c781e932a375552 +size 60550 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 2685cf557a8..48e0faa0fb4 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:c6f039f8db0f5e95ad240697157dddd2115876deec42616a6b43ddcab6e4d9d8 -size 73593 +oid sha256:fa08671396751f9562ea6ee83bc353276bfe72524809ec44f666804c7c164413 +size 73614 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 80fa524c9a6..bcc13c40395 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:15a0cff9d79d66b032a5b8cf99e731299fe56bf8de7328c858745974bf776b8d -size 57060 +oid sha256:b82f3f8fe10b89962c5d3dedd593eee2f11b2c133e863b2bd1d540f83d07dd4f +size 57893 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 ac0672cc0bd..5ed1dc9981e 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:6ce61ea0b2c06cdd7b1ea0cf3dade22cc56c8434908d540b35752b3af6a6eaed -size 71709 +oid sha256:de1b9014a911c3a7154cad9c52f1d2e60d73efad6e626e10623c59652cff763c +size 71905 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 b29d8c5a40d..159c59edc06 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:5db1a345670e7ae8af6149e97bf1dc6362326f1de55e4884a95b248591562f3a -size 82831 +oid sha256:63a8eb5a1cc04455f270fce24811a389e7d274691e5b20a7df8595549ccbef87 +size 82985 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 c9992326af7..e652ef65db8 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:f455cd10db8660d33aa51b6c487d2dd2b86674d94a60a38a83cff3785ec48524 -size 60328 +oid sha256:d8648126878bcaf3bb0e05a26c052710627e00ece4e2567977b8b52497a940cd +size 61057 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 40c7888405b..32c4e328b62 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:f7b23306044dec23152302ab6ffa9f32d5d4eee6e95cdfb28453e04007e4906d -size 58692 +oid sha256:8ff99ca0e81db14b574c734ea5ba5d94c8a059b63b07502aa090113a776f6b22 +size 60195 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 8da7483264d..7f5038f397d 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:b3013792b094d1224e76b9428818083f4d586e759f3de412e6f1139df572b9d0 -size 67069 +oid sha256:b14c0f2ecd79fbed4a6c3a6b3459fe204cae344f42210987fb52eb9de836df6f +size 67953 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 e6a5a6b5645..7a1cd7f5224 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:d4744282f54264abc7cb833efb7b178acd6e334bf03905e0a0f5d1c21b427da9 -size 57653 +oid sha256:841cbfe819f61338f25484e63916e50d07aa258782b50c6108a71fa10a792fea +size 101688 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 682e6c8ecfa..e1e4aad0f8e 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:c94c6a421aba335a6af4e6548d0ceba7f09e1dfe34f8813072ddff943eea8aab -size 58312 +oid sha256:8e009d6cc28b8d98ae9c72e70a81af22d62b4428fae5dd7b10589d7aa46f1bf4 +size 59075 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 242e5b0c0f8..9079286c361 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:76de82231ffb2d5f39bd5e393159495b1c0f84710c84d69fe678a7831cc383bd -size 60615 +oid sha256:74398889e5df4d87fd34196bccd07b0a9d8dd6c545613fb1a7a0c1aee26f37a4 +size 61167 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 7fba8057e4a..d8c49bb94a9 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:ea57d3607d3a9f1db5a0f7a111de31859c67c17da09cf0b0fb26485c59e41eff -size 67533 +oid sha256:a89fecb881a6ae6ce48e23abe2e3f3ed35e29f7a0b231c0c467b6dff022ad77b +size 68284 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 fc9cc280cd9..5a8a7b6faaf 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:14082dae20c3a415221fabda507a6d0b338c04f114319fd53bbcf4b1ec4da549 -size 57868 +oid sha256:16e4ac5820b57fe720e3c3b1d8383661b9d90ea230be19955f525ac7df346741 +size 58689 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en.png new file mode 100644 index 00000000000..ad27b617df1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:899158f6f0ece99609f76ed9d3ee23489043b0bfdf4daa0264bef52814b8af15 +size 56923 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en.png new file mode 100644 index 00000000000..34951203a51 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8b9c61ee214c3d222eda082120ae681ef5b1e4359c65d34d6d08106403aeef6 +size 54539 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png index 05cb8123c29..2c582cdea18 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:663a2f1bada754dd92043b26fdafdcb4974f1dfe8368e4126bf41baa4356e0ac -size 46655 +oid sha256:d5e4def8107b07a630a8991b20f541768bf8090e0cd964bb3c5b524955f12318 +size 46869 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png index 6bf091cc3e4..52d5770cf98 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cca813483d17bafd6b2c09759659525b59909d76b90e124e03e7f8e0a161121e -size 44476 +oid sha256:11633c6e4562ab6a3144d80ff630574bc800a880a2804edf986c1fc8aeeff6b7 +size 44873 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png new file mode 100644 index 00000000000..395292f84e6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cd1be9d2376be6970c2612514c342346b18779fd3b3eace3b33221fccb1a7d9 +size 37552 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png new file mode 100644 index 00000000000..5754e839e1d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98a7981891ddddcc24340b37c8d35602deab9374184f37b102886637f1e7d4ff +size 35741 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png index be6cdff9359..97965f22c39 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:884c4844e2b54a83fbc78cb81262fac15d99d3c45cc51888ec8666bb0bd8bf38 -size 26260 +oid sha256:acde66c5c538f2f3170d9908ebcad7a3f3938e1a4904268b8c35236886c979b0 +size 26501 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png index ba7be13749f..709965cc42c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64411c282cfe9c9100df5dc4b503a9c7544252286ccf17ec9c64af73e1b0616b -size 25275 +oid sha256:3f7890a2ee68ed75e1a14f125e53b101e52f6b4738efd828d9099661b44f4f8c +size 25606 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en.png new file mode 100644 index 00000000000..f8ec4377526 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:673fc1787251cbd1f0d6ea6971c1c7a35b4a990e47adbf5b3e2ac442cf74f809 +size 12656 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en.png new file mode 100644 index 00000000000..9afa0c1a04e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a778dc2f98f1ad4ee901e4ca0d9b593cb78f44ab67337350349869f4b211a51 +size 44773 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en.png new file mode 100644 index 00000000000..3f76ccdc772 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b32c39383aef9ed41ba65109d4ee5e97f057221474aeeb90522c161d3b0c2409 +size 21014 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en.png new file mode 100644 index 00000000000..8bd1f33ecbf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97c2af47763d65926e407c94901e1b0a691218b1cf152203fc4fd2f396c17e43 +size 12270 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en.png new file mode 100644 index 00000000000..b103e9e094a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfb8a1f9bd060141b0f20490d4066724f34d1560296235f5e45f98de9d3d7aad +size 43310 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en.png new file mode 100644 index 00000000000..909315d2405 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4e8fb83c87caccd084c0321a5e47451c1a8bba3eed1e948e95d6184667f710 +size 19339 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en.png index ffbcbc631b8..1b2f5bc0892 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26942a8b605cecdf4d02a6586ced4741e4abcfa68a83dd79795ec2ff9fbd8f13 +oid sha256:d5188b829b027af8be3c1a002bfd3e849fde220e23a3619f238d2dad5d705fb0 size 20051 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en.png index f4536a9c5c8..cca6f78beb6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac685e19b0eed69e921113624641a913936c6741fb44f23c5310d14e8842b51e -size 30329 +oid sha256:112cfc55da3e9bf9337d4f92c2326d136891672792cdc2b32de1bbc4242d7dc1 +size 30330 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en.png index e0064fbb344..82cf7f9f3ba 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35407f64be6239445cf89fb4596bfe16c55559f11772b50a4eaf5ba10e9d93f6 -size 24254 +oid sha256:d866f9ae09c07e9127479bc795040be34e52051eddef10dc87eec295febfa5f1 +size 24252 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en.png index c1301573b8a..1e266033ae7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bedfc7fd98f62bfab69924e52f8efb30ce8864ed20bfeb428bc2b9db8a3727cc -size 36145 +oid sha256:cca8e1694334f4362b65c3fb0747ef4eb9e5c5541ea885ac2e4c968286b63947 +size 36149 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en.png index c285d5aac9a..5c6df6d650c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dad58a351e183d22ab5134aea812272e8668d28189c9e708e3cb695b226ec5a -size 25343 +oid sha256:e777d7db936fd5cf029b4a0792a1883fdbcb65c755d63305e40802fd979b8859 +size 25345 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en.png index 51c23db4c0f..faddf60fe3f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d6c785b7d8895df8e3afa9638a9077746329c13744a8908e5b8267d194f2cf3 +oid sha256:5cd3833e9d8f467762d92578da1eb6f9fafe0d698c52123cd110c52150b6fd6f size 19481 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en.png index 6c6ebc7e81c..89d3b794810 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31835ace76d50a148c93278f333cb87454d552cd4e048673b4e19e6253384cb3 +oid sha256:22bc0a665144ce8a324acac65eddc61e01830583a55489a4de8289d17554ce62 size 29360 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en.png index 06249f4a34f..4771961e255 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ff92f4fe39b40c7b9f8d4b2ac192152777afc76584fec1db28dc9b6dc1438d0 -size 23553 +oid sha256:1ad6a1ed15e86cdea1a6279ed9a2855a24b0f9f5c138102f9dafb831a238a386 +size 23540 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en.png index 2c7a0e96c3b..c6e57b87093 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0170fa8112b811db35c86322058586e2e705a610ae9daa95736b3d67d6d6752 -size 34756 +oid sha256:157c4724e207389c2384f26b9d43f43ec1976288513637d9f38a7ed4e040a68c +size 34757 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en.png index 4a9873522d3..cc988c17cec 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be6920f335a793069223714c25aec71c530310200bbddb148f0f578987544122 -size 24500 +oid sha256:c1d4a43b869b5355107ba0fe2a601a66a3ce3cfa5931c8ebdacb88476cedf79d +size 24499 diff --git a/tools/adb/deeplink_matrix.sh b/tools/adb/deeplink_matrix.sh new file mode 100755 index 00000000000..f11d810ec9f --- /dev/null +++ b/tools/adb/deeplink_matrix.sh @@ -0,0 +1,9 @@ +#! /bin/bash + +# 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. + +adb shell am start -a android.intent.action.VIEW \ + -d "matrix:r/element-android:matrix.org" diff --git a/tools/adb/deeplink_mobile.sh b/tools/adb/deeplink_mobile.sh new file mode 100755 index 00000000000..aefb63f5be3 --- /dev/null +++ b/tools/adb/deeplink_mobile.sh @@ -0,0 +1,12 @@ +#! /bin/bash + +# 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. + +# Format is: +# https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org + +adb shell am start -a android.intent.action.VIEW \ + -d "https://mobile.element.io/element/?account_provider=element.io\\&login_hint=mxid:@alice:element.io" diff --git a/tools/adb/disable_app_standby.sh b/tools/adb/disable_app_standby.sh new file mode 100755 index 00000000000..311921aa33a --- /dev/null +++ b/tools/adb/disable_app_standby.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# 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. + +# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby + +echo " => Standby OFF" + +set -x +package_name="io.element.android.x.debug" +adb shell dumpsys battery reset +adb shell am set-inactive "${package_name}" false +adb shell am get-inactive "${package_name}" + +tools/adb/print_device_state.sh diff --git a/tools/adb/disable_doze_mode.sh b/tools/adb/disable_doze_mode.sh new file mode 100755 index 00000000000..b0f00a76eb8 --- /dev/null +++ b/tools/adb/disable_doze_mode.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# 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. + +# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze + +echo " => Disable doze mode" + +set -x +adb shell dumpsys deviceidle unforce +adb shell dumpsys battery reset + +tools/adb/print_device_state.sh diff --git a/tools/adb/disable_talkback.sh b/tools/adb/disable_talkback.sh new file mode 100755 index 00000000000..b16b693a0ad --- /dev/null +++ b/tools/adb/disable_talkback.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# 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. + +adb shell settings put secure enabled_accessibility_services null + diff --git a/tools/adb/enable_app_standby.sh b/tools/adb/enable_app_standby.sh new file mode 100755 index 00000000000..393bfa14b96 --- /dev/null +++ b/tools/adb/enable_app_standby.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# 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. + +# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby + +echo " => Standby ON" + +set -x +package_name="io.element.android.x.debug" +adb shell dumpsys battery unplug +adb shell am set-inactive "${package_name}" true +adb shell am get-inactive "${package_name}" + +tools/adb/print_device_state.sh diff --git a/tools/adb/enable_doze_mode.sh b/tools/adb/enable_doze_mode.sh new file mode 100755 index 00000000000..5eb9d23c929 --- /dev/null +++ b/tools/adb/enable_doze_mode.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# 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. + +# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze + +echo " => Enable doze mode" + +set -x +adb shell dumpsys battery unplug +adb shell dumpsys deviceidle force-idle + +tools/adb/print_device_state.sh diff --git a/tools/adb/enable_talkback.sh b/tools/adb/enable_talkback.sh new file mode 100755 index 00000000000..96f815f685e --- /dev/null +++ b/tools/adb/enable_talkback.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# 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. + +adb shell settings put secure enabled_accessibility_services com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService diff --git a/tools/adb/oidc.sh b/tools/adb/oidc.sh index 850495d921e..6772977e83a 100755 --- a/tools/adb/oidc.sh +++ b/tools/adb/oidc.sh @@ -8,7 +8,7 @@ # Format is: # Error -# adb shell am start -a android.intent.action.VIEW -d "io.element:/callback?error=access_denied\\&state=IFF1UETGye2ZA8pO" +# adb shell am start -a android.intent.action.VIEW -d "io.element.android:/?error=access_denied\\&state=IFF1UETGye2ZA8pO" # Success -adb shell am start -a android.intent.action.VIEW -d "io.element:/callback?state=IFF1UETGye2ZA8pO\\&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" +adb shell am start -a android.intent.action.VIEW -d "io.element.android:/?state=IFF1UETGye2ZA8pO\\&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" diff --git a/tools/adb/print_device_state.sh b/tools/adb/print_device_state.sh new file mode 100755 index 00000000000..4d0061fe75d --- /dev/null +++ b/tools/adb/print_device_state.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# 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. + +# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze + +echo " => Device state" + +set -x +adb shell dumpsys deviceidle get light +adb shell dumpsys deviceidle get deep +adb shell dumpsys deviceidle get force +adb shell dumpsys deviceidle get screen +adb shell dumpsys deviceidle get charging +adb shell dumpsys deviceidle get network diff --git a/tools/dependencies/checkDependencies.py b/tools/dependencies/checkDependencies.py index 3067e322617..29ef7ea7fcb 100755 --- a/tools/dependencies/checkDependencies.py +++ b/tools/dependencies/checkDependencies.py @@ -56,7 +56,7 @@ def checkThatThereIsNoTestDependency(dependencies): continue else: subProject = line.split(" ")[-1] - if subProject.endswith(":test") or ":tests:" in subProject: + if subProject.endswith(":test") or ":tests:" in subProject and "detekt-rules" not in subProject: error = "Error: '" + currentProject + "' depends on the test project '" + subProject + "'\n" error += " Please replace occurrence(s) of 'implementation(projects" + subProject.replace(":", ".") + ")'" error += " with 'testImplementation(projects" + subProject.replace(":", ".") + ")'." diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 0ebe19bcc43..9af20180c6d 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -227,8 +227,10 @@ Compose: - LocalMediaItemPresenterFactories - LocalTimelineItemPresenterFactories - LocalRoomMemberProfilesCache - - LocalMentionSpanTheme + - LocalMentionSpanUpdater - LocalAnalyticsService + - LocalBuildMeta + - LocalUiTestMode CompositionLocalNaming: active: true ContentEmitterReturningValues: diff --git a/tools/localazy/checkForbiddenTerms.py b/tools/localazy/checkForbiddenTerms.py index 5d04743529b..2ebe582ba3d 100755 --- a/tools/localazy/checkForbiddenTerms.py +++ b/tools/localazy/checkForbiddenTerms.py @@ -23,6 +23,7 @@ "screen_onboarding_welcome_title", # Contains "Element Call" "screen_incoming_call_subtitle_android", + "call_invalid_audio_device_bluetooth_devices_disabled", # Contains "Element X" "screen_room_timeline_legacy_call", ] diff --git a/tools/localazy/config.json b/tools/localazy/config.json index b75c0380ffe..9d8d1f09b71 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -4,7 +4,8 @@ "name" : ":appnav", "includeRegex" : [ "banner\\.migrate_to_native_sliding_sync\\.force_logout.title", - "banner\\.migrate_to_native_sliding_sync\\.action" + "banner\\.migrate_to_native_sliding_sync\\.action", + "banner\\.migrate_to_native_sliding_sync\\.app_force_logout\\.title" ] }, { @@ -36,13 +37,8 @@ { "name" : ":features:roomaliasresolver:impl", "includeRegex" : [ - "screen_room_alias_resolver_.*" - ] - }, - { - "name" : ":features:onboarding:impl", - "includeRegex" : [ - "screen_onboarding_.*" + "screen_room_alias_resolver_.*", + "screen.join_room.loading_alert_title" ] }, { @@ -54,7 +50,9 @@ { "name" : ":features:invite:impl", "includeRegex" : [ - "screen_invites_.*" + "screen_invites_.*", + "screen\\.join_room\\.decline_and_block_.*", + "screen\\.decline_and_block\\..*" ] }, { @@ -62,7 +60,9 @@ "includeRegex" : [ "screen_create_room_.*", "screen\\.create_room\\..*", - "screen_start_chat_.*" + "screen_start_chat_.*", + "screen\\.start_chat\\..*", + "screen_room_directory_search_title*" ] }, { @@ -143,6 +143,7 @@ { "name" : ":features:login:impl", "includeRegex" : [ + "screen_onboarding_.*", "screen_login_.*", "screen_server_confirmation_.*", "screen_change_server_.*", @@ -161,17 +162,17 @@ ] }, { - "name" : ":features:roomlist:impl", + "name" : ":features:home:impl", "includeRegex" : [ "screen_roomlist_.*", + "screen\\.roomlist\\..*", "session_verification_banner_.*", "confirm_recovery_key_banner_.*", "banner\\.set_up_recovery\\..*", - "banner\\.migrate_to_native_sliding_sync\\..*", + "banner\\.battery_optimization\\..*", "full_screen_intent_banner_.*", "screen_migration_.*", - "screen_invites_.*", - "screen\\.join_room\\.knock_sent_title" + "screen_invites_.*" ] }, { @@ -210,6 +211,7 @@ "screen_room_message.*", "screen_room_retry.*", "screen_room_timeline.*", + "screen\\.room_timeline.*", "screen_room_typing.*" ] }, @@ -229,7 +231,9 @@ "name" : ":features:ftue:impl", "includeRegex" : [ "screen_welcome_.*", - "screen_notification_optin_.*" + "screen_notification_optin_.*", + "screen_identity_.*", + "screen_session_verification_enter_recovery_key" ] }, { @@ -240,6 +244,12 @@ "screen_polls_history_.*" ] }, + { + "name" : ":features:poll:api", + "includeRegex" : [ + "a11y\\.polls\\..*" + ] + }, { "name" : ":features:securebackup:impl", "includeRegex" : [ @@ -267,13 +277,15 @@ { "name" : ":libraries:troubleshoot:impl", "includeRegex" : [ - "troubleshoot_notifications_screen_.*" + "troubleshoot_notifications_screen_.*", + "screen\\.push_history\\..*" ] }, { "name" : ":libraries:matrixui", "includeRegex" : [ - "screen_invites_invited_you" + "screen_invites_invited_you", + "screen\\.bottom_sheet\\.create_dm\\..*" ] }, { @@ -310,6 +322,18 @@ "screen\\.room\\.single_knock_request.*", "screen\\.room\\.multiple_knock_requests.*" ] + }, + { + "name" : ":features:reportroom:impl", + "includeRegex" : [ + "screen\\.report_room\\..*" + ] + }, + { + "name" : ":features:roommembermoderation:impl", + "includeRegex" : [ + "screen\\.bottom_sheet\\.manage_room_member\\..*" + ] } ] } diff --git a/tools/localazy/downloadStrings.sh b/tools/localazy/downloadStrings.sh index ffd7a2eaef6..9d855115c30 100755 --- a/tools/localazy/downloadStrings.sh +++ b/tools/localazy/downloadStrings.sh @@ -38,14 +38,6 @@ if [[ $allFiles == 1 ]]; then find . -name 'translations.xml' -exec ./tools/localazy/formatXmlResourcesFile.py {} \; fi -set +e -echo "Moving files from values-id to values-in..." -find . -type d -name 'values-id' -execdir mv {}/translations.xml {}/../values-in/translations.xml \; 2> /dev/null - -echo "Deleting all the folders values-id..." -find . -type d -name 'values-id' -exec rm -rf {} \; 2> /dev/null -set -e - echo "Checking forbidden terms..." find . -name 'localazy.xml' -exec ./tools/localazy/checkForbiddenTerms.py {} \; if [[ $allFiles == 1 ]]; then diff --git a/tools/localazy/generateLocalazyConfig.py b/tools/localazy/generateLocalazyConfig.py index bddaf6bc451..cab23b17eea 100755 --- a/tools/localazy/generateLocalazyConfig.py +++ b/tools/localazy/generateLocalazyConfig.py @@ -65,7 +65,10 @@ def convertModuleToPath(name): "excludeKeys": list(map(lambda i: "REGEX:" + i, excludeRegex)), "conditions": [ "!equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json" - ] + ], + "langAliases": { + "id": "in" + } } allActions.append(actionTranslation) allRegexToExcludeFromMainModule.extend(entry["includeRegex"]) @@ -88,7 +91,10 @@ def convertModuleToPath(name): "excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)), "conditions": [ "!equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json" - ] + ], + "langAliases": { + "id": "in" + } } allActions.append(mainActionTranslation) diff --git a/tools/localazy/importSupportedLocalesFromLocalazy.py b/tools/localazy/importSupportedLocalesFromLocalazy.py index e58426e3753..526dc23b4f4 100755 --- a/tools/localazy/importSupportedLocalesFromLocalazy.py +++ b/tools/localazy/importSupportedLocalesFromLocalazy.py @@ -36,6 +36,8 @@ def normalizeForResourceConfigurations(locale): return "pt-rBR" case "zh#Hans": return "zh-rCN" + case "en_US": + return "en-rUS" case _: return locale diff --git a/tools/release/release.sh b/tools/release/release.sh index 735c57503ef..4120a1da06c 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -62,12 +62,13 @@ if [ ${envError} == 1 ]; then exit 1 fi -minSdkVersion=24 +# Read minSdkVersion from file plugins/src/main/kotlin/Versions.kt +minSdkVersion=$(grep "MIN_SDK_FOSS =" ./plugins/src/main/kotlin/Versions.kt |cut -d '=' -f 2 |xargs) buildToolsVersion="35.0.0" buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}" if [[ ! -d ${buildToolsPath} ]]; then - printf "Fatal: ${buildToolsPath} folder not found, ensure that you have installed the SDK version ${buildToolsVersion}.\n" + printf "Fatal: %s folder not found, ensure that you have installed the SDK version %s.\n" "${buildToolsPath}" "${buildToolsVersion}" exit 1 fi @@ -99,7 +100,7 @@ versionYearCandidate=$(date +%y) currentVersionMonth=$(grep "val versionMonth" ${versionsFile} | cut -d " " -f6) # Get current month on 2 digits versionMonthCandidate=$(date +%m) -versionMonthCandidateNoLeadingZero=$(echo ${versionMonthCandidate} | sed 's/^0//') +versionMonthCandidateNoLeadingZero=${versionMonthCandidate/#0/} currentVersionReleaseNumber=$(grep "val versionReleaseNumber" ${versionsFile} | cut -d " " -f6) # if the current month is the same as the current version, we increment the release number, else we reset it to 0 if [[ ${currentVersionMonth} -eq ${versionMonthCandidateNoLeadingZero} ]]; then @@ -109,17 +110,17 @@ else fi versionCandidate="${versionYearCandidate}.${versionMonthCandidate}.${versionReleaseNumberCandidate}" -read -p "Please enter the release version (example: ${versionCandidate}). Format must be 'YY.MM.x' or 'YY.MM.xy'. Just press enter if ${versionCandidate} is correct. " version +read -r -p "Please enter the release version (example: ${versionCandidate}). Format must be 'YY.MM.x' or 'YY.MM.xy'. Just press enter if ${versionCandidate} is correct. " version version=${version:-${versionCandidate}} # extract year, month and release number for future use versionYear=$(echo "${version}" | cut -d "." -f1) versionMonth=$(echo "${version}" | cut -d "." -f2) -versionMonthNoLeadingZero=$(echo ${versionMonth} | sed 's/^0//') +versionMonthNoLeadingZero=${versionMonth/#0/} versionReleaseNumber=$(echo "${version}" | cut -d "." -f3) printf "\n================================================================================\n" -printf "Starting the release ${version}\n" +printf "Starting the release %s\n" "${version}" git flow release start "${version}" # Note: in case the release is already started and the script is started again, checkout the release branch again. @@ -146,7 +147,7 @@ fastlaneFile="20${versionYear}${versionMonth}${versionReleaseNumber2Digits}0.txt fastlanePathFile="./fastlane/metadata/android/en-US/changelogs/${fastlaneFile}" printf "Main changes in this version: TODO.\nFull changelog: https://github.com/element-hq/element-x-android/releases" > "${fastlanePathFile}" -read -p "I have created the file ${fastlanePathFile}, please edit it and press enter to continue. " +read -r -p "I have created the file ${fastlanePathFile}, please edit it and press enter to continue. " git add "${fastlanePathFile}" git commit -a -m "Adding fastlane file for version ${version}" @@ -155,11 +156,11 @@ printf "OK, finishing the release...\n" git flow release finish "${version}" printf "\n================================================================================\n" -read -p "Done, push the branch 'main' and the new tag (yes/no) default to yes? " doPush +read -r -p "Done, push the branch 'main' and the new tag (yes/no) default to yes? " doPush doPush=${doPush:-yes} if [ "${doPush}" == "yes" ]; then - printf "Pushing branch 'main' and tag 'v${version}'...\n" + printf "Pushing branch 'main' and tag 'v%s'...\n" "${version}" git push origin main git push origin "v${version}" else @@ -172,7 +173,7 @@ git checkout develop printf "\n================================================================================\n" printf "The GitHub action https://github.com/element-hq/element-x-android/actions/workflows/release.yml?query=branch%%3Amain should have start a new run.\n" -read -p "Please enter the url of the run, no need to wait for it to complete (example: https://github.com/element-hq/element-x-android/actions/runs/9065756777): " runUrl +read -r -p "Please enter the url of the run, no need to wait for it to complete (example: https://github.com/element-hq/element-x-android/actions/runs/9065756777): " runUrl targetPath="./tmp/Element/${version}" @@ -189,7 +190,7 @@ while [[ $ret -ne 0 ]]; do ret=$? if [[ $ret -ne 0 ]]; then - read -p "Error while downloading the artifacts. You may want to fix the issue and retry. Retry (yes/no) default to yes? " doRetry + read -r -p "Error while downloading the artifacts. You may want to fix the issue and retry. Retry (yes/no) default to yes? " doRetry doRetry=${doRetry:-yes} if [ "${doRetry}" == "no" ]; then exit 1 @@ -224,7 +225,7 @@ cp "${fdroidTargetPath}"/app-fdroid-arm64-v8a-release.apk \ --ks-pass pass:"${keyStorePassword}" \ --ks-key-alias elementx \ --key-pass pass:"${keyPassword}" \ - --min-sdk-version ${minSdkVersion} \ + --min-sdk-version "${minSdkVersion}" \ "${fdroidTargetPath}"/app-fdroid-arm64-v8a-release-signed.apk cp "${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release.apk \ @@ -236,7 +237,7 @@ cp "${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release.apk \ --ks-pass pass:"${keyStorePassword}" \ --ks-key-alias elementx \ --key-pass pass:"${keyPassword}" \ - --min-sdk-version ${minSdkVersion} \ + --min-sdk-version "${minSdkVersion}" \ "${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release-signed.apk cp "${fdroidTargetPath}"/app-fdroid-x86-release.apk \ @@ -248,7 +249,7 @@ cp "${fdroidTargetPath}"/app-fdroid-x86-release.apk \ --ks-pass pass:"${keyStorePassword}" \ --ks-key-alias elementx \ --key-pass pass:"${keyPassword}" \ - --min-sdk-version ${minSdkVersion} \ + --min-sdk-version "${minSdkVersion}" \ "${fdroidTargetPath}"/app-fdroid-x86-release-signed.apk cp "${fdroidTargetPath}"/app-fdroid-x86_64-release.apk \ @@ -260,7 +261,7 @@ cp "${fdroidTargetPath}"/app-fdroid-x86_64-release.apk \ --ks-pass pass:"${keyStorePassword}" \ --ks-key-alias elementx \ --key-pass pass:"${keyPassword}" \ - --min-sdk-version ${minSdkVersion} \ + --min-sdk-version "${minSdkVersion}" \ "${fdroidTargetPath}"/app-fdroid-x86_64-release-signed.apk printf "\n================================================================================\n" @@ -276,10 +277,10 @@ printf "File app-fdroid-x86_64-release-signed.apk:\n" "${buildToolsPath}"/aapt dump badging "${fdroidTargetPath}"/app-fdroid-x86_64-release-signed.apk | grep package printf "\n" -read -p "Does it look correct? Press enter when it's done. " +read -r -p "Does it look correct? Press enter when it's done. " printf "\n================================================================================\n" -printf "The APKs in ${fdroidTargetPath} have been signed!\n" +printf "The APKs in %s have been signed!\n" "${fdroidTargetPath}" printf "\n================================================================================\n" printf "Unzipping the Gplay artifact...\n" @@ -291,7 +292,7 @@ unsignedBundlePath="${gplayTargetPath}/app-gplay-release.aab" signedBundlePath="${gplayTargetPath}/app-gplay-release-signed.aab" printf "\n================================================================================\n" -printf "Signing file ${unsignedBundlePath} with build-tools version ${buildToolsVersion} for min SDK version ${minSdkVersion}...\n" +printf "Signing file %s with build-tools version %s for min SDK version %s...\n" "${unsignedBundlePath}" "${buildToolsVersion}" "${minSdkVersion}" cp "${unsignedBundlePath}" "${signedBundlePath}" @@ -301,7 +302,7 @@ cp "${unsignedBundlePath}" "${signedBundlePath}" --ks-pass pass:"${keyStorePassword}" \ --ks-key-alias elementx \ --key-pass pass:"${keyPassword}" \ - --min-sdk-version ${minSdkVersion} \ + --min-sdk-version "${minSdkVersion}" \ "${signedBundlePath}" printf "\n================================================================================\n" @@ -313,13 +314,13 @@ printf "Version name: " bundletool dump manifest --bundle="${signedBundlePath}" --xpath=/manifest/@android:versionName printf "\n" -read -p "Does it look correct? Press enter to continue. " +read -r -p "Does it look correct? Press enter to continue. " printf "\n================================================================================\n" -printf "The file ${signedBundlePath} has been signed and can be uploaded to the PlayStore!\n" +printf "The file %s has been signed and can be uploaded to the PlayStore!\n" "${signedBundlePath}" printf "\n================================================================================\n" -read -p "Do you want to build the APKs from the app bundle? You need to do this step if you want to install the application to your device. (yes/no) default to no " doBuildApks +read -r -p "Do you want to build the APKs from the app bundle? You need to do this step if you want to install the application to your device. (yes/no) default to no " doBuildApks doBuildApks=${doBuildApks:-no} if [ "${doBuildApks}" == "yes" ]; then @@ -328,12 +329,12 @@ if [ "${doBuildApks}" == "yes" ]; then --ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \ --overwrite - read -p "Do you want to install the application to your device? Make sure there is one (and only one!) connected device first. (yes/no) default to yes " doDeploy + read -r -p "Do you want to install the application to your device? Make sure there is one (and only one!) connected device first. (yes/no) default to yes " doDeploy doDeploy=${doDeploy:-yes} if [ "${doDeploy}" == "yes" ]; then printf "Installing apk for your device...\n" bundletool install-apks --apks="${gplayTargetPath}"/elementx.apks - read -p "Please run the application on your phone to check that the upgrade went well. Press enter to continue. " + read -r -p "Please run the application on your phone to check that the upgrade went well. Press enter to continue. " else printf "APK will not be deployed!\n" fi @@ -345,14 +346,14 @@ printf "\n====================================================================== printf "Create the open testing release on GooglePlay.\n" printf "On GooglePlay console, go the the open testing section and click on \"Create new release\" button, then:\n" -printf " - upload the file ${signedBundlePath}.\n" +printf " - upload the file %s.\n" "${signedBundlePath}" printf " - copy the release note from the fastlane file.\n" printf " - download the universal APK, to be able to provide it to the GitHub release: click on the right arrow next to the \"App bundle\", then click on the \"Download\" tab, and download the \"Signed, universal APK\".\n" printf " - submit the release.\n" -read -p "Press enter to continue. " +read -r -p "Press enter to continue. " printf "You can then go to \"Publishing overview\" and send the new release for a review by Google.\n" -read -p "Press enter to continue. " +read -r -p "Press enter to continue. " printf "\n================================================================================\n" githubCreateReleaseLink="https://github.com/element-hq/element-x-android/releases/new?tag=v${version}&title=Element%20X%20Android%20v${version}" @@ -361,22 +362,22 @@ printf -- "Open this link: %s\n" "${githubCreateReleaseLink}" printf "Then\n" printf " - Click on the 'Generate releases notes' button.\n" printf " - Optionally reorder items and fix typos.\n" -printf " - Add the file ${signedBundlePath} to the GitHub release.\n" +printf " - Add the file %s to the GitHub release.\n" "${signedBundlePath}" printf " - Add the universal APK, downloaded from the GooglePlay console to the GitHub release.\n" -printf " - Add the 4 signed APKs for F-Droid, located at ${fdroidTargetPath} to the GitHub release.\n" -read -p ". Press enter to continue. " +printf " - Add the 4 signed APKs for F-Droid, located at %s to the GitHub release.\n" "${fdroidTargetPath}" +read -r -p ". Press enter to continue. " printf "\n================================================================================\n" printf "Update the project release notes:\n\n" -read -p "Copy the content of the release note generated by GitHub to the file CHANGES.md and press enter to commit the change. " +read -r -p "Copy the content of the release note generated by GitHub to the file CHANGES.md and press enter to commit the change. " printf "\n================================================================================\n" printf "Committing...\n" git commit -a -m "Changelog for version ${version}" printf "\n================================================================================\n" -read -p "Done, push the branch 'develop' (yes/no) default to yes? (A rebase may be necessary in case develop got new commits) " doPush +read -r -p "Done, push the branch 'develop' (yes/no) default to yes? (A rebase may be necessary in case develop got new commits) " doPush doPush=${doPush:-yes} if [ "${doPush}" == "yes" ]; then @@ -389,12 +390,12 @@ fi printf "\n================================================================================\n" printf "Message for the Android internal room:\n\n" message="@room Element X Android ${version} is ready to be tested. You can get it from https://github.com/element-hq/element-x-android/releases/tag/v${version}. You can install the universal APK. If you want to install the application from the app bundle, you can follow instructions [here](https://github.com/element-hq/element-x-android/blob/develop/docs/install_from_github_release.md). Please report any feedback. Thanks!" -printf "${message}\n\n" +printf "%s\n\n" "${message}" if [[ -z "${elementBotToken}" ]]; then - read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter to continue. " + read -r -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter to continue. " else - read -p "Send this message to the room (yes/no) default to yes? " doSend + read -r -p "Send this message to the room (yes/no) default to yes? " doSend doSend=${doSend:-yes} if [ "${doSend}" == "yes" ]; then printf "Sending message...\n" diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt index 0080f8d9052..c4f3b1b5b24 100644 --- a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt @@ -1,12 +1,12 @@ #if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text @@ -19,7 +19,7 @@ fun ${NAME}View( Box(modifier, contentAlignment = Alignment.Center) { Text( "${NAME} feature view", - color = MaterialTheme.colorScheme.primary, + color = ElementTheme.colors.textPrimary, ) } }